/* m-and-g-pools-global.css — Phase 6A canonical stylesheet (generated 2026-10-06)
   ============================================================================
   This file IS the client-approved stylesheet. Operator decision 2026-10-06:
   skill 5 did not hand over a design direction here, it handed over a finished,
   contrast-verified sheet the client signed off at mandg-pools.netlify.app.
   Generating a fresh R1-R56 baseline would not reproduce that, so none was.
   Byte-identical to _SKILL5_design/_working/styles.css (sha256 ad3701080c225f32)
   below this header. Do not edit here - edit the approved sheet and re-run 6A.
   Post-launch tweaks belong in client_custom.css, loaded after this file.
   ========================================================================= */
/* ============================================================================
   M & G POOLS — DESIGN PREVIEW — DIRECTION: "DEEP WATER" (round 3b)
   Skill 5 v10 / plugin 10.9.3 / 2026-09-18
   NEW brand board applied 2026-09-18: Primary Blue #4B4DED · Aqua #2891F3 ·
   Dark Navy #1E1E3A · Montserrat headings + Open Sans body. Direction is LAYOUT.
   ========================================================================= */
:root{
  /* ---- BRAND GUIDE v12 (2026-09-25), used verbatim --------------------------
     #4B4DED Brand Violet · #7666F8 Violet Glow · #2891F3 Sky Blue
     #FFCB4B Sunshine     · #1E1E3A Deep Indigo · #FFFFFF White
     Dark board adds      · #1E1E3A Dark Navy · #5BA9F6 Light Blue · #FFFFFF   */
  --primary:#4B4DED;       /* Primary Blue — CTA fill, headings/links on light. 6.85:1 on #fff */
  --primary-hover:#7666F8;
  --sky:#2891F3;           /* Brand Guide Sky Blue, verbatim. Links/icons/active states.
                              3.26:1 on white and 4.28/3.65 on the lifted dark grounds:
                              clears the 3:1 graphic floor everywhere, the 4.5 text floor
                              nowhere. So it is kept for the gradient and for graphics. */
  --aqua:var(--sky);  /* Sky Blue verbatim: 4.94:1 on Deep Indigo, which is now the only dark ground */
                              the band roles already guarantee. 6.49:1 on Deep Indigo,
                              4.80:1 on the lightest lift. 232 inline color:var(--txt-accent)
                              spans across 98 pages inherit the fix from this one line. */
  --aqua-hover:var(--sky);
  /* board Black and Gray retired: neither is a Brand Guide colour, --gray had no
     consumers, and the guide assigns Deep Indigo to text and headings. */
  --ink-navy:#1E1E3A;      /* board Dark Navy — deepest ground, label on aqua fills */
  --light-blue-d:var(--sky);

  --wash:#FFFFFF;          /* Brand Guide White — backgrounds, cards, clean space. The
                              old #EEF0FE tint was a derivation, not a guide colour. */
  --bg:#FFFFFF;            /* Brand Guide White */
  --white:#fff;
  --tint:rgba(75,77,237,.06);   /* Brand Violet at 6% — the hover/surface tint that used
                                   to be an off-white. A brand colour, not a grey. */
  --sunshine:#FFCB4B;      /* Brand Guide v12 — secondary accents, callouts. 1.51:1 on
                              white, so it is a FILL that carries dark text, never text. */
  --grad:linear-gradient(90deg,var(--primary-hover) 0%,var(--sky) 100%);  /* signature gradient */

  /* ---- ground derivatives: NON-TEXT surfaces only (L-045) -------------------
     Depth banding stepped off the board Dark Navy. No TEXT colour is derived. */
  --navy-deep:#1E1E3A;
  --navy:var(--navy-deep);      /* Brand Guide has ONE dark: Deep Indigo */
  --navy-lift:var(--navy-deep);

  /* ---- rendered text roles — every pair measured, see gate_results ---------- */
  --ink:var(--navy-deep);                 /* Deep Indigo on white    16.1:1  */
  --ink-strong:var(--primary);            /* headings on light        6.85:1 */
  --ink-muted:rgba(30,30,58,.72);         /* Deep Indigo at alpha on white  6.43:1 */
  --link-on-light:var(--primary);         /* 6.85 on #fff · 6.49 on --bg · 6.00 on --wash */
  --on-dark:var(--white);                 /* body on dark             17.39:1 */
  --on-dark-muted:rgba(255,255,255,.80);
  /* Sky Blue verbatim is 4.94:1 on Deep Indigo but only 4.28 / 3.65 on the two lifted
     dark grounds, and 3.26 on white - it clears the 3:1 graphic floor everywhere, not
     the 4.5 text floor. So the GUIDE colour stays for icons, actives and the gradient,
     and dark-band accent TEXT uses this lighter sky tint: 6.49 on Deep Indigo,
     4.80 on the lightest lift. Same split the previous board used (--light-blue-d). */
  --accent-on-dark:var(--aqua);

  --hair:rgba(255,255,255,.17);
  --hair-dark:rgba(0,0,0,.14);

  --ff:'Open Sans',system-ui,-apple-system,Segoe UI,sans-serif;          /* board BODY font */
  --ff-display:'Montserrat','Open Sans',system-ui,sans-serif;            /* board HEADINGS font */
  --container:1280px;
  --gutter:24px;
  --space-section:104px;
  --space-section-sm:64px;
  --r:5px;                  /* reference-measured dominant radius */
  --r-sig:30px 0 0 0;       /* reference-measured signature corner */
  --eyebrow-gap:12px;       /* locked sitewide */
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{
  margin:0;font-family:var(--ff);font-size:16px;line-height:1.65;
  color:var(--on-dark);background:var(--navy);
  overflow-x:hidden;
}
/* height:auto is REQUIRED alongside the width/height attributes we set for CLS —
   with both attributes specified the box has a used height, and `aspect-ratio`
   is then ignored. Every fixed-height image below sets its height explicitly. */
img{max-width:100%;display:block;height:auto}
a{color:inherit}

/* ---------- type: board pairing — Montserrat headings, Open Sans body ----------
   Display face lives on h1/h2 ONLY. Nav, buttons, card titles, table labels,
   eyebrows and footer headings stay Open Sans. Montserrat's max weight here is
   700 (the board lists Bold | SemiBold | Regular), so h1/h2 are 700, not 800. */
h1,h2,h3,h4{margin:0;font-weight:700;letter-spacing:-.01em;text-wrap:balance}
h1,h2{font-family:var(--ff-display)}
h1{font-size:clamp(2.05rem,1.35rem + 2.6vw,3.375rem);line-height:1.06;font-weight:700;letter-spacing:-.02em}
h2{font-size:clamp(1.55rem,1.24rem + 1.15vw,2.125rem);line-height:1.18;font-weight:700;letter-spacing:-.018em}
h3{font-size:1.6875rem;line-height:1.25}
h4{font-size:1.375rem;line-height:1.3}
p{margin:0 0 1.15em}
p:last-child{margin-bottom:0}
strong{font-weight:700}

.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.container{width:100%;max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.section{padding-block:var(--space-section);position:relative}
.section--tight{padding-block:calc(var(--space-section) * .68)}

/* ---------- ground bands ---------- */
.band-navy{background:var(--navy)}
.band-lift{background:var(--navy-lift)}
.band-deep{background:var(--navy-deep)}
.band-light{background:var(--wash);color:var(--ink)}       /* Brand Guide White #FFFFFF */
.band-light--bg{background:var(--bg)}
/* band-light and band-light--bg were told apart by a tint; both are White now, so on
   the 7 pages where two light sections sit adjacent the join needs a rule instead. */
.band-light + .band-light,.band-light + .band-white,.band-white + .band-light{
  --seamline:var(--hair-dark)}
/* The three dark grounds used to be three derived tones; the Brand Guide has ONE dark, so
   they are all Deep Indigo now and 64 adjacent pairs across 14 pages had merged into flat
   blocks. Same answer as the light bands: a hairline, drawn in white-at-alpha so no colour
   outside the guide is introduced. */
.band-navy + .band-navy,.band-navy + .band-lift,.band-navy + .band-deep,
.band-lift + .band-navy,.band-lift + .band-lift,.band-lift + .band-deep,
.band-deep + .band-navy,.band-deep + .band-lift,.band-deep + .band-deep{
  --seamline:var(--hair)}
.band-light h1,.band-light h2,.band-light h3,.band-light h4{color:var(--ink-strong)}
.band-light a{color:var(--link-on-light)}
.band-white{background:var(--white);color:var(--ink)}
.band-white h2,.band-white h3,.band-white h4{color:var(--ink-strong)}

/* caustic light ripple — the pool-theme texture, dark bands only */
.band-navy::before,.band-lift::before,.band-deep::before{
  content:"";position:absolute;inset:0;pointer-events:none;opacity:.05;mix-blend-mode:screen;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='520' height='520'%3E%3Cfilter id='c'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.014 0.03' numOctaves='3' seed='7'/%3E%3CfeColorMatrix values='0 0 0 0 0.6 0 0 0 0 0.9 0 0 0 0 1 0 0 0 -1.4 0.72'/%3E%3C/filter%3E%3Crect width='520' height='520' filter='url(%23c)'/%3E%3C/svg%3E");
}
.section > .container{position:relative;z-index:1}

/* ---------- the band seam ----------------------------------------------
   Operator 2026-09-25: the separator must line up with the section content, not
   run the full bleed. A border-top cannot do that - it always spans the border
   box - so the line is a pseudo-element sized to the container's CONTENT box:
   min(--container,100%) less the two gutters, which is exactly what
   .container{max-width:var(--container);padding-inline:var(--gutter)} resolves to.

   Drawn on every section but transparent by default; the adjacency rules just set
   --seamline. That leaves their long selector lists alone and puts the geometry in
   one place. .section > .container is already z-index:1 so this paints under the
   content, and .cta-band generates ::after last, so the line sits over its photo
   rather than beneath it. */
.section::after,.cta-band::after{
  content:"";position:absolute;top:0;left:50%;transform:translateX(-50%);
  height:1px;pointer-events:none;
  width:calc(min(var(--container),100%) - var(--gutter) * 2);
  background:var(--seamline,transparent)}

/* ---------- M7 #1 + #2 : the ONE universal section marker ---------- */
.sechead{margin-bottom:44px;max-width:62ch}
/* 62ch is 567px and the contact page's "None of this is paperwork..." lead-in measures
   588px on one line, so it dropped "van." onto a second line as a widow. 66ch gives
   604px - 16px of slack for font rendering - without touching the other 103 pages.
   Same specificity as .sechead (0,1,0), so it has to sit AFTER it to win. */
.sechead--wide{max-width:66ch}
.sechead--center{margin-inline:auto;text-align:center}
.eyebrow{
  display:block;font-size:.8125rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;
  color:var(--accent-on-dark);margin:0 0 var(--eyebrow-gap)
}
/* Eyebrow text is BODY-SIZE (13px/600) so it takes the 4.5:1 floor, not 3:1.
   --aqua is 2.77:1 on --wash and 3.29:1 on white: it fails as text on BOTH.
   On light grounds the eyebrow takes navy (11.49:1 on wash, 13.63:1 on white). */
.band-light .eyebrow,.band-white .eyebrow{color:var(--ink-strong)}
.wavemark{display:block;width:34px;height:auto;margin-bottom:14px;color:var(--txt-accent)}
.band-light .wavemark,.band-white .wavemark{color:var(--primary)}  /* same 3:1 graphic reason */
.sechead--center .wavemark{margin-inline:auto}
.sechead p{margin-top:18px;color:var(--on-dark-muted);font-size:1.0625rem}
.band-light .sechead p,.band-white .sechead p{color:var(--ink)}

/* ======================= SERVICE TABS (operator reference 2026-09-23) ==================
   The reference drives its tabs with React buttons that do nothing when JS is off. This build
   is progressive-enhancement first (same reason the slider is scroll-snap and the reveal is
   gated on .js), so the switch is RADIO INPUTS + :checked. No JS at all, arrow-key navigation
   works natively because they are radios, and the panel is real markup a crawler can read.
   The inputs are visually hidden but NOT display:none, or they would stop being focusable. */
.svctabs{position:relative}
.svctabs__in{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.svctabs__bar{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin-bottom:30px}
.svctabs__tab{display:inline-flex;align-items:center;min-height:48px;padding:12px 24px;
  border-radius:999px;border:1px solid var(--edge);background:var(--surface);
  color:var(--txt-strong);font-weight:700;font-size:.9375rem;line-height:1.2;cursor:pointer;
  transition:background-color .18s,border-color .18s,color .18s}
.svctabs__tab:hover{border-color:var(--txt-accent)}
/* Icons on the pills, as the reference puts a Lucide mark on its format cards. The droplet is
   the build's own; the sun and the snowflake are NEW - nothing resembling either existed at any
   viewBox, and I checked all 30 before drawing. Both follow the 16x16 / 1.5 stroke / round-cap
   convention the rest of the set uses. currentColor is deliberate: the mark is accent-coloured
   on a resting pill and turns white with the label on the active one, with no second rule. */
.svctabs__tab{gap:9px}
.svctabs__ic{flex:none;width:17px;height:17px;color:var(--txt-accent)}

/* align-items:stretch + a flex-filling image: the media column matches whichever side is
   taller, so the photo never leaves dead space under itself. Measured on the three tabs the
   gap was 145 / -9 / 32px - Pool Opening has the longest checklist. Tab 2's image is already
   the taller side, so it is unaffected; the rule is not scoped to one panel because the same
   gap appears on any tab whose list outgrows a 4:3 frame. */
.svcpanel{display:none;grid-template-columns:1fr 1fr;gap:clamp(24px,3.4vw,46px);align-items:stretch}
/* The image must FILL the column, not size it. flex:1 with aspect-ratio:auto let the photo
   fall back to its intrinsic 900x1200 portrait and drive every panel to 792px - taller than
   the content, which is the opposite of the fix. Absolute inset:0 gives the img no intrinsic
   contribution, so the grid row is set by the body column and the photo fills whatever that is. */
.svcpanel__media{position:relative;min-height:300px;
  border-radius:var(--r);overflow:hidden;border:1px solid var(--edge)}
.svcpanel__media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.svcpanel__price{position:absolute;left:18px;bottom:18px;display:flex;flex-direction:column;
  gap:2px;padding:12px 18px;border-radius:var(--r);background:var(--white);
  border-top:3px solid var(--primary)}
.svcpanel__price b{font-size:1.625rem;font-weight:800;line-height:1;color:var(--primary);
  font-variant-numeric:tabular-nums}
.svcpanel__price span{font-size:.75rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  color:rgba(0,0,0,.66)}
.svcpanel__body h3{font-size:1.5rem;color:var(--txt-strong);margin-bottom:18px}
.svcpanel__note{margin-top:18px;font-size:.875rem;line-height:1.6;color:var(--txt)}
.svcpanel__body .btn{margin-top:22px}
.svclist{list-style:none;margin:0;padding:0}
.svclist li{display:flex;gap:11px;align-items:flex-start;padding:9px 0;
  border-bottom:1px solid var(--rule);color:var(--txt);font-size:.9688rem;line-height:1.55}
.svclist li:last-child{border-bottom:0}
.svclist__ic{flex:none;width:17px;height:17px;margin-top:2px;color:var(--txt-accent)}

.svcmini{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-top:30px}
.svcmini__item{display:flex;align-items:center;gap:15px;padding:14px 18px 14px 14px;
  text-decoration:none;border:1px solid var(--edge);border-radius:var(--r);
  background:var(--surface);transition:background-color .18s,border-color .18s}
.svcmini__item:hover{border-color:var(--txt-accent)}
.svcmini__item img{flex:none;width:74px;height:56px;object-fit:cover;border-radius:calc(var(--r) - 6px)}
.svcmini__txt{min-width:0}
.svcmini__txt b{display:block;color:var(--txt-strong);font-size:.9688rem;line-height:1.3}
.svcmini__txt i{display:block;font-style:normal;font-size:.8125rem;color:var(--txt);margin-top:3px}
.svcmini__txt em{font-style:normal;opacity:.8}
.svcmini__item .btn__arrow{flex:none;margin-left:auto;color:var(--txt-accent)}

/* the :checked wiring - panel, active pill, and hiding the mini for whichever tab is open */
#svct1:checked ~ .svctabs__stage .svcpanel:nth-child(1),
#svct2:checked ~ .svctabs__stage .svcpanel:nth-child(2),
#svct3:checked ~ .svctabs__stage .svcpanel:nth-child(3){display:grid}
#svct1:checked ~ .svctabs__bar .svctabs__tab:nth-child(1),
#svct2:checked ~ .svctabs__bar .svctabs__tab:nth-child(2),
#svct3:checked ~ .svctabs__bar .svctabs__tab:nth-child(3){
  background:var(--primary);border-color:var(--primary);color:var(--white)}
#svct1:checked ~ .svctabs__bar .svctabs__tab:nth-child(1) .svctabs__ic,
#svct2:checked ~ .svctabs__bar .svctabs__tab:nth-child(2) .svctabs__ic,
#svct3:checked ~ .svctabs__bar .svctabs__tab:nth-child(3) .svctabs__ic{color:inherit}
#svct1:checked ~ .svcmini .svcmini__item:nth-child(1),
#svct2:checked ~ .svcmini .svcmini__item:nth-child(2),
#svct3:checked ~ .svcmini .svcmini__item:nth-child(3){display:none}
/* the radio is off-screen, so focus has to show on the label it drives */
#svct1:focus-visible ~ .svctabs__bar .svctabs__tab:nth-child(1),
#svct2:focus-visible ~ .svctabs__bar .svctabs__tab:nth-child(2),
#svct3:focus-visible ~ .svctabs__bar .svctabs__tab:nth-child(3){
  outline:3px solid var(--txt-accent);outline-offset:2px}

@media (max-width:900px){
  /* single column - nothing to match, so the photo goes back to a fixed ratio */
  .svcpanel{grid-template-columns:1fr;gap:26px}
  .svcpanel__media{aspect-ratio:4/3;min-height:0}
  .svcmini{grid-template-columns:1fr}
  .svctabs__tab{padding:12px 18px;font-size:.875rem}
}

/* Footnote panel, matching the reference's "Certified Material" note: a rounded, tinted,
   1px-bordered panel rather than a heavy 4px left bar, with the mark the operator's mock
   showed. The icon is the build's own info glyph - checked the full 30-icon set first. */
.svcnote{display:flex;align-items:center;gap:18px;margin-top:30px;padding:20px 24px;
  background:var(--surface);border:1px solid var(--edge);border-radius:var(--r)}
/* Circular badge, matching the .scard__ic device already on the site rather than inventing a
   second one - same tint, same ring, just smaller. Keeps one icon-badge treatment sitewide. */
.svcnote__badge{flex:none;width:48px;height:48px;border-radius:50%;display:grid;place-items:center;
  background:rgba(40,145,243,.16);border:1px solid rgba(40,145,243,.42)}
.svcnote__badge svg{color:var(--txt-accent)}
.svcnote .btn svg:first-child{flex:none;opacity:.9}
.svcnote p{margin:0;color:var(--txt);line-height:1.65}
@media (max-width:560px){.svcnote{padding:18px 18px;gap:12px}}

/* "What We Do Not Do" as a card grid (operator reference 2026-09-23). flex rather than grid
   so the incomplete last row CENTRES, which is the build's standard for card grids. The mark is
   the build's own X - one uniform badge across all eight, which suits a list of exclusions and
   adds nothing to the icon set. Hover matches .call / .scard / .statcard / .step. */
.donotgrid{display:flex;flex-wrap:wrap;justify-content:center;gap:18px;list-style:none;margin:0;padding:0}
.donotgrid li{flex:0 1 calc((100% - 36px) / 3);min-width:250px;
  display:flex;flex-direction:column;align-items:center;text-align:center;gap:13px;
  padding:30px 26px;border:1px solid var(--edge);border-top:3px solid var(--txt-accent);
  border-radius:var(--r);background:var(--surface);
  transition:background-color .18s,border-top-color .18s}
.donotgrid li:hover{background:rgba(40,145,243,.10);border-top-color:var(--bar-hover)}
.donotgrid__ic{flex:none;width:54px;height:54px;border-radius:50%;display:grid;place-items:center;
  background:rgba(40,145,243,.16);border:1px solid rgba(40,145,243,.42);color:var(--txt-accent)}
.donotgrid__ic svg{width:22px;height:22px}
.donotgrid b{color:var(--txt-strong);font-size:1.0625rem;line-height:1.35}
.donotgrid p{color:var(--txt);font-size:.9375rem;line-height:1.6;margin:0}
@media (max-width:900px){.donotgrid li{flex:0 1 calc((100% - 18px) / 2)}}
@media (max-width:560px){.donotgrid li{flex:0 1 100%}}

/* --rail: the reference's timeline treatment - circled number badges sitting ON a continuous
   connector, everything centred. A MODIFIER, not a change to .steps--media, because that
   component is also on /services/safety-cover-installation/ and must keep its own look.
   The rail spans badge-centre to badge-centre: with 4 equal columns those sit at 12.5% and
   87.5%, so the line stops under the outer badges instead of running off the ends. It is
   dropped below 900px, where the grid is no longer a single row and a horizontal rail would
   connect nothing. */
.steps--rail{position:relative}
/* MARGIN, not padding: the rail's connecting line is an absolutely positioned
   ::before at top:24px inside this container, so padding would push the circles down
   and leave the line stranded above them. Margins move the whole box and the line
   with it. Sibling margins collapse to the larger value, which is why these are the
   FINAL gaps rather than an addition - .sechead already contributes 44px above and
   the following <p> 28px below, so anything smaller would have changed nothing.
   Scoped to /twinsburg-oh/pool-safety-covers/; the other three .steps--rail pages
   keep the tighter default. */
.steps--pad{margin-top:72px;margin-bottom:56px}
@media (max-width:900px){ .steps--pad{margin-top:52px;margin-bottom:44px} }
/* .steps--media.steps--rail, not .steps--rail alone: the two tie at (0,2,0) and .steps--media
   sits ~1300 lines LATER, so it was winning. The badge kept the strip's 13px / display:flex /
   .1em tracking, which left the number small and flex-aligned instead of centred in the circle.
   (0,3,0) settles it regardless of order. */
.steps--media.steps--rail .steps__item{align-items:center;text-align:center;gap:13px}
.steps--media.steps--rail .steps__n{position:relative;z-index:2;
  width:48px;height:48px;flex:none;border-radius:50%;display:grid;place-items:center;
  background:var(--navy-deep);border:2px solid var(--txt-accent);color:var(--txt-accent);
  font-size:.9375rem;font-weight:800;letter-spacing:.02em;line-height:1}
/* The badge fill is OPAQUE on purpose - it punches a hole in the dotted connector that
   runs behind it - but --navy-deep is a hardcoded dark, so on a light band the accent
   number sat on near-black at 2.54:1. The ground has to follow the band like every other
   portable component; --surface is white on a light band, which puts #4B4DED on white. */
.band-light .steps--media.steps--rail .steps__n,
.band-white .steps--media.steps--rail .steps__n{background:var(--surface)}
.steps--media.steps--rail .steps__n::after{content:none}
/* NOT 12.5%. .steps--media carries gap:20px, so a column is (100% - 3*20px)/4, not 100%/4,
   and a percentage inset lands 7.5px short of the outer badge centres at four across
   (6.7px at three). The calc puts the dotted line exactly centre-to-centre. */
.steps--media.steps--rail::before{content:"";position:absolute;top:24px;
  left:calc((100% - 60px) / 8);right:calc((100% - 60px) / 8);
  border-top:2px dotted var(--rule);z-index:1;pointer-events:none}
.steps--media.steps--rail.steps--3::before{left:calc((100% - 40px) / 6);right:calc((100% - 40px) / 6)}
@media (max-width:900px){.steps--media.steps--rail::before{content:none}}

/* ---------- buttons (M7 #3) ---------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:.6em;
  min-height:48px;padding:13px 26px;border-radius:8px;border:2px solid transparent;
  /* Measured off Brand Guide v12 s.4: cap height 10px on a 44px button = ~14px, and a
     stem/cap of 0.200 = Bold. Scaled to this site's 48px buttons that is 15px/700,
     which is also what the header already used - so every button is now one value. */
  font-family:var(--ff);font-size:.9375rem;font-weight:700;line-height:1;text-decoration:none;
  text-transform:uppercase;letter-spacing:.04em;
  cursor:pointer;transition:background-color .18s,border-color .18s,color .18s,transform .18s,filter .18s,box-shadow .18s
}
.btn__arrow{transition:transform .2s;flex:none}
.btn:hover .btn__arrow{transform:translateX(4px)}
/* Brand Guide v12, section 4. Three button styles:
   primary  = signature gradient, white label  (the guide's "GET A FREE ESTIMATE")
   sunshine = #FFCB4B fill, Deep Indigo label  (the guide's "OUR SERVICES")
   outline  = white fill, violet border+label  (the guide's "CONTACT US", see .btn--outline)
   .btn--secondary IS this site's primary CTA - 337 uses, every "Request a Quote".
   2026-09-25 (operator): it no longer carries the Signature Gradient. White on the
   gradient measured 3.26:1 at the violet end and 4.18:1 at the blue end, and the label
   is 15px/700 - below the 18.66px-bold large-text threshold - so it needed 4.5:1 and
   got neither. Flat Brand Violet is 5.86:1 and is constant across the whole fill, so
   the CTA passes at every point of the button instead of at none of them.
   --grad stays defined: it is a guide element, just not a text ground. */
.btn--primary{background:var(--primary);color:var(--white);border-color:var(--primary)}   /* 5.86:1 */
/* The fill must NOT move on hover: Violet Glow under white is 4.18:1, the exact
   number this change removes. The state reads as a Violet Glow ring instead. */
.btn--primary:hover,.btn--secondary:hover{background:var(--primary);border-color:var(--primary-hover);box-shadow:0 0 0 3px rgba(118,102,248,.45)}
.btn--secondary{background:var(--primary);color:var(--white);border-color:var(--primary)}   /* 5.86:1 */
.btn--sunshine{background:var(--sunshine);color:var(--ink-navy);border-color:var(--sunshine)}  /* 10.7:1 */
.btn--sunshine:hover{filter:brightness(1.06)}
.btn--inverted{background:var(--white);color:var(--primary);border-color:var(--white)}     /* 6.85:1 */
.btn--inverted:hover{background:var(--aqua);border-color:var(--aqua);color:var(--ink-navy)}
/* The guide's SECOND button is the Sunshine fill ("OUR SERVICES"). On this site the
   second action in every CTA row is .btn--ghost-dark - 143 uses - so that is where it
   belongs. Deep Indigo on #FFCB4B is 10.7:1, which holds at any size on any ground,
   so it works in the hero, the CTA bands and the light sections alike. */
.btn--ghost-dark{background:var(--sunshine);color:var(--ink-navy);border-color:var(--sunshine)}
.btn--ghost-dark:hover{filter:brightness(1.06)}
/* Secondary navigational CTA (operator instruction 2026-09-22: "change to buttons").
   Outlined, so it never competes with the section's own primary action - the locked
   "one primary action per section" rule (Hick). BAND-AWARE because these rows sit on
   both grounds: a white label on --wash is unreadable. Dark bands take white
   (15.8:1 on --navy); light bands take --primary (4.17:1 on --wash, 4.94:1 on white),
   which also clears the 3:1 floor for the border as a non-text graphic. */
.btn--outline{background:transparent;color:var(--white);border-color:rgba(255,255,255,.55)}
.btn--outline:hover{background:var(--white);color:var(--primary);border-color:var(--white)}
.band-light .btn--outline,.band-white .btn--outline{color:var(--primary);border-color:var(--primary)}
.band-light .btn--outline:hover,.band-white .btn--outline:hover{background:var(--primary);color:var(--white);border-color:var(--primary)}
.btn-row{display:flex;flex-wrap:wrap;gap:14px}

/* arrow text link, no chrome */
.tlink{
  display:inline-flex;align-items:center;gap:.5em;font-weight:700;text-decoration:none;
  color:var(--accent-on-dark);border-bottom:2px solid transparent;transition:border-color .18s
}
.tlink:hover{border-bottom-color:currentColor}
.tlink svg{transition:transform .2s}
.tlink:hover svg{transform:translateX(4px)}
.band-light .tlink,.band-white .tlink{color:var(--link-on-light)}

/* ============================ PREVIEW CHROME (L-058) ===================== */
.pvbar{
  background:var(--navy-deep);color:var(--txt);font-size:.6875rem;font-weight:700;letter-spacing:.12em;
  text-transform:uppercase;padding:7px var(--gutter);text-align:center;position:relative;z-index:80
}
.pvbar b{color:var(--txt-accent)}

/* ============================ HEADER ==================================== */
.header{position:absolute;top:35px;left:0;right:0;z-index:70;transition:background-color .25s,box-shadow .25s}
.header-inner{
  position:relative;                      /* L-110: mega anchors to the CONTAINER */
  display:flex;align-items:center;gap:32px;padding-block:18px
}
.brand{flex:none;display:block;line-height:0}
.brand img{height:66px;width:auto}

/* ---- SCROLLED STATE: white header + the BLUE logo -------------------------
   Client request 2026-09-18. Everything in the bar has to switch together —
   the nav links are white on the transparent state and would render 1:1 on a
   white bar. The aqua underline is 2.03:1 on white and fails even the 3:1
   graphic floor, so it becomes Primary Blue here. */
.brand__blue{display:none}
.header.is-stuck{
  position:fixed;top:0;background:var(--white);
  /* Sunshine rule under the stuck header (2026-09-29, operator), the pattern on
     eb-stc-preview.netlify.app - there it is `nav.sticky-nav{border-bottom:1.6px
     solid <accent>}` on a white nav. This replaces the old .08 black hairline
     rather than sitting next to it.

     2px, set by the operator in DevTools against the live page. I had put 3px to
     match the card top rules, on the reasoning that Sunshine is only 1.51:1 on
     white and a thinner line would not register - but that contrast is against
     the header ABOVE it, and the edge the line actually marks is the dark hero
     BELOW, where it is 10.67:1. 2px is plenty there. */
  box-shadow:0 2px 18px rgba(30,30,58,.13);border-bottom:2px solid var(--sunshine)
}
.header.is-stuck .header-inner{padding-block:10px}
.header.is-stuck .brand img{height:54px}
.header.is-stuck .brand__white{display:none}
.header.is-stuck .brand__blue{display:block}
/* CHILD COMBINATORS ARE LOAD-BEARING HERE. The mega panel lives INSIDE `.nav`,
   so a descendant selector (`.header.is-stuck .nav a`) also repaints all 104
   mega links — which rendered --ink-navy on the --navy-deep panel at exactly
   1.00:1, invisible. That is L-109's signature on this very site, in reverse.
   These rules must only ever reach the TOP-LEVEL nav links. */
.header.is-stuck .nav > ul > li > a{color:var(--ink-navy)}            /* 17.39:1 on white */
.header.is-stuck .nav > ul > li > a:hover{color:var(--primary)}       /*  6.85:1 on white */
.header.is-stuck .nav > ul > li > a::after{background:var(--primary)} /* aqua would be 2.03:1 */
.header.is-stuck .burger{color:var(--ink-navy);border-color:rgba(0,0,0,.22)}
/* The mega is a dark panel in BOTH states, so it needs NO colour override here.
   An earlier `.header.is-stuck .mega a{color:...}` rule was (0,3,1) and silently
   beat `.mega a:hover` (0,2,1), killing the hover colour change in the scrolled
   state only. Same family as L-047. Do not re-add it. */

/* Flush the panel to the white bar when scrolled. `.header-inner`'s box ends 7px
   above the header's painted bottom edge (padding + border), so `top:100%` alone
   leaves a strip through which the page shows. Invisible while the bar is
   transparent; obvious once it is white. Asserted in the gate so it cannot drift. */
.header.is-stuck .mega{margin-top:1px}   /* exactly the bar-s 1px border-bottom */
.nav{margin-left:auto;display:flex;align-items:center;gap:30px}
.nav ul{list-style:none;display:flex;align-items:center;gap:30px;margin:0;padding:0}
.nav li{position:static}                  /* L-110: so the mega can use the container box */
.nav a{
  position:relative;display:inline-flex;align-items:center;gap:.35em;
  color:var(--white);text-decoration:none;font-size:.9375rem;font-weight:600;padding-block:10px
}
.nav a::after{                            /* M7 #4 */
  content:"";position:absolute;left:0;right:100%;bottom:2px;height:2px;
  background:var(--aqua);transition:right .25s
}
.nav a:hover::after,.nav a:focus-visible::after{right:0}
.nav .chev{width:10px;height:10px;flex:none;transition:transform .2s}
.nav li:hover .chev{transform:rotate(180deg)}
/* two-class scoping so .nav a cannot repaint the button (L-047) */
/* `.nav a` is (0,1,1) and outruns `.btn`, so nav buttons restate size and weight. */
.nav a.btn,.mobilebar a.btn{font-size:.9375rem;font-weight:700}

.nav .btn,.header .nav .btn--secondary{color:var(--white)}
.nav .btn::after{display:none}

/* ---- mega (M7 #7), anchored to the header container's RIGHT edge (L-110) ---- */
.has-mega > a::before{                    /* hover bridge, spans link bottom -> container bottom */
  content:"";position:absolute;left:-14px;right:-14px;top:100%;height:34px
}
/* FLUSH TO THE BAR. `top:100%` lands on `.header-inner`'s bottom, which IS the
   header's content edge — so margin 0 is flush, not 22px. In the scrolled state
   the bar adds `border-bottom:1px`, so the panel clears exactly that 1px below.
   Neither number is arbitrary: 0 = no border, 1 = the border width.
   Asserted in the gate, because measuring this before `document.fonts.ready`
   returns a header height that is still settling and gives a false reading. */
.mega{
  position:absolute;top:100%;right:0;margin-top:0;width:min(1080px,calc(100vw - 48px));
  background:var(--navy-deep);border:1px solid var(--hair);
  border-top:3px solid var(--aqua);            /* seals the join to the bar */
  border-radius:0 0 var(--r) var(--r);
  box-shadow:0 24px 64px rgba(0,0,0,.42);
  padding:30px;display:grid;gap:26px;grid-template-columns:repeat(4,1fr);
  opacity:0;visibility:hidden;transform:translateY(-8px);transition:opacity .2s,transform .2s,visibility .2s;
  z-index:75
}
.has-mega:hover .mega,.has-mega:focus-within .mega{opacity:1;visibility:visible;transform:none}
.mega__col .mega__label{display:block;margin-top:0;
  font-size:.75rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;
  color:var(--txt-accent);margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid var(--hair)
}
.mega__col ul{display:block;margin:0;padding:0;list-style:none}
.mega__col li{margin-bottom:3px}
.mega a{
  display:block;padding:8px 10px;border-radius:var(--r);font-weight:400;font-size:.875rem;
  color:var(--txt);line-height:1.35
}
.mega a::after{display:none}
.mega a:hover{background:rgba(40,145,243,.14);color:var(--white)}
.mega a b{display:block;font-weight:700;font-size:.9375rem;color:var(--white);margin-bottom:1px}
.mega a span{display:block;font-size:.78125rem;color:var(--txt);line-height:1.4}
.mega__all{grid-column:1/-1;border-top:1px solid var(--hair);padding-top:18px;margin-top:2px}
/* Areas mega: a flat list of places rather than a town-by-town tree. The list spans the
   whole panel and sets its own columns, because .mega's 4-col grid would otherwise put
   all 17 towns in a single column. The pin is the same glyph the coverage list uses. */
.mega--areas .mega__places{grid-column:1/-1;display:grid;grid-template-columns:repeat(4,1fr);
  gap:2px 20px;list-style:none;margin:0;padding:0}
.mega--areas .mega__places a{display:flex;align-items:center;gap:10px}
.mega--areas .mega__places svg{flex:none;width:15px;height:15px;color:var(--txt-accent)}
@media (max-width:1100px){.mega--areas .mega__places{grid-template-columns:repeat(3,1fr)}}
@media (max-width:900px){.mega--areas .mega__places{grid-template-columns:repeat(2,1fr)}}
@media (max-width:560px){.mega--areas .mega__places{grid-template-columns:1fr}}
.mega__col .mega__label a{color:inherit;text-decoration:none}
.mega__col .mega__label a:hover{color:var(--white)}
.mega__col .mega__label + ul{margin-bottom:18px}
.mega__col > .mega__label:not(:first-child){margin-top:4px}
/* Cap EVERY mega to the viewport, not just the tall one. The Services panel is
   654px at 4 columns but 1076px once the grid collapses to 3 below 1100px, so a
   rule scoped to one panel leaves the other running off the bottom of the screen. */
.mega{max-height:min(72vh,660px);overflow-y:auto;overscroll-behavior:contain}
.drawer .drawer__sub{padding-left:18px;opacity:.86;font-size:.875rem}
.drawer details li b{font-weight:700;color:var(--white)}

.burger{
  display:none;margin-left:auto;background:none;border:1px solid var(--hair);border-radius:var(--r);
  width:48px;height:48px;align-items:center;justify-content:center;cursor:pointer;color:var(--white)
}

/* ============================ HERO ====================================== */
/* The bottom padding has to TRACK the wave divider, not sit at a fixed value.
   .wavediv is an SVG pinned to the hero's bottom edge with height:auto, so it is a
   constant share of the viewport width - measured at 8.06% across 430px to 1600px
   (35px -> 129px). A flat 128/104px stops clearing it once the viewport passes
   ~1300px, and the hero trust row slid under the white wave and vanished. 8.1vw
   plus 26px of clearance holds at any width; max() keeps the old value wherever it
   is already the larger of the two, so nothing below ~1250px moves. */
.hero{
  position:relative;padding-top:168px;padding-bottom:max(128px,calc(8.1vw + 26px));
  overflow:hidden;  /* L-002: keep hero padding tight */
  /* The mid stop is a 55/45 mix of Brand Violet and Deep Indigo, not Brand Violet neat.
     The guide assigns Brand Violet to "buttons, accents" and Deep Indigo to "dark
     elements"; a whole hero field of #4B4DED is the former doing the latter's job, and
     it read as electric purple. The radial glow drops from .46 to .30 for the same
     reason - that alpha was tuned for the old, far less saturated #4554A5. */
  background:
    radial-gradient(120% 92% at 82% 12%, rgba(75,77,237,.30) 0%, rgba(75,77,237,0) 58%),
    linear-gradient(196deg, rgba(75,77,237,.24) 0%, rgba(75,77,237,.30) 34%,
                            rgba(75,77,237,.04) 72%, rgba(75,77,237,0) 100%),
    var(--navy-deep);
}
.hero::before{                            /* caustics over the hero ground */
  content:"";position:absolute;inset:0;opacity:.07;mix-blend-mode:screen;pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='620' height='620'%3E%3Cfilter id='h'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.011 0.026' numOctaves='4' seed='3'/%3E%3CfeColorMatrix values='0 0 0 0 0.55 0 0 0 0 0.88 0 0 0 0 1 0 0 0 -1.3 0.7'/%3E%3C/filter%3E%3Crect width='620' height='620' filter='url(%23h)'/%3E%3C/svg%3E")
}
/* Montserrat sets wider than Open Sans, so the copy column carries more of the
   grid than it did under the old pairing — otherwise the H1 breaks inside
   "In-Ground" at its real hyphen. */
.hero__grid{position:relative;z-index:2;display:grid;grid-template-columns:1.2fr .8fr;gap:52px;align-items:center}
.hero__eyebrow{
  display:inline-flex;align-items:center;gap:10px;font-size:.8125rem;font-weight:700;
  letter-spacing:.14em;text-transform:uppercase;color:var(--txt-accent);margin-bottom:20px
}
.hero__eyebrow::before{content:"";width:34px;height:3px;background:var(--aqua);border-radius:2px}
.hero h1{color:var(--white);margin-bottom:22px}
.hero__sub{font-size:1.0625rem;color:var(--txt);margin-bottom:30px}  /* 11.0:1 — no 88ch re-cap (L-091·3) */
.hero__cta{margin-bottom:34px}

/* media column over the full-bleed ground (the treatment L-064 permits) */
.hero__media{position:relative}
.hero__media img{
  width:100%;aspect-ratio:4/5;object-fit:cover;border-radius:var(--r-sig);
  border:1px solid rgba(255,255,255,.22)
}
/* Hero trust row (operator reference 2026-09-23). The eligibility notice used to be its own
   band below the hero; it now reads as one of four credentials inside the hero, with the full
   exclusion sentence as fine print beneath. All four marks are icons ALREADY in this build -
   the pin, the two-person family mark, the shield-check that was the notice's own icon, and
   the star glyph the review card uses. Nothing new was drawn. */
/* The rule spans the HERO TEXT COLUMN only, not the container. Measured off the reference:
   it runs 62->464 in an 818px-wide frame whose content box is 62->756, i.e. 0.579 of the
   width, while the photo starts at 490. The hero grid is 1.2fr/.8fr with a 52px gap, so the
   text column is (100% - 52px) * .6 = 0.575 - the same line. Full width once the grid
   collapses to one column at 900px. */
.herotrust{position:relative;z-index:2;margin-top:-8px;padding-top:22px}
.herotrust::before{content:"";position:absolute;top:0;left:0;height:1px;
  width:calc((100% - 52px) * .6);background:var(--hair)}
/* Inner heroes lost their media column when the statcard moved out to the stat strip, so
   there is nothing for the rule to stop before - clipping it to 57.5% there just looks like
   a truncated line. Full width on those; only the homepage, which still has the photo,
   keeps the short rule. */
.hero--inner .herotrust::before{width:100%}
@media (max-width:900px){.herotrust::before{width:100%}}
/* Packed LEFT at content width with dividers between - not four equal columns. In the
   reference the four credentials sit together under the copy rather than stretching the
   full container. */
.herotrust__row{display:flex;flex-wrap:wrap;align-items:center;list-style:none;margin:0;padding:0}
.herotrust__row li{display:flex;align-items:center;gap:9px;padding:2px 24px;
  border-right:1px solid var(--hair);min-width:0}
.herotrust__row li:first-child{padding-left:0}
.herotrust__row li:last-child{border-right:0;padding-right:0}
.herotrust__row svg{flex:none;width:20px;height:20px;color:var(--txt-accent)}
.herotrust__row b{font-size:.875rem;font-weight:700;color:var(--white);line-height:1.35}
/* One uniform line, as in the reference - the sub-label is NOT a faded tail on a bold
   label. Same size and weight as the label, only marginally softer in colour. */
.herotrust__row i{font-style:normal;font-size:.875rem;font-weight:700;
  color:var(--txt);white-space:nowrap}
.herotrust__note{margin:15px 0 0;font-size:.8125rem;line-height:1.5;color:var(--txt);max-width:96ch}
/* Stats rendered in the trust-row style: one compact line, small bold value, caption in
   the same size a shade softer, hairline dividers between. The base rule pins `i` to
   nowrap because its tails are two words; these captions are sentences, so they must be
   allowed to wrap or a single item drags the row past the container. */
.herotrust__row--stats i{white-space:normal;font-weight:400;color:var(--txt)}
/* the base rule sizes the trust-row glyph at 20px; the stat row carries 24x24 icons
   alongside 16x16 ones, and the shared box keeps them optically equal. */
.herotrust__row--stats svg{flex:none;width:18px;height:18px;color:var(--txt-accent)}
/* Separation is TRAILING only - padding-right + the divider, no left inset. With a left
   inset the base rule only zeroes it on :first-child, so the moment the row wraps the
   item that starts line two sits 24px in while line one starts flush. */
/* One row of four, each cell stacked: icon + value on line one, caption beneath it and
   aligned to the value rather than the icon. A 2x2 grid does that with no extra wrapper
   element - the svg spans both rows in column one, b and i take column two.
   min-width:0 is what lets a flex item shrink below its content width at all. */
/* top-aligned, not centred: with equal-width cells the captions wrap to different line
   counts, and centring each cell independently staggers the values down the row. */
.herotrust__row--stats{flex-wrap:nowrap;align-items:flex-start}
.herotrust__row--stats li{display:grid;grid-template-columns:auto 1fr;column-gap:9px;row-gap:1px;
  padding:6px 18px 6px 0;margin-right:18px;min-width:0;border-right:1px solid var(--hair)}
.herotrust__row--stats svg{grid-column:1;grid-row:1;margin-top:1px}
.herotrust__row--stats b{grid-column:2;grid-row:1;font-size:1rem}
.herotrust__row--stats i{grid-column:2;grid-row:2}
.herotrust__row--stats li:last-child{border-right:0;margin-right:0;padding-right:0}
@media (max-width:1100px){.herotrust__row li{padding:2px 18px}}
@media (max-width:900px){
  .herotrust__row{row-gap:14px}
  .herotrust__row li{flex:0 0 50%;padding-left:0;padding-right:18px}
  .herotrust__row li:nth-child(2n){border-right:0}
}
@media (max-width:560px){
  .herotrust__row{row-gap:12px}
  .herotrust__row li{flex:0 0 100%;border-right:0;padding-right:0}
}

/* MUST sit after the base .herotrust__row breakpoints. Both are (0,2,1) and a media query
   adds no specificity, so the base `li{flex:0 0 50%}` at max-width:900 was winning on
   source order and the stat row went 2-up instead of stacking. */
/* Also undoes the base `li{flex:0 0 50%}` from the 900px breakpoint: with nowrap that put
   4 x 50% = 200% of content in the row. 1 1 0 lets the four share the width and shrink. */
.herotrust__row--stats li{flex:1 1 0}
@media (max-width:760px){
  .herotrust__row--stats{flex-wrap:wrap}
  .herotrust__row--stats li{flex:1 1 100%;border-right:0;margin-right:0;padding-left:0;padding-right:0}
}
.hero__badge{
  position:absolute;left:-26px;bottom:38px;background:var(--white);color:var(--primary);
  border-radius:var(--r);padding:18px 22px;min-width:186px;
  box-shadow:0 18px 44px rgba(0,0,0,.34)
}
.hero__badge b{display:block;font-size:2.125rem;font-weight:800;line-height:1;letter-spacing:-.02em}
.hero__badge span{display:block;font-size:.75rem;font-weight:700;letter-spacing:.11em;text-transform:uppercase;color:rgba(0,0,0,.66);margin-top:6px}

/* water-surface divider */
/* the divider paints in currentColor so it can match whatever ground follows it */
.wavediv{position:absolute;left:0;right:0;bottom:-1px;z-index:3;line-height:0;
  pointer-events:none;color:var(--navy-deep)}
.wavediv svg{width:100%;height:auto;display:block}

/* ============================ ELIGIBILITY BAND ========================== */
.elig{background:var(--navy-deep);padding-block:34px}
.elig__in{display:flex;gap:18px;align-items:flex-start;
  border-left:4px solid var(--aqua);padding-left:22px}
.elig svg{flex:none;color:var(--txt-accent);margin-top:3px}
.elig p{font-size:1.0625rem;color:var(--txt);margin:0}
.elig strong{color:var(--white)}

/* ============================ TRUST STRIP =============================== */
/* Stat strip - operator instruction 2026-09-22: the hero's 2x2 statcard moves OUT of
   the hero and becomes a full-width 4-up band directly under it. Its own class, not a
   .statcard modifier, because .statcard is a 2-col grid whose nth-child border rules
   would fight a 4-col layout (the L-047 family: a container repainting a nested
   component). Collapses 4 -> 2 -> 1 so the captions never crush. */
.statstrip{background:var(--navy-deep);border-bottom:1px solid var(--hair)}
.statstrip__row{display:grid;grid-template-columns:repeat(4,1fr)}
.statstrip__row > div{padding:22px 26px;border-right:1px solid var(--hair);min-width:0}
.statstrip__row > div:last-child{border-right:0}
.statstrip__row b{display:block;font-size:1.5rem;font-weight:800;line-height:1.05;color:var(--txt-accent);letter-spacing:-.02em}
.statstrip__row span{display:block;font-size:.8125rem;color:var(--txt);margin-top:5px;line-height:1.4}
@media (max-width:900px){
  .statstrip__row{grid-template-columns:repeat(2,1fr)}
  .statstrip__row > div:nth-child(2n){border-right:0}
  .statstrip__row > div:nth-child(-n+2){border-bottom:1px solid var(--hair)}
}
@media (max-width:560px){
  .statstrip__row{grid-template-columns:1fr}
  .statstrip__row > div{border-right:0;border-bottom:1px solid var(--hair)}
  .statstrip__row > div:last-child{border-bottom:0}
}
/* the hero keeps its 2-col grid ONLY where a second column still exists (the homepage
   photo). Every other hero lost its statcard and goes single-column. */
.hero__grid--solo{grid-template-columns:1fr}
/* Trust slot - operator instruction 2026-09-22: the second statcard is replaced by an
   image, with a COMPACT one-line stat row beneath it carrying only the credentials the
   hero strip does not already show. Both library photos here are PORTRAIT (3:4); cropping
   them 4:3 from the centre beheads people, so object-position is set per image. */
/* Alignment (operator screenshot 2026-09-22): the columns matched in TOTAL height, but the
   stat row hung BELOW the photo, so the image bottom landed ~13px above the review card's.
   The photo now flexes to the full column height and the stats sit ON its lower edge over a
   scrim, so image-bottom == revcard-bottom by construction at any quote length. */
.trustmedia{position:relative;min-width:0;display:flex;flex-direction:column;
  border-radius:var(--r);overflow:hidden;border:1px solid var(--edge)}
/* The trust image stretches to whatever the quote + review column comes to -- 516px on
   every page that carries this block. This caps it and stops it stretching, so the photo
   is a panel beside the reviews rather than the tallest thing in the row. Scoped: the
   other four pages using this block are untouched. */
.trustmedia--short{align-self:start;max-height:420px;min-height:0}
/* Fixed 4:3, the size the operator approved - NOT flex:1. The alignment comes from the
   stat row being an overlay: the left column is then the image and nothing else, so the
   right column stretches to exactly the image height and the review card ends level. */
.trustmedia img{display:block;width:100%;height:auto;aspect-ratio:4/3;object-fit:cover}
/* A soft gradient measured 1.80:1 for the caption and 1.13:1 for the values where it faded
   out over the van's white lettering - both far under AA. Text over a photo needs a band that
   is opaque BEHIND THE TEXT, not a fade that happens to be dark in the average case; the fade
   is pushed above the text as a separate strip. */
.trustline{position:absolute;left:0;right:0;bottom:0;display:flex;flex-wrap:wrap;
  gap:8px 26px;margin:0;padding:15px 20px;background:rgba(30,30,58,.94)}
.trustline::before{content:"";position:absolute;left:0;right:0;bottom:100%;height:56px;
  background:linear-gradient(to top,rgba(30,30,58,.94),rgba(30,30,58,0));pointer-events:none}
.trustline > span{display:flex;align-items:baseline;gap:.45em;font-size:.8125rem;
  color:var(--txt);line-height:1.4}
.trustline b{font-size:1.125rem;font-weight:800;color:var(--txt-accent);letter-spacing:-.01em;
  font-variant-numeric:tabular-nums;white-space:nowrap}
.trustline:empty{display:none}
@media (max-width:560px){.trustline{gap:8px 18px}}

/* The image column runs ~2x the quote's height, leaving the right column blank below the
   testimonial (operator screenshot 2026-09-22). Filled with the review score, which belongs
   beside the review anyway. flex:1 makes the card absorb exactly the leftover height, so the
   gap cannot come back when a quote is shorter or longer. Real figures only - 4.4 across 106
   reviews, linking to the same GBP listing the About page already links to. */
.trust__col{display:flex;flex-direction:column;gap:30px;min-width:0}   /* 18 -> 30, operator 2026-09-25 */
/* .quote sets align-self:start, which in a GRID parent pins it vertically (what it was for)
   but in this COLUMN flex container is the inline axis, so the card shrank to its text and
   sat ~72px narrower than the .revcard beneath it. Stretch only inside this column; every
   other .quote keeps the original behaviour. */
.trust__col > .quote{align-self:stretch}
.revcard{display:flex;flex-direction:column;justify-content:center;flex:1;
  background:var(--navy-lift);border:1px solid var(--hair);border-radius:var(--r);
  padding:22px 26px;text-decoration:none;transition:border-color .18s}
.revcard:hover{border-color:var(--aqua)}
.revcard__score{display:flex;align-items:baseline;gap:.55em;flex-wrap:wrap}
.revcard__score b{font-size:2.25rem;font-weight:800;line-height:1;color:var(--txt-accent);
  letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.revcard__stars{color:var(--txt-accent);font-size:1.0625rem;letter-spacing:.08em}
.revcard__meta{font-size:.875rem;color:var(--txt);margin-top:9px;line-height:1.45}
/* Rendered as a button (operator 2026-09-23). It stays a <span>: the whole .revcard is already
   an <a>, and a nested <a> or <button> inside a link is invalid interactive content. The .btn
   classes give it the pill; only the layout bits belong here. align-self:start stops the flex
   column stretching it to full width. The a.btn.* label guard does not match a span, so the
   colour comes from .btn--outline itself. */
.revcard__go{margin-top:16px;align-self:start;gap:.5em}
.revcard:hover .revcard__go{background:var(--white);color:var(--primary);border-color:var(--white)}
@media (max-width:900px){.revcard{flex:0 0 auto}}

.trust__grid{display:grid;grid-template-columns:1.15fr .85fr;gap:36px;align-items:stretch}
/* Aqua top rule + hover, 2026-09-23. The card had a plain 1px border on all four sides, so
   there was no top line to recolour and it did not match the lock's "Cards - aqua top border"
   that .call and .scard carry. --txt-accent keeps it band-aware. The hover is the same tint
   .nexttiles uses on About, so every card on the site reacts the same way; the global
   prefers-reduced-motion block already disables both transitions. */
/* Four separate tiles, not one panel ruled into quarters. The wrapper keeps no border,
   ground or hover of its own - each cell is a tile in its own right, with the top bar and
   --bar-hover every other tile on the site uses. */
.statcard{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.statcard > div{padding:24px 22px;background:var(--surface);
  border:1px solid var(--edge);border-top:3px solid var(--txt-accent);
  border-radius:var(--r);
  transition:background-color .18s,border-top-color .18s}
.statcard > div:hover{background:var(--surface-hover,rgba(40,145,243,.10));
  border-top-color:var(--bar-hover)}
.statcard b{display:block;font-size:1.875rem;font-weight:800;line-height:1.05;color:var(--txt-accent);letter-spacing:-.02em}
.statcard span{display:block;font-size:.875rem;color:var(--txt);margin-top:7px;line-height:1.45}
/* speech-bubble pull quote */
.quote{position:relative;background:var(--navy-lift);border:1px solid var(--hair);border-radius:var(--r);padding:28px 30px;align-self:start}
.quote::after{
  content:"";position:absolute;left:38px;bottom:-11px;width:20px;height:20px;
  background:var(--navy-lift);border-right:1px solid var(--hair);border-bottom:1px solid var(--hair);
  transform:rotate(45deg)
}
.quote p{font-size:1.0625rem;color:var(--txt);font-style:italic;margin-bottom:0}
.quote__by{display:flex;align-items:center;gap:12px;margin-top:26px;padding-left:4px}
.quote__by b{color:var(--white);font-size:.9375rem;display:block}
.quote__by span{color:var(--txt);font-size:.8125rem}
/* L-047: `.quote__by span` is (0,1,1) and beats `.quote__av` (0,1,0) on specificity,
   so the avatar must be scoped with TWO classes or it renders muted-grey on the fill.
   Fill is --light-aqua with a navy label: 8.12:1. White on --aqua would be 3.29:1,
   which fails the 4.5:1 floor at this size (15px/800 is not "large"). */
.quote__by .quote__av{width:42px;height:42px;border-radius:50%;background:var(--aqua);
  color:var(--ink-navy);display:flex;align-items:center;justify-content:center;
  font-weight:800;font-size:.9375rem;flex:none;line-height:1}

/* ============================ SPLIT / MEDIA ============================= */
.split{display:grid;grid-template-columns:1fr 1fr;gap:60px;align-items:center}
.split--wide-l{grid-template-columns:1.12fr .88fr}
/* .split centres its columns, which is right when both carry copy. In /about/'s first
   section the right column is the reserved portrait .slot - 645px against 395px of text -
   so centring pushed the section's opening heading 125px down: the gap under the hero
   measured 229px there against 104px on every other inner page. Top-aligning gives the
   section's own padding back as the gap. Applied to that one split, not the two below it. */
.split--top{align-items:start}
.split__media img{width:100%;aspect-ratio:4/5;object-fit:cover;border-radius:var(--r-sig)}
/* offset overlapping pair */
.pair{position:relative;padding-bottom:64px;padding-right:56px}
/* nth-of-type, NOT :last-child — the last CHILD is .pair__badge, so :last-child
   matched nothing and the second photo stacked in flow instead of overlapping. */
.pair img:nth-of-type(1){width:76%;aspect-ratio:3/4;object-fit:cover;border-radius:var(--r-sig)}
.pair img:nth-of-type(2){
  position:absolute;right:0;bottom:0;width:54%;aspect-ratio:1/1;object-fit:cover;
  border-radius:var(--r);border:6px solid var(--navy)
}
.pair--onlift img:nth-of-type(2){border-color:var(--navy-lift)}
.pair__badge{
  position:absolute;left:52%;top:52%;transform:translate(-50%,-50%);z-index:2;
  width:132px;height:132px;border-radius:50%;background:var(--aqua);color:var(--ink-navy);
  display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;
  box-shadow:0 14px 40px rgba(0,0,0,.34)
}
.pair__badge b{font-size:1.75rem;font-weight:800;line-height:1;letter-spacing:-.02em}
.pair__badge span{font-size:.625rem;font-weight:700;letter-spacing:.11em;text-transform:uppercase;margin-top:5px;line-height:1.3}

/* ============================ NUMBERED PROBLEM BLOCK ==================== */
.calls{display:grid;grid-template-columns:1fr 1fr;gap:34px}
.call{position:relative;transition:background-color .18s,border-top-color .18s;border:1px solid var(--hair);border-top:3px solid var(--aqua);
  border-radius:var(--r);padding:38px 32px 34px;background:rgba(255,255,255,.03);overflow:hidden}
.call__n{
  position:absolute;right:16px;top:2px;font-size:6.5rem;font-weight:800;line-height:1;
  color:var(--white);opacity:.07;letter-spacing:-.04em;pointer-events:none
}
.call h3{color:var(--white);margin-bottom:14px;font-size:1.375rem}
.call p{color:var(--txt);margin-bottom:0;font-size:.9688rem}
.call p + p{margin-top:.9em}
/* Hover, 2026-09-23: the same background tint .nexttiles uses on About, so the two read as
   one interaction language, plus the top rule switching off the aqua accent. --txt-strong is
   band-declared (white on the dark bands, ink on light), so the card does not need a second
   rule if it is ever moved to a light ground. The global prefers-reduced-motion block already
   kills both transitions. */
.call:hover{background:rgba(40,145,243,.10);border-top-color:var(--bar-hover)}

/* ============================ SCOPE CARDS (light band) ================== */
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:28px}
.cards--center{justify-content:center}
.card{
  background:var(--white);border:1px solid var(--hair-dark);border-top:3px solid var(--aqua);
  border-radius:var(--r);padding:0;overflow:hidden;display:flex;flex-direction:column
}                                            /* M7 #5 — no drop shadow anywhere */
.card__img{width:100%;aspect-ratio:16/10;object-fit:cover}
.card__body{padding:26px 26px 28px;display:flex;flex-direction:column;flex:1}
.card__top{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:16px}
.card__top h3{font-size:1.25rem;color:var(--ink-strong);line-height:1.28}
.card__price{flex:none;text-align:right}
.card__price b{display:block;font-size:1.5rem;font-weight:800;color:var(--primary);line-height:1;letter-spacing:-.02em}
.card__price span{display:block;font-size:.6875rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:rgba(0,0,0,.62);margin-top:5px}
.dotlist{list-style:none;margin:0;padding:0}
.dotlist li{
  padding:11px 0 11px 26px;border-bottom:1px dotted rgba(0,0,0,.26);
  font-size:.9375rem;color:var(--ink);position:relative;line-height:1.5
}
.dotlist li:last-child{border-bottom:0}
.dotlist li::before{
  content:"";position:absolute;left:4px;top:19px;width:8px;height:8px;border-radius:50%;
  border:2px solid var(--aqua)
}
.card__note{margin-top:18px;padding-top:16px;border-top:1px solid var(--hair-dark);font-size:.875rem;color:rgba(0,0,0,.66)}

/* ============================ DIAGNOSTIC ROWS =========================== */
/* L-068: a capped column in a 1280 container reads as "blank space" to reviewers.
   This section pairs the intro + callout column with the rows column instead. */
.diag{display:grid;grid-template-columns:.82fr 1.18fr;gap:56px;align-items:start}
.drows{display:grid;gap:4px}
/* --edge / --rule rather than the raw white-at-alpha: on a dark band they resolve to
   .17 and .22 against the .18 and .22 hardcoded here, so the dark build is unchanged
   to within a hundredth of an alpha - and both separators sit on WHITE in the light
   build, 17 coverage rows and 3 diagnostic rows, where nothing was drawn at all. */
.drow{display:flex;gap:22px;align-items:flex-start;padding:24px 0;border-bottom:1px dotted var(--rule)}
.drow:last-child{border-bottom:0}
.dbadge{
  flex:none;width:52px;height:52px;border-radius:50%;border:2px solid var(--aqua);
  display:flex;align-items:center;justify-content:center;color:var(--txt-accent);
  font-weight:800;font-size:1rem;background:rgba(40,145,243,.14)
}
.drow .drow__h{color:var(--white);font-size:1.0625rem;margin-bottom:7px;
  font-family:var(--ff);font-weight:700;letter-spacing:0}  /* h3, not h4: H2->H4 skips a level */
.drow p{color:var(--txt);font-size:.9375rem;margin:0}
/* Hover, matching .donot. The badge is ALREADY the accent at rest, so unlike .donot__x
   it cannot hover TO the accent - it would not move at all. Badge and heading swap
   roles instead: badge accent -> strong, heading strong -> accent. Same resolution
   .pstep needed for the same reason. */
.dbadge{transition:border-color .18s,color .18s,background-color .18s}
.drow .drow__h{transition:color .18s}
.drow:hover .dbadge{border-color:var(--txt-strong);color:var(--txt-strong);
  background:rgba(255,255,255,.14)}
.drow:hover .drow__h{color:var(--txt-accent)}
.callout{
  margin-top:40px;border:1px solid var(--hair);border-left:4px solid var(--aqua);
  border-radius:var(--r);padding:26px 30px;background:rgba(255,255,255,.04);
  display:flex;align-items:center;gap:26px;flex-wrap:wrap
}
.callout b{font-size:2.125rem;font-weight:800;color:var(--txt-accent);line-height:1;letter-spacing:-.02em}
.callout p{margin:0;color:var(--txt);font-size:1rem}

/* ============================ CTA BANDS ================================= */
.cta-band{position:relative;overflow:hidden;padding-block:86px;background:var(--navy-deep)}
.cta-band__bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:1}
.cta-band__scrim{
  /* Lightened 2026-09-23 on operator instruction - it was .95/.93/.72, which buried the photo
     entirely. Measured, not guessed: at this strength the lightest pixel behind the copy is
     ~rgb(92,99,108), which is 4.50:1 against the old 86%-white body text (a fail) but 6.1:1
     against pure white - so the paragraph goes to full white below and the overlay can come
     down two steps rather than one. */
  position:absolute;inset:0;
  background:linear-gradient(94deg, rgba(30,30,58,.72) 0%, rgba(30,30,58,.66) 46%, rgba(30,30,58,.44) 100%),
             linear-gradient(180deg, rgba(30,30,58,0) 30%, rgba(30,30,58,.42) 100%)
}
.cta-band__in{position:relative;z-index:2;max-width:760px}
/* The eyebrow is body-size (13px/600) so it takes the 4.5:1 floor, and --accent-on-dark only
   cleared that against the old near-black scrim. Measured after lightening: 2.69-3.66:1 across
   the 13 bands - a fail on every one. White clears it at 5.4:1 on the worst background pixel
   in the whole set, rgb(99,106,116). Same per-ground recolour the light bands already do. */
.cta-band .eyebrow{color:var(--white)}
.cta-band h2{color:var(--white);margin-bottom:16px}
.cta-band p{color:var(--white);font-size:1.0625rem;margin-bottom:30px}

/* ============================ BRAND ROW ================================= */
.brands{display:flex;flex-wrap:wrap;gap:12px}
/* A GENERIC mark only. No manufacturer logo or trade dress: 00_Build_Standards.md 13c gates
   any visual that asserts an authorized-dealer relationship, and no certificate is on file for
   these brands (Hayward is excluded outright). A wrench says "equipment we service" and claims
   nothing. Wrench because a circle-and-rays gear would be indistinguishable from the sun mark
   on the service tabs at this size. */
/* Monogram treatment (operator choice, 2026-09-23 - switched back from the plaque). Reuses the
   circular-initial device already on the testimonials, so it is a device the site owns rather
   than an invented mark. UNIFORM by construction: one fill colour, one typeface, one shape for
   all seven; only the letter and the text change. That is what keeps it clear of 13c - a badge
   shaped or coloured to evoke a particular manufacturer is trade dress even with no artwork
   copied. The initial is aria-hidden; the full name beside it is the accessible content. The
   "Serviced" label goes with the plaque, but the section lede still says outright that we
   service, repair and replace equipment from these manufacturers. */
.brands{display:flex;flex-wrap:wrap;gap:12px}
.brands span{display:inline-flex;align-items:center;gap:11px;
  padding:9px 20px 9px 9px;background:var(--white);
  border:1px solid var(--hair-dark);border-radius:999px}
.brands span i{flex:none;width:34px;height:34px;display:grid;place-items:center;
  border-radius:50%;background:var(--primary);color:var(--white);
  font-style:normal;font-weight:800;font-size:.9375rem;line-height:1}
.brands span b{font-size:.9375rem;font-weight:700;color:var(--ink-strong);line-height:1.2}
.brands span{transition:background-color .18s,border-color .18s}
.brands span i{transition:background-color .18s}
.brands span b{transition:color .18s}
.brands span:hover{background:var(--tint);border-color:var(--primary)}
.brands span:hover i{background:var(--primary-hover)}
/* --ink-strong (the label at rest) and --primary both resolve to #4B4DED on a light
   band, so hovering to --primary was a no-op. --primary-hover is the only value here
   that actually moves - the same collapse .claims, .pstep and .hubcard each hit. */
.brands span:hover b{color:var(--primary-hover)}
.brands--dark span{border-color:var(--hair);background:rgba(255,255,255,.05)}
.brands--dark span b{color:var(--white)}
.brands--dark span i{background:var(--aqua);color:var(--ink-navy)}
.brands--dark span:hover{background:rgba(40,145,243,.10);border-color:var(--aqua)}
.brands--dark span:hover i{background:var(--white)}
.brands--dark span:hover b{color:var(--txt-accent)}

/* ============================ COVERAGE GRID ============================= */
.coverage{display:grid;grid-template-columns:repeat(4,1fr);gap:10px 26px;margin-bottom:34px}
.coverage li{list-style:none;display:flex;align-items:center;gap:10px;font-size:.9375rem;
  padding:9px 0;border-bottom:1px dotted var(--edge);color:var(--txt)}
/* Belt and braces: an inline svg with no width/height fills its flex line - the pin pulled
   279px and pushed 68px of page overflow before the attributes went back on. */
.coverage li svg{flex:none;width:15px;height:15px}
.coverage svg{flex:none;color:var(--txt-accent)}
/* Hover. The pin is ALREADY the accent, so it swaps with the label rather than hovering
   to a colour it is already wearing - same resolution as .drow and .pstep. */
.coverage li{transition:color .18s}
.coverage li svg{transition:color .18s}
.coverage li:hover{color:var(--txt-accent)}
.coverage li:hover svg{color:var(--txt-strong)}
/* The embed can fail to paint (network policy, headless, Google blocking the
   referrer). A labelled slot sits BEHIND it so a reviewer never sees an empty box. */
.mapwrap{position:relative;border:1px solid var(--hair);border-radius:var(--r);overflow:hidden;
  line-height:0;background:var(--navy-lift);min-height:380px}
.mapwrap iframe{position:relative;z-index:2;width:100%;height:380px;border:0;display:block}
/* Map matched to the text column: starts at the h2 cap height (same 71px lead-in as a
   media split) and stretches to the bottom of the copy instead of stopping at 380px.
   The iframe has to be told to fill too -- its own rule pins it to 380px. */
/* The row centres its columns by default, which puts the h2 cap at an unpredictable
   offset, and .mapwrap carries min-height:380px which stops it shrinking to the copy.
   Both have to go for the map to run cap-to-baseline. */
.split:has(> .mapwrap--fill){align-items:start}
.mapwrap--fill{align-self:stretch;margin-top:71px;min-height:0}
.mapwrap--fill iframe{height:100%}
/* Sits BEHIND the live embed purely as a no-JS / blocked-network fallback.
   Not a placeholder treatment — no dashed or hatched "pending" styling, because
   the map in front of it is the real thing. */
.mapfall{
  position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;align-items:center;
  justify-content:center;text-align:center;gap:10px;line-height:1.5;padding:24px;
  background:var(--navy-lift)
}
.mapfall svg{color:var(--txt-accent)}
.mapfall b{color:var(--white);font-size:1rem}
.mapfall span{color:var(--txt);font-size:.8125rem}
.mapnote{font-size:.75rem;color:var(--txt);margin-top:10px;letter-spacing:.02em}

/* ============================ STAGGER GRID ============================== */
.stagger{display:grid;grid-template-columns:repeat(3,1fr);gap:26px;align-items:start}
.stagger > :nth-child(2){margin-top:38px}
.stagger > :nth-child(3){margin-top:76px}
.scard{transition:background-color .18s,border-top-color .18s;border:1px solid var(--hair);border-top:3px solid var(--aqua);border-radius:var(--r);
  padding:32px 28px;background:rgba(255,255,255,.035)}
/* Same hover as .call and .statcard so every card on the site reacts identically: the tint
   .nexttiles uses on About, and the aqua top rule going white. */
.scard:hover{background:rgba(40,145,243,.10);border-top-color:var(--bar-hover)}
.scard__ic{width:54px;height:54px;border-radius:50%;background:rgba(40,145,243,.16);
  border:1px solid rgba(40,145,243,.42);display:flex;align-items:center;justify-content:center;
  color:var(--txt-accent);margin-bottom:20px}
.scard h3{color:var(--white);font-size:1.1875rem;margin-bottom:12px}
.scard p{color:var(--txt);font-size:.9375rem;margin:0}

/* ---- signature-gradient hover, homepage only (2026-09-29, operator) -------
   mandgpools.com fills the whole card with the gradient on hover and inverts the
   content to white. This is that effect, built from the Brand Guide's own two
   stops rather than the live site's near-misses - it runs #4347DE / #7768F9 over
   #2196F3 there, against #4B4DED / #7666F8 / #2891F3 in the guide. Same two
   tokens --grad is made of, so no new colour enters the sheet.

   SCOPED to .scard--grad on operator instruction: .scard is also used by two
   cards on /services/pool-safety-covers/, and those keep the hover they have.
   The plain .scard:hover rule above is therefore left exactly as it was.

   Painted by a ::before, not by swapping `background`: a background-image cannot
   be transitioned, only an overlay's opacity can, so the fill fades instead of
   snapping. The overlay sits at z-index 0 with the card's own children lifted to
   1 - a NEGATIVE z-index would have put it behind the card's opaque --surface,
   where it would have rendered nothing at all.

   Diagonal rather than --grad's 90deg: on a card this tall the horizontal ramp
   barely reads, and the reference runs corner to corner.

   CONTRAST, measured at the ramp's worst point: the h3 is 19px/700, over the
   18.66px-bold large-text threshold, so it takes the 3:1 floor and clears it at
   3.26:1. The 15px paragraph takes the 4.5:1 floor and gets 3.26:1 at the Sky
   Blue end - it does not clear. No gradient inside this palette can carry 15px
   white text; white peaks at 4.19:1 on Violet Glow. Raised with the operator,
   who asked for the signature gradient specifically. */
.scard--grad{position:relative;isolation:isolate}
.scard--grad > *{position:relative;z-index:1}
.scard--grad::before{
  content:"";position:absolute;inset:-1px;top:-3px;z-index:0;
  border-radius:var(--r);
  background:linear-gradient(135deg,var(--primary-hover) 0%,var(--sky) 100%);
  opacity:0;transition:opacity .22s
}
.scard--grad:hover::before{opacity:1}
/* (0,2,0) beats the shared .scard:hover at (0,1,1), so the aqua tint underneath is
   cancelled - left in place it would show through the half-faded overlay.

   The content is inverted by flipping the band-role TOKENS, not by declaring
   `color` on each child. The light build repaints every h1-h4 inside .band-lift
   through `.theme-light .band-lift:not(.band--keepdark) h3` at (0,3,1), which
   outranks a plain `.scard--grad:hover h3` at (0,2,1): the paragraph went white
   and the heading stayed Brand Violet. That mirror only reaches these cards at
   all because the build is now light-locked. Escalating specificity would mean
   re-pinning every child against a rule that can move; setting the tokens lets
   the mirror keep winning its declaration and simply read white out of it, and
   it covers the icon - and any child added later - for free. */
.scard--grad:hover{
  background:var(--surface);
  --txt:var(--white);--txt-strong:var(--white);--ink-strong:var(--white);
  --txt-accent:var(--white)}
.scard--grad:hover .scard__ic{
  background:rgba(255,255,255,.18);border-color:rgba(255,255,255,.55);color:var(--white)}
@media (prefers-reduced-motion:reduce){.scard--grad::before{transition:none}}

/* ============================ FAQ ======================================= */
.faq{max-width:880px;margin-inline:auto}
.faq details{border-bottom:1px solid var(--hair-dark);background:transparent}
.faq details[open]{background:var(--white);border-radius:var(--r);border-bottom-color:transparent;
  border:1px solid var(--hair-dark);border-top:3px solid var(--aqua);margin-block:10px}
.faq summary{
  list-style:none;cursor:pointer;padding:22px 58px 22px 4px;position:relative;
  font-weight:700;font-size:1.0625rem;color:var(--ink-strong);line-height:1.4
}
.faq details[open] summary{padding-inline:26px 58px}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{
  content:"";position:absolute;right:16px;top:50%;width:13px;height:13px;margin-top:-8px;
  border-right:2.5px solid var(--aqua);border-bottom:2.5px solid var(--aqua);
  transform:rotate(45deg);transition:transform .22s
}
.faq details[open] summary::after{transform:rotate(-135deg);margin-top:-3px}
.faq__a{padding:0 58px 24px 4px;color:var(--ink);font-size:1rem}
.faq details[open] .faq__a{padding-inline:26px 58px}

/* ============================ FOOTER (M7 #6) ============================ */
/* padding-top has to TRACK the wave, exactly as .hero's bottom padding does.
   .footer__wave is an SVG pinned to the top edge with height:auto, so it is a
   fixed share of the viewport width - measured at 5.8% from 430px to 2560px
   (25px -> 149px). A flat 92px stopped clearing it at about 1600px, and past
   that the wave ran over the column headings: 19px at 1920, 56px at 2560, which
   is what zooming out exposes. max() keeps 92 wherever it is still the larger,
   so nothing at or below 1280px moves. */
.footer{background:var(--navy-deep);position:relative;
  padding-top:max(92px,calc(5.85vw + 18px))}
.footer__wave{position:absolute;top:-1px;left:0;right:0;line-height:0;pointer-events:none}
.footer__wave svg{width:100%;height:auto;display:block}
.footer__grid{display:grid;grid-template-columns:1.5fr 1fr 1fr 1.15fr;gap:44px;padding-bottom:56px}
.footer__brand img{height:62px;width:auto;margin-bottom:22px}
.footer__brand p{color:var(--txt);font-size:.9375rem;max-width:36ch}
.footer .footer__label{display:block;margin:0 0 18px;
  font-size:.75rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;
  color:var(--txt-accent);margin-bottom:18px;padding-bottom:12px;border-bottom:1px solid var(--hair)
}
.footer ul{list-style:none;margin:0;padding:0}
.footer li{margin-bottom:11px}
.footer a{color:var(--txt);text-decoration:none;font-size:.9375rem;transition:color .18s}
.footer a:hover{color:var(--txt-accent)}
.footer address{font-style:normal;color:var(--txt);font-size:.9375rem;line-height:1.75}
.footer address a{display:inline-block}
.footer address strong{color:var(--white)}
/* two-class scoping — .footer a must not repaint a button (L-047) */
.footer__cta .btn--secondary{color:var(--white)}
.footer__cta .btn--inverted{color:var(--primary)}
/* The footer wordmark and the EB badge are links now. Two-class scoped so
   `.footer a` cannot repaint them, and given a tight box so the hit area is the
   mark itself rather than a full-width line box (L-047 family). */
.footer .footer__logo{display:inline-block;line-height:0;transition:opacity .18s}
.footer .footer__logo:hover{opacity:.82}
.footer .eb a{display:inline-block;line-height:0;transition:opacity .18s}
.footer .eb a:hover{opacity:.82}
.socials{display:flex;flex-wrap:wrap;gap:11px;margin-top:24px}
.socials a{
  width:44px;height:44px;border-radius:50%;border:1px solid var(--hair);   /* >=44px tap target (Fitts) */
  display:flex;align-items:center;justify-content:center;color:var(--white);
  transition:background-color .18s,border-color .18s
}
.socials a:hover{background:var(--aqua);border-color:var(--aqua);color:var(--white)}
.footer__bar{
  border-top:1px solid var(--hair);padding-block:26px;display:flex;flex-wrap:wrap;gap:16px 30px;
  align-items:center;justify-content:space-between
}
.footer__bar p{margin:0;color:var(--txt);font-size:.8125rem}
.footer__legal{display:flex;flex-wrap:wrap;gap:20px}
.footer__legal a{font-size:.8125rem}
.eb{display:flex;align-items:center;gap:12px}
.eb img{height:26px;width:auto}

/* ============================ STICKY MOBILE BAR (M7 #8) ================= */
/* show-by-default + hide on desktop — never display:none + a max-width override */
.mobilebar{
  display:flex;position:fixed;left:0;right:0;bottom:0;z-index:9999;
  background:var(--navy-deep);border-top:1px solid var(--hair);padding:9px;gap:9px;
  box-shadow:0 -8px 28px rgba(0,0,0,.4)
}
.mobilebar a{flex:1 1 0;min-width:0;font-size:.9375rem;padding-inline:12px}
.mobilebar .btn--secondary{color:var(--white)}

/* ============================ MOBILE DRAWER ============================= */
.drawer{
  display:none;position:fixed;inset:0;z-index:9998;background:var(--navy-deep);
  padding:22px var(--gutter) 96px;overflow-y:auto
}
.drawer[data-open="true"]{display:block}
.drawer__top{display:flex;align-items:center;justify-content:space-between;margin-bottom:26px}
.drawer__top img{height:56px;width:auto}
.drawer__close{background:none;border:1px solid var(--hair);border-radius:var(--r);
  width:48px;height:48px;color:var(--white);cursor:pointer;font-size:1.4rem;line-height:1}
.drawer details{border-bottom:1px solid var(--hair)}
.drawer summary{list-style:none;cursor:pointer;padding:16px 4px;font-weight:700;font-size:1.0625rem;
  color:var(--white);display:flex;justify-content:space-between;align-items:center}
.drawer summary::-webkit-details-marker{display:none}
.drawer summary::after{content:"";width:11px;height:11px;border-right:2.5px solid var(--aqua);
  border-bottom:2.5px solid var(--aqua);transform:rotate(45deg);margin-top:-5px;transition:transform .2s}
.drawer details[open] summary::after{transform:rotate(-135deg);margin-top:3px}
.drawer details ul{list-style:none;margin:0;padding:0 0 14px}
.drawer details li a{display:block;padding:10px 4px;color:var(--txt);text-decoration:none;font-size:.9375rem}
.drawer > a{display:block;padding:16px 4px;color:var(--white);font-weight:700;text-decoration:none;
  border-bottom:1px solid var(--hair);font-size:1.0625rem}
.drawer__hub{color:var(--txt-accent)!important;font-weight:700!important}  /* L-022: hub first in its own submenu */

/* ============================ RESPONSIVE ================================ */
@media (min-width:769px){ .mobilebar{display:none} }

@media (max-width:1100px){
  .hero__grid{gap:40px}
  .coverage{grid-template-columns:repeat(3,1fr)}
  .mega{width:min(940px,calc(100vw - 48px));grid-template-columns:repeat(3,1fr)}
}
@media (max-width:900px){
  :root{--space-section:var(--space-section-sm)}
  .nav{display:none}
  .burger{display:flex}
  .header{top:31px}
  .hero{padding-top:150px;padding-bottom:110px}
  .hero__grid,.split,.split--wide-l,.trust__grid,.calls,.diag{grid-template-columns:1fr;gap:42px}
  .hero__media{max-width:440px}
  .hero__badge{left:auto;right:-14px;bottom:-16px}
  .cards{grid-template-columns:repeat(2,1fr)}
  .stagger{grid-template-columns:repeat(2,1fr)}
  .stagger > :nth-child(2),.stagger > :nth-child(3){margin-top:0}
  .footer__grid{grid-template-columns:1fr 1fr;gap:36px}
  .coverage{grid-template-columns:repeat(2,1fr)}
  body{padding-bottom:74px}
}
@media (max-width:640px){
  .cards,.stagger,.footer__grid{grid-template-columns:1fr}
  /* 17 short town names read badly as one long column. Two up fits the longest
     label ("Moreland Hills") at 375px with room left; the tighter column gap
     is what buys that margin. */
  .coverage{grid-template-columns:repeat(2,1fr);column-gap:16px}
  .statcard{grid-template-columns:1fr}
  .pair{padding-right:38px;padding-bottom:48px}
  .pair__badge{width:104px;height:104px}
  .pair__badge b{font-size:1.375rem}
  .hero__badge{position:static;margin-top:16px;width:100%}
  .btn-row .btn{flex:1 1 100%}          /* cta_equal_width */
  .drow{gap:16px}
  .callout{gap:16px}
  .faq summary{padding-right:46px}
  .faq__a{padding-right:46px}
  .footer__bar{justify-content:flex-start}
}
@media (prefers-reduced-motion:reduce){
  *{transition:none!important;animation:none!important}
  html{scroll-behavior:auto}
}

/* ==========================================================================
   INNER-TEMPLATE COMPONENTS — first codified on /about/ (2026-09-18)
   Every remaining inner template inherits these. Deviating needs a reason
   recorded in _DESIGN_STANDARD_LOCK.md.
   ========================================================================== */

/* --- breadcrumbs: between the nav and the hero, never below it (L-065) ----- */
.crumbs{position:relative;z-index:2;padding-top:150px;padding-bottom:0}
.crumbs ol{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;align-items:center;gap:8px;
  font-size:.8125rem;font-weight:600;letter-spacing:.02em}
.crumbs a{color:var(--txt-accent);text-decoration:none}          /* 8.58:1 on the hero ground */
.crumbs a:hover{text-decoration:underline}
.crumbs li[aria-current]{color:var(--txt)}
.crumbs svg{color:rgba(255,255,255,.45);flex:none}

/* --- inner hero: the homepage ramp, shorter (L-002), no media column ------- */
.hero--inner{padding-top:26px;padding-bottom:max(104px,calc(8.1vw + 26px))}
.hero--inner .hero__grid{grid-template-columns:1.15fr .85fr;align-items:center;gap:52px}
.hero--inner h1{margin-bottom:18px}

/* --- two-column contrast block (the "service, not building" thesis) -------- */
.contrast{display:grid;grid-template-columns:1fr 1fr;gap:26px}
/* --edge/--surface, not the raw white-at-alpha: on a dark band those tokens are
   exactly rgba(255,255,255,.17) and rgba(255,255,255,.035), so the dark build does
   not move - but on a band the light theme flips, the card had no visible fill and
   no visible border at all. Same fix as .reach and .napcard. */
.contrast__col{border:1px solid var(--edge);border-radius:var(--r);padding:32px 30px;
  background:var(--surface);
  transition:background-color .18s,border-top-color .18s,border-color .18s}
/* Same hover language as .step, .call, .scard, .statcard and .reach - the fill takes
   the Sky Blue wash and the 3px bar takes --bar-hover - so every card on the site
   reacts identically. --bar-hover is band-aware (#fff on a dark ground, Violet Glow
   on a light one), which is why this one rule works in both builds. */
.contrast__col:hover{background:rgba(40,145,243,.10);border-top-color:var(--bar-hover)}
/* The Sky Blue tag is 12px/700, so it needs 4.5:1 - and the hover wash lifts the dark
   ground from (30,38,71) to (30,41,77), which drops it from 4.6 to 4.35. Measured, not
   assumed. --txt-strong takes it to white on a dark band and to ink on a light one,
   both comfortably clear, and recolouring the label on hover is what .claims__row and
   .pstep already do. */
.contrast__col:hover .contrast__tag{color:var(--txt-strong)}
.contrast__col--ours{border-top:3px solid var(--aqua);background:rgba(40,145,243,.07)}
.contrast__col--theirs{border-top:3px solid rgba(255,255,255,.28)}
.contrast__tag{display:inline-block;font-size:.75rem;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;margin-bottom:14px;color:var(--txt)}
.contrast__col--ours .contrast__tag{color:var(--txt-accent)}
.contrast__col h3{color:var(--white);font-size:1.1875rem;margin-bottom:12px}
.contrast__col p{color:var(--txt);font-size:.9375rem;margin:0}

/* --- service sheet: the "written record after every visit" device ---------- */
.sheet{background:var(--white);border:1px solid var(--hair-dark);border-top:3px solid var(--primary);
  border-radius:var(--r);padding:0;overflow:hidden;max-width:520px}
.sheet__head{display:flex;align-items:center;justify-content:space-between;gap:16px;
  padding:18px 24px;border-bottom:1px dashed rgba(0,0,0,.26);background:var(--bg)}
.sheet__head b{font-size:.9375rem;color:var(--ink-strong)}
.sheet__head span{font-size:.6875rem;font-weight:700;letter-spacing:.11em;text-transform:uppercase;
  color:var(--ink-muted)}
.sheet__rows{list-style:none;margin:0;padding:8px 24px 20px}
.sheet__rows li{display:flex;align-items:baseline;gap:14px;padding:13px 0;
  border-bottom:1px dotted rgba(0,0,0,.26);font-size:.9375rem;color:var(--ink)}
.sheet__rows li:last-child{border-bottom:0}
.sheet__rows b{flex:none;width:92px;font-size:.6875rem;font-weight:700;letter-spacing:.09em;
  text-transform:uppercase;color:var(--primary)}

/* --- "what we will not claim": refusal + reason, stated confidently -------- */
.claims{display:grid;gap:0;max-width:960px}
/* Indents the whole list from the heading. The rows are a grid, not a <ul>, so there is no
   list-style indent to lean on -- it has to be explicit padding. */
.claims--indent{padding-left:28px}
@media (max-width:560px){ .claims--indent{padding-left:0} }
.claims__row{display:grid;grid-template-columns:auto 1fr;gap:22px;align-items:start;
  padding:26px 0;border-bottom:1px dotted rgba(255,255,255,.22)}
.claims__row:last-child{border-bottom:0}
.claims__mark{flex:none;width:44px;height:44px;border-radius:50%;
  border:2px solid rgba(255,255,255,.34);display:flex;align-items:center;justify-content:center;
  color:var(--txt)}
.claims__row h3{font-size:1.0625rem;margin-bottom:8px}
.claims__row p{font-size:.9375rem;margin:0}
/* Hover, 2026-09-23: the circled mark and the heading take the accent. The transitions sit on
   the CHILDREN, not on .claims__row - .claims carries reveal--stagger, and
   `.js .reveal.reveal--stagger.is-in > *` (0,4,0) targets the row itself, so a transition
   declared there would be overridden and the hover would snap (the trap .statcard hit). The
   mark and the h3 are grandchildren, which that selector never matches. */
.claims__mark{transition:border-color .18s,color .18s}
.claims__row h3{transition:color .18s}
.claims__row:hover .claims__mark{border-color:var(--txt-accent);color:var(--txt-accent)}
.claims__row:hover h3{color:var(--txt-accent)}
/* On a light band --txt-accent, --txt-strong, --primary and --ink-strong all resolve to
   #4B4DED, so the rule above recolours aqua-to-aqua and the hover does nothing. The same
   collapse bit the .vs__col and .nexttiles hovers earlier. --primary-hover (#7666F8) is the
   one token in that family that is actually different. */
.band-light .claims__row:hover .claims__mark,.band-white .claims__row:hover .claims__mark{
  border-color:var(--primary-hover);color:var(--primary-hover)}
.band-light .claims__row:hover h3,.band-white .claims__row:hover h3{color:var(--primary-hover)}
.band-light .pstep:hover::before,.band-white .pstep:hover::before{color:var(--primary-hover)}
.band-light .pstep:hover h3,.band-white .pstep:hover h3{color:var(--primary-hover)}

/* --- oversized review figure ---------------------------------------------- */
/* baseline alignment put the number's baseline on the span's FIRST line, so the second line
   ("106 Google reviews") dropped below it and left 23px of blank space under the 4.4 - the
   hole in the operator's screenshot. Centred, the two-line span straddles the number and the
   row tightens from 111px to 88px with no gap. */
.bigstat{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.bigstat b{font-family:var(--ff-display);font-size:clamp(3.2rem,8vw,5.5rem);font-weight:700;
  line-height:1;letter-spacing:-.03em;color:var(--txt-accent)}
.bigstat span{font-size:1.0625rem;color:var(--txt)}

/* --- NAP card ------------------------------------------------------------- */
/* --edge/--surface, not the raw white-at-alpha: on a dark band they resolve to
   rgba(255,255,255,.17) and rgba(255,255,255,.035), so the dark build does not move,
   but on a flipped band the card had no visible edge and no visible fill - only its
   4px Sky Blue bar. Fifth component with this defect, after .reach, .slot, .contrast
   and this card's own bold line. The shorthand stays FIRST and border-left
   re-declares after it, or the accent bar is repainted away. */
.napcard{border:1px solid var(--edge);border-left:4px solid var(--aqua);border-radius:var(--r);
  padding:28px 30px;background:var(--surface);max-width:520px}
.napcard address{font-style:normal;color:var(--txt);line-height:1.8;font-size:1rem}
/* --txt-strong, not --white: these three sit on bands the light theme flips, and their
   sibling lines (.napcard address / .reach span / .parenthub span.d) were already
   band-aware - only the bold line was hardcoded, so it rendered white on white.
   The fallback keeps the dark default if the component ever lands outside a band. */
.napcard strong{color:var(--txt-strong,var(--white))}
.napcard a{color:var(--txt-accent);text-decoration:none}
/* :not(.tlink) — .tlink brings its OWN hover underline as a border-bottom, so this
   container rule stacked a second text-decoration line on top of it: two underlines
   on every .tlink inside a NAP card. Same family as .article__body a:not(.btn) —
   a container rule must not claim links that already style their own hover. */
.napcard a:not(.tlink):hover{text-decoration:underline}

/* --- PENDING IMAGE SLOT: a designed placeholder, not a broken box ----------
   Used where the manifest declares a slot whose real asset has not been
   delivered. It must read as deliberate and must never be filled with a
   stand-in that asserts a real person's likeness. */
.slot{border:1px dashed rgba(255,255,255,.34);border-radius:var(--r-sig);
  background:repeating-linear-gradient(45deg,rgba(255,255,255,.028) 0 12px,transparent 12px 24px),
    rgba(255,255,255,.03);
  aspect-ratio:4/3.8;display:flex;flex-direction:column;align-items:center;justify-content:center;   /* 4/5 -> 4/3.8, operator 2026-09-25 */
  text-align:center;gap:10px;padding:28px}
.slot svg{color:var(--txt-accent)}
/* --txt-strong, not --white: white on a band the light theme flips is white on white,
   and "Matt Cellura, owner" simply vanished there. Same fix as .napcard strong and
   .reach b; a no-op on every dark band. */
.slot b{color:var(--txt-strong,var(--white));font-size:.9375rem}
.slot span{color:var(--txt);font-size:.8125rem;line-height:1.5;max-width:28ch}
.slot em{display:block;margin-top:4px;font-style:normal;font-size:.6875rem;font-weight:700;
  letter-spacing:.11em;text-transform:uppercase;color:var(--txt-accent)}
.slot--wide{aspect-ratio:16/10}

@media (max-width:900px){
  .crumbs{padding-top:118px}
  .hero--inner .hero__grid{grid-template-columns:1fr}
  .contrast,.claims__row{grid-template-columns:1fr}
  .claims__row{gap:14px}
}

/* ==========================================================================
   FORM — first codified on /contact/ (2026-09-18). Every later form inherits.
   White card on a dark band: one unmistakable focal point (Hick/Fitts).
   ========================================================================== */
.formcard{background:var(--white);border-radius:var(--r);border-top:3px solid var(--primary);
  padding:34px 34px 30px;color:var(--ink)}
.formcard h3{color:var(--ink-strong);font-size:1.375rem;margin-bottom:6px}
.formcard > p{color:var(--ink-muted);font-size:.9375rem;margin-bottom:24px}
.form{display:grid;grid-template-columns:1fr 1fr;gap:18px}
.field{display:flex;flex-direction:column;gap:7px}
.field--full{grid-column:1/-1}
.field label{font-size:.8125rem;font-weight:700;letter-spacing:.04em;color:var(--ink-strong)}
.field label .req{color:var(--primary)}
.field .hint{font-size:.75rem;font-weight:400;letter-spacing:0;color:var(--ink-muted)}
.field input,.field select,.field textarea{
  font-family:var(--ff);font-size:1rem;color:var(--ink);background:var(--bg);
  border:1px solid rgba(0,0,0,.26);border-radius:var(--r);padding:12px 14px;min-height:48px;
  width:100%;transition:border-color .16s,box-shadow .16s
}
.field textarea{min-height:110px;resize:vertical;line-height:1.55}
.field select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none'%3E%3Cpath d='M2 4.5 6 8.5l4-4' stroke='%234B4DED' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 14px center;padding-right:38px}
/* placeholders are TEXT — the browser default grey is ~2.5:1 and fails AA */
.field ::placeholder{color:var(--ink-muted);opacity:1}
.field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{
  outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(75,77,237,.28)}
/* consent is REQUIRED on every form capturing a phone number — never pre-ticked */
.consent{grid-column:1/-1;display:flex;gap:12px;align-items:flex-start;
  background:var(--wash);border-radius:var(--r);padding:16px 18px;margin-top:2px}
.consent input{flex:none;width:22px;height:22px;min-height:0;margin-top:2px;accent-color:var(--primary)}
.consent label{font-size:.8125rem;line-height:1.55;color:var(--ink);font-weight:400;letter-spacing:0}
.consent a{color:var(--link-on-light);font-weight:700}
.form__submit{grid-column:1/-1;margin-top:4px}
.form__submit .btn{width:100%}
.form__note{grid-column:1/-1;font-size:.75rem;color:var(--ink-muted);margin:0}

/* contact method cards */
.reach{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
/* --edge/--surface, not the raw white-at-alpha values: on a dark band those tokens
   resolve to exactly rgba(255,255,255,.17) and rgba(255,255,255,.035), so this is a
   no-op there - but on a band the light theme flips, white-on-white made the box
   vanish and left the text floating with only its 3px bar. The shorthand runs FIRST
   and border-top re-declares after it, which is the only safe order. */
.reach a,.reach div{display:flex;gap:16px;align-items:flex-start;text-decoration:none;
  border:1px solid var(--edge);border-top:3px solid var(--aqua);border-radius:var(--r);
  padding:24px 22px;background:var(--surface);
  transition:background-color .18s,border-top-color .18s}
.reach a:hover,.reach div:hover{background:rgba(40,145,243,.10);border-top-color:var(--bar-hover)}
/* TWO classes, deliberately: `.reach span` is (0,1,1) and out-specifies a
   single-class `.reach__ic`, which silently replaced display:flex with
   display:block and left the glyph jammed in the corner. Same family as
   `.quote__by span` vs `.quote__av`. Do not reduce this to one class. */
.reach .reach__ic{flex:none;width:42px;height:42px;border-radius:50%;background:rgba(40,145,243,.14);
  border:1px solid rgba(40,145,243,.42);display:flex;align-items:center;justify-content:center;
  color:var(--txt-accent);line-height:0}
.reach b{display:block;color:var(--txt-strong,var(--white));font-size:1.0625rem;margin-bottom:3px}
.reach span{display:block;color:var(--txt);font-size:.875rem;line-height:1.5}

/* numbered "before you call" steps */
.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:26px}
.step{position:relative;transition:background-color .18s,border-top-color .18s;border:1px solid var(--hair);border-top:3px solid var(--txt-accent);border-radius:var(--r);
  padding:34px 28px 28px;background:rgba(255,255,255,.035);overflow:hidden}
/* Same aqua top rule + hover as .call, .scard and .statcard, so every card on the site
   reacts identically. The card had a plain 1px border with no top line to recolour. */
.step:hover{background:rgba(40,145,243,.10);border-top-color:var(--bar-hover)}
.step__n{position:absolute;right:14px;top:0;font-family:var(--ff-display);font-size:5rem;
  font-weight:700;line-height:1;color:var(--white);opacity:.07;letter-spacing:-.04em;pointer-events:none}
.step h3{color:var(--white);font-size:1.0625rem;margin-bottom:10px}
.step p{color:var(--txt);font-size:.9375rem;margin:0}

/* ---- signature-gradient hover, contact page (2026-09-29, operator) --------
   The same effect as the homepage's .scard--grad, from the Brand Guide's own two
   stops. .step is used on /contact/ and nowhere else, so this needs no modifier
   class and no markup edit - the selector is already page-scoped in practice.

   .step carries overflow:hidden (it is what clips the oversized .step__n numeral),
   and overflow clips a pseudo-element to the PADDING box. The overlay therefore
   cannot cover the 1px/3px border the way .scard--grad's does, so the border is
   taken to transparent on hover instead and the gradient meets the band directly.

   .step__n is position:absolute already; it only needs lifting above the overlay,
   NOT position:relative - a blanket `.step > *` would have re-statically
   positioned it at equal specificity and thrown the numeral back into the flow.
   Its opacity goes up on hover because .07 of Brand Violet on white reads clearly
   while .07 of white on a saturated gradient is invisible. It is aria-hidden, so
   no contrast floor applies to it.

   CONTRAST: h3 is 17px/700 - UNDER the 18.66px-bold large-text threshold, so
   unlike the homepage card's 19px h3 it takes the full 4.5:1 and will not clear
   it on this gradient. Measured after the fact and reported to the operator, who
   asked for the signature gradient specifically. */
.step{isolation:isolate}
.step::before{
  content:"";position:absolute;inset:0;z-index:0;border-radius:inherit;
  background:linear-gradient(135deg,var(--primary-hover) 0%,var(--sky) 100%);
  opacity:0;transition:opacity .22s
}
.step:hover::before{opacity:1}
.step > h3,.step > p{position:relative;z-index:1}
.step__n{z-index:1}
/* later than the shared .step:hover above, and the band-role tokens are flipped
   rather than each child re-coloured - the light build repaints every h1-h4 in
   .band-lift at (0,3,1), which a plain .step:hover h3 at (0,2,1) cannot beat. */
.step:hover{
  background:var(--surface);border-color:transparent;
  --txt:var(--white);--txt-strong:var(--white);--ink-strong:var(--white);
  --txt-accent:var(--white)}
.step:hover .step__n{opacity:.18}
@media (prefers-reduced-motion:reduce){.step::before{transition:none}}

/* price list */
.prices{list-style:none;margin:0;padding:0;max-width:720px}
.prices li{display:flex;justify-content:space-between;align-items:baseline;gap:20px;
  padding:15px 0;border-bottom:1px dotted rgba(0,0,0,.26);font-size:.9375rem;color:var(--ink)}
.prices li:last-child{border-bottom:0}
.prices b{flex:none;font-family:var(--ff-display);font-size:1.25rem;font-weight:700;color:var(--primary)}

/* "where to go next" link tiles */
.nexttiles{display:grid;grid-template-columns:repeat(2,1fr);gap:18px}
.nexttiles a{display:block;text-decoration:none;border:1px solid var(--hair);
  border-top:3px solid var(--aqua);border-radius:var(--r);padding:22px 24px;
  background:rgba(255,255,255,.035);transition:background-color .18s,border-top-color .18s}
.nexttiles a:hover{background:rgba(40,145,243,.10);border-top-color:var(--bar-hover)}
.nexttiles b{display:block;color:var(--white);font-size:1rem;margin-bottom:4px}
.nexttiles span{display:block;color:var(--txt);font-size:.875rem;line-height:1.5}

@media (max-width:900px){
  .reach,.steps{grid-template-columns:1fr}
  .form,.nexttiles{grid-template-columns:1fr}
  .formcard{padding:26px 22px 24px}
}

/* ==========================================================================
   THIRD-PARTY FORM EMBED (GoHighLevel) — codified on /contact/ 2026-09-18
   Authority: Cortex L-097. Two independent hazards, both handled here.

   GEOMETRY. The rendered form measured 872 and 908px at 760px wide across
   loads, and 1214px at 560px and below, as its bot-protection widget
   resolves. `height:100%` on an iframe in normal flow collapses to nothing,
   and a tight reservation clips. So: reserve a FLOOR ABOVE the tallest
   observed height, per breakpoint, and accept the dead space deliberately —
   a clipped form is conversion-fatal, dead space is not. form_embed.js may
   grow it from there. Re-measure after ANY change to the form: these floors
   are only valid for the version measured on 2026-09-18.

   COMPLIANCE. See the note rendered beside the form. It is a MITIGATION,
   NOT COMPLIANCE — a cross-origin iframe cannot be given a field, a
   required attribute or a different button label from this page.
   ========================================================================== */
/* form_embed.js measures the real form and writes an inline height on the iframe, so this
   is a PRE-LOAD placeholder, not the display height. At 940 it out-ranked an 804px form
   and forced 136px of dead space inside the card - half of "the form is too large". */
.ghl{width:100%;border:none;display:block;min-height:804px}
.formcard--embed{padding:22px}
.formcard--embed .formcard__head{padding:12px 12px 20px}
/* iframe width = min(784, viewport-92), and GHL flips to its tall layout below
   ~560px of iframe width — i.e. below ~652px of viewport. Reserving the tall
   floor any earlier just buys dead space. */
@media (max-width:660px){ .ghl{min-height:1111px} }

/* the consent wording we CAN control, rendered beside the embed */
.consent-note{grid-column:1/-1;display:flex;gap:12px;align-items:flex-start;
  background:var(--wash);border-left:3px solid var(--primary);border-radius:var(--r);
  padding:16px 18px;margin-top:18px}
.consent-note svg{flex:none;color:var(--primary);margin-top:2px}
.consent-note p{font-size:.8125rem;line-height:1.55;color:var(--ink);margin:0}
.consent-note a{color:var(--link-on-light);font-weight:700}

/* ==========================================================================
   HUB ROUTING CARDS — first codified on /services/ (2026-09-18)
   Contract: card_image required · grid_arity 3/2/1 · orphan_centering required.
   FLEX, not grid: 7 cards in 3 columns leave one orphan, and flex-wrap with
   justify-content:center centres it. A CSS grid would left-hang it.
   ========================================================================== */
.hubgrid{display:flex;flex-wrap:wrap;justify-content:center;gap:28px}
.hubcard{flex:0 1 calc((100% - 56px) / 3);min-width:0;display:flex;flex-direction:column;
  text-decoration:none;background:var(--white);border:1px solid var(--hair-dark);
  border-top:3px solid var(--aqua);border-radius:var(--r);overflow:hidden;
  transition:transform .18s,border-color .18s}
.hubcard:hover{transform:translateY(-3px);border-top-color:var(--bar-hover)}
.hubcard__media{position:relative;aspect-ratio:16/10;overflow:hidden;background:var(--wash)}
.hubcard__media img{width:100%;height:100%;object-fit:cover;display:block}
.hubcard__tag{position:absolute;right:12px;bottom:12px;background:var(--white);color:var(--primary);
  font-size:.6875rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  padding:7px 12px;border-radius:999px}
.hubcard__body{padding:24px 24px 22px;display:flex;flex-direction:column;flex:1}
/* h2, not h3: the content doc declares each hub as a top-level section heading,
   and heading LEVEL is a content property even when the composition is a card. */
.hubcard h2{font-size:1.1875rem;color:var(--ink-strong);margin-bottom:10px;line-height:1.3;
  font-family:var(--ff-display);font-weight:700;letter-spacing:-.01em}
.hubcard p{font-size:.9375rem;color:var(--ink);margin-bottom:14px}
.hubcard__for{font-size:.875rem;color:var(--ink-muted);padding-top:14px;
  border-top:1px dotted rgba(0,0,0,.26);margin-top:auto}
.hubcard__for b{color:var(--ink-strong)}
.hubcard__go{display:inline-flex;align-items:center;gap:.5em;margin-top:16px;
  font-weight:700;font-size:.9375rem;color:var(--link-on-light)}
.hubcard__go svg{transition:transform .2s}
.hubcard:hover .hubcard__go svg{transform:translateX(4px)}

/* in-card image slot, for a hub with no eligible photograph yet.
   The card still exists and still routes — dropping it would break navigation. */
.slot--card{aspect-ratio:16/10;border:0;border-bottom:1px dashed rgba(0,0,0,.24);
  border-radius:0;background:
    repeating-linear-gradient(45deg,rgba(75,77,237,.05) 0 12px,transparent 12px 24px),var(--wash);
  gap:8px;padding:18px}
.slot--card svg{color:var(--primary)}
.slot--card b{color:var(--ink-strong);font-size:.875rem}
.slot--card span{color:var(--ink-muted);font-size:.75rem;max-width:30ch}
.slot--card em{color:var(--primary);font-size:.625rem}

/* exclusions list */
.donot{list-style:none;margin:0;padding:0;max-width:900px}
.donot li{display:grid;grid-template-columns:auto 1fr;gap:16px;align-items:start;padding:16px 0;
  border-bottom:1px dotted rgba(255,255,255,.22);color:var(--txt);font-size:.9375rem}
.donot li:last-child{border-bottom:0}
/* Hover, matching .claims. Transitions sit on the GRANDCHILDREN, not the li: the li is a
   .reveal--stagger child and that rule (0,3,0) outruns a transition declared on the li. */
.donot__x{transition:border-color .18s,color .18s}
.donot li strong{transition:color .18s}
.donot li:hover .donot__x{border-color:var(--txt-accent);color:var(--txt-accent)}
.donot li:hover strong{color:var(--txt-accent)}

.donot__x{flex:none;width:26px;height:26px;border-radius:50%;border:1.5px solid rgba(255,255,255,.34);
  display:flex;align-items:center;justify-content:center;color:var(--txt);margin-top:1px}

@media (max-width:1000px){ .hubcard{flex:0 1 calc((100% - 28px) / 2)} }
@media (max-width:640px){ .hubcard{flex:0 1 100%} .donot li{grid-template-columns:1fr} }

/* ==========================================================================
   SERVICE HUB COMPONENTS — first codified on /services/pool-safety-covers/
   (2026-09-18). R22 locks every later hub's section sequence to this page.
   ========================================================================== */

/* --- comparison table with the MOBILE CARD-STACK treatment ----------------
   Below 760px the table stops being a grid and each row becomes a labelled
   card row, so nothing has to scroll sideways. data-label carries the column
   header; without it the stacked view loses its meaning. */
.ctable-wrap{border:1px solid var(--hair-dark);border-top:3px solid var(--primary);
  border-radius:var(--r);overflow:hidden;background:var(--white)}
.ctable{width:100%;border-collapse:collapse;font-size:.9375rem}
.ctable th,.ctable td{padding:15px 20px;text-align:left;vertical-align:top;
  border-bottom:1px dotted rgba(0,0,0,.26)}
.ctable thead th{background:var(--tint);font-size:.75rem;font-weight:700;letter-spacing:.11em;
  text-transform:uppercase;color:var(--primary);border-bottom:1px solid rgba(0,0,0,.16)}
.ctable tbody th{font-weight:700;color:var(--ink-strong);width:30%}
.ctable td{color:var(--ink)}
.ctable tr:last-child th,.ctable tr:last-child td{border-bottom:0}
@media (max-width:760px){
  .ctable,.ctable tbody,.ctable tr,.ctable td,.ctable th{display:block;width:auto}
  .ctable thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
  .ctable tr{border-bottom:1px solid rgba(0,0,0,.16);padding:6px 0}
  .ctable tr:last-child{border-bottom:0}
  .ctable tbody th{width:auto;padding:14px 18px 6px;font-size:.9375rem}
  .ctable td{padding:6px 18px 12px;display:flex;gap:10px}
  .ctable td::before{content:attr(data-label);flex:none;min-width:58px;font-size:.75rem;
    font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--primary);
    padding-top:3px}
}

/* --- [CLIENT TO CONFIRM] marker: must be VISIBLE, never silently dropped --- */
.tbc{display:inline-flex;align-items:center;gap:6px;background:rgba(75,77,237,.10);
  color:var(--primary);border:1px dashed rgba(75,77,237,.5);border-radius:var(--r);
  padding:3px 9px;font-size:.75rem;font-weight:700;letter-spacing:.02em;white-space:normal}
.band-navy .tbc,.band-lift .tbc,.band-deep .tbc{background:rgba(40,145,243,.12);
  color:var(--txt-accent);border-color:rgba(40,145,243,.5)}

/* --- numbered process steps (.process-step in the content doc) ------------ */
.psteps{counter-reset:ps;display:grid;gap:0;max-width:940px}
.pstep{counter-increment:ps;display:grid;grid-template-columns:auto 1fr;gap:24px;
  padding:26px 0;border-bottom:1px dotted rgba(255,255,255,.22)}
.pstep:last-child{border-bottom:0}
.pstep::before{content:counter(ps,decimal-leading-zero);font-family:var(--ff-display);
  font-size:1.75rem;font-weight:700;line-height:1;color:var(--txt-accent);min-width:52px;
  padding-top:2px;letter-spacing:-.02em}
.pstep h3{font-size:1.0625rem;margin-bottom:8px}
.pstep p{font-size:.9375rem;margin:0}

/* --- two-column "this vs that" prose comparison --------------------------- */
.vs{display:grid;grid-template-columns:1fr 1fr;gap:26px;margin-top:8px}
/* --txt-accent, not a hardcoded --aqua: these cards sit on a WHITE ground, where aqua is
   2.03:1 and fails the 3:1 floor for a non-text graphic. The band token resolves to
   --primary on light grounds (4.94:1 on white) - the same correction the eyebrow rule
   and the wavemark already carry. */
.vs__col{transition:border-top-color .18s,transform .18s,box-shadow .18s;border:1px solid var(--hair-dark);border-top:3px solid var(--txt-accent);border-radius:var(--r);
  padding:28px 26px;background:var(--white)}
/* Hover: the top rule takes --txt-strong (band-aware, so navy on this light ground) and the
   card lifts 2%. transform is cheap to animate and the global prefers-reduced-motion block
   already disables it. z-index keeps the scaled card above its neighbour rather than being
   clipped by it. */
/* --primary-hover, not --txt-strong: on a light band --txt-accent, --txt-strong, --primary
   and --ink-strong ALL resolve to #4B4DED, so the hover recoloured the rule to the colour
   it already was and nothing visibly changed. --primary-hover is the darker step the
   primary button already uses. */
.vs__col:hover{border-top-color:var(--bar-hover);transform:scale(1.02);
  box-shadow:0 10px 30px rgba(0,0,0,.10);position:relative;z-index:2}
.vs__col h3{font-size:1.125rem;color:var(--ink-strong);margin-bottom:12px}
.vs__col p{font-size:.9375rem;color:var(--ink);margin-bottom:0}
.vs__col p + p{margin-top:.9em}

@media (max-width:900px){ .vs{grid-template-columns:1fr} .pstep{grid-template-columns:auto 1fr;gap:16px} }

/* ==========================================================================
   SERVICE SPOKE — first codified on /services/safety-cover-installation/
   (2026-09-18). A spoke is deliberately LIGHTER than its hub: the hub owns the
   argument at depth, the spoke owns one intent. The parent-hub card makes that
   relationship visible so the spoke never reads as an orphan.
   ========================================================================== */
/* --edge/--surface: on a dark band they resolve to exactly rgba(255,255,255,.17) and
   rgba(255,255,255,.035), so the dark build is unchanged - on a flipped band the card
   had no edge and no fill, only its 3px Sky Blue bar. Sixth component with this
   defect, after .reach, .slot, .contrast, .napcard and this card's own bold line.
   border shorthand FIRST, border-left after it, or the accent bar is repainted. */
.parenthub{display:flex;gap:18px;align-items:center;text-decoration:none;
  border:1px solid var(--edge);border-left:3px solid var(--aqua);border-radius:var(--r);
  padding:20px 24px;background:var(--surface);transition:background-color .18s}
.parenthub:hover{background:rgba(40,145,243,.10)}
.parenthub__ic{flex:none;width:42px;height:42px;border-radius:50%;background:rgba(40,145,243,.14);
  border:1px solid rgba(40,145,243,.42);display:flex;align-items:center;justify-content:center;
  color:var(--txt-accent);line-height:0}
.parenthub__t{display:block;font-size:.75rem;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:var(--txt-accent);margin-bottom:4px}
.parenthub b{display:block;color:var(--txt-strong,var(--white));font-size:1.0625rem;margin-bottom:3px}
.parenthub span.d{display:block;color:var(--txt);font-size:.875rem;line-height:1.5}
.parenthub__go{flex:none;margin-left:auto;color:var(--txt-accent);transition:transform .2s}
.parenthub:hover .parenthub__go{transform:translateX(4px)}
@media (max-width:640px){ .parenthub{flex-wrap:wrap} .parenthub__go{margin-left:0} }

/* ==========================================================================
   GROUND-AWARE .nexttiles  (fix, 2026-09-18)
   The component was authored for DARK grounds (color:var(--white)). Reused on
   a light band it rendered white-on-white at 1.00:1 — 100 gate failures on
   /twinsburg-oh/. A component that hardcodes a text colour is only correct on
   the ground it was born on. Light-ground roles are declared here, and the
   arity modifiers below replace inline grid-template overrides that never
   collapsed on mobile.
   ========================================================================== */
.band-light .nexttiles a,.band-white .nexttiles a{background:var(--white);
  border-color:var(--hair-dark);border-top-color:var(--primary)}
.band-light .nexttiles a:hover,.band-white .nexttiles a:hover{background:var(--tint)}
.band-light .nexttiles b,.band-white .nexttiles b{color:var(--ink-strong)}
.band-light .nexttiles span,.band-white .nexttiles span{color:var(--ink)}

/* Flex, not grid, so an incomplete last row CENTRES instead of hanging left -- the same
   pattern .donotgrid already uses. A grid cannot do this: the 4th item is locked to
   column 1. The basis matches a 3-column grid exactly (3 x basis + 2 x 18px gap = 100%),
   so the one complete-row instance on /twinsburg-oh/ renders identically to before. */
.nexttiles--3{display:flex;flex-wrap:wrap;justify-content:center;gap:18px}
.nexttiles--3 > *{flex:0 1 calc((100% - 36px) / 3)}
/* Image-bearing tiles. A MODIFIER, not a change to .nexttiles, because Contact, Resources
   and the cover-installation page all use the plain text tile and must not gain a photo slot.
   16/9 matches .artfig, the build's existing figure ratio. It crops the one portrait source
   hard, so check that tile if its photo is ever swapped. */
.nexttiles--media a{padding:0;overflow:hidden;display:flex;flex-direction:column}
.nexttiles--media img{width:100%;aspect-ratio:16/8;object-fit:cover;display:block;
  transition:transform .35s cubic-bezier(.22,.61,.36,1)}
.nexttiles--media a:hover img{transform:scale(1.04)}
.nexttiles--media .nexttiles__txt{padding:18px 22px 20px}
@media (prefers-reduced-motion:reduce){.nexttiles--media a:hover img{transform:none}}
/* Icon tiles. A MODIFIER, like .nexttiles--media, because the plain tile is used without
   an icon on several other pages. TWO classes on the icon deliberately: `.nexttiles span`
   is (0,1,1) and would otherwise replace display:flex with display:block and jam the glyph
   into the corner - the trap already documented on .reach__ic and .quote__av. The circle
   reads band roles, so it cannot render wrong on a light band; the hover target is
   --bar-hover because the glyph is ALREADY --txt-accent at rest. */
.nexttiles--icons a{display:flex;gap:14px;align-items:flex-start}
.nexttiles .nexttiles__ic{flex:none;width:38px;height:38px;border-radius:50%;
  background:var(--surface-hover);border:1px solid var(--rule);
  display:flex;align-items:center;justify-content:center;color:var(--txt-accent);
  line-height:0;transition:background-color .18s,border-color .18s,color .18s}
.nexttiles .nexttiles__ic svg{width:18px;height:18px}
.nexttiles--icons a:hover .nexttiles__ic{border-color:var(--bar-hover);color:var(--bar-hover)}
.nexttiles--4{grid-template-columns:repeat(4,1fr)}
.nexttiles--1{grid-template-columns:1fr}
/* Centred flex, not a 2-col grid: the only --2 grid on the site holds FIVE tiles, and a
   grid strands the fifth at the left of its row. Same resolution as .nexttiles--3 and the
   card-grid rule in the build standards - an incomplete last row centres. */
.nexttiles--2{display:flex;flex-wrap:wrap;justify-content:center;gap:18px}
.nexttiles--2 > *{flex:0 1 calc((100% - 18px) / 2)}
@media (max-width:640px){.nexttiles--2 > *{flex-basis:100%}}
/* Non-link tile. These are statements, not destinations, so they must not be anchors --
   which is why they cannot just reuse the .nexttiles a rule. Built on the band-declared
   roles instead of on --hair/--white, so it cannot render white-on-near-white the way
   .nexttiles and .claims each did once. h3 keeps the heading level the claims list had. */
.nexttiles .tile{border:1px solid var(--edge);border-top:3px solid var(--txt-accent);
  border-radius:var(--r);padding:22px 24px;background:var(--surface);
  transition:background-color .18s,border-top-color .18s}
.nexttiles .tile:hover{background:var(--surface-hover,rgba(40,145,243,.10));
  border-top-color:var(--bar-hover)}
.nexttiles .tile h3{color:var(--txt-strong);font-size:1rem;margin:0 0 5px;transition:color .18s}
.nexttiles .tile p{color:var(--txt);font-size:.875rem;line-height:1.55;margin:0}
.nexttiles .tile:hover h3{color:var(--txt-accent)}
/* Image-bearing non-link tile. Must sit AFTER the .tile block above: both are (0,2,0), so
   .nexttiles .tile would otherwise win on source order and restore the 22px padding. */
.nexttiles--media .tile{padding:0;overflow:hidden;display:flex;flex-direction:column}
.nexttiles--media .tile:hover img{transform:scale(1.04)}
@media (prefers-reduced-motion:reduce){.nexttiles--media .tile:hover img{transform:none}}
@media (max-width:1000px){ .nexttiles--4{grid-template-columns:repeat(2,1fr)}
  .nexttiles--3 > *{flex-basis:calc((100% - 18px) / 2)} }
@media (max-width:640px){ .nexttiles--4{grid-template-columns:1fr}
  .nexttiles--3 > *{flex-basis:100%} }

/* ==========================================================================
   GROUND-AGNOSTIC TEXT ROLES  (structural fix, 2026-09-18)

   Twice now a component authored on a dark band has been reused on a light one
   and rendered white-on-near-white: .nexttiles on /twinsburg-oh/ (100 failures)
   and .claims on the LSP (50). Fixing each instance is whack-a-mole, because the
   defect is that a component hardcodes a text colour AT ALL.

   So the BAND declares the roles and portable components reference them. A
   component then cannot be wrong about the ground it is standing on.
   ========================================================================== */
.band-navy,.band-lift,.band-deep,.cta-band,.hero,.elig{
  --btn-on-accent:var(--ink-navy);
  --txt:rgba(255,255,255,.80);
  --txt-strong:var(--white);
  --txt-accent:var(--accent-on-dark);
  --rule:rgba(255,255,255,.22);
  --surface:rgba(255,255,255,.035);
  --surface-hover:rgba(40,145,243,.10);
  --bar-hover:var(--white);            /* card top-bar on hover - white on dark ground */
  --edge:var(--hair);
}
.band-light,.band-white{
  --btn-on-accent:var(--white);
  --txt:var(--ink);
  --txt-strong:var(--ink-strong);
  --txt-accent:var(--primary);
  --rule:rgba(0,0,0,.26);
  --surface:var(--white);
  --surface-hover:var(--tint);
  /* white would be invisible on a light card, so the light band darkens instead.
     --txt-strong/--txt-accent/--primary all collapse to #4B4DED here, which is the
     REST colour of most of these bars, so --primary-hover is the only value that
     actually reads as a change. Same collapse that bit .claims and .pstep. */
  --bar-hover:var(--primary-hover);
  --edge:var(--hair-dark);
}

/* portable components now read the band's roles instead of hardcoding */
.claims__row{border-bottom-color:var(--rule)}
.claims__row h3{color:var(--txt-strong)}
.claims__row p{color:var(--txt)}
.claims__mark{border-color:var(--rule);color:var(--txt)}
.claims__row strong{color:var(--txt-accent)}

.pstep{border-bottom-color:var(--rule)}
.pstep h3{color:var(--txt-strong)}
.pstep p{color:var(--txt)}
.pstep strong{color:var(--txt-accent)}
.pstep::before{color:var(--txt-accent)}
/* Hover, matching .claims. The number is ALREADY --txt-accent at rest, so it moves the other
   way -- number to --txt-strong, heading to --txt-accent -- or hovering would change nothing.
   Transitions sit on the pseudo and the h3, not on .pstep: .pstep is a .reveal--stagger child
   and that rule (0,3,0) outruns a transition declared on the item itself. */
.pstep::before{transition:color .18s}
.pstep h3{transition:color .18s}
.pstep:hover::before{color:var(--txt-strong)}
.pstep:hover h3{color:var(--txt-accent)}

.donot li{border-bottom-color:var(--rule);color:var(--txt)}
.donot li strong{color:var(--txt-strong)}
.donot__x{border-color:var(--rule);color:var(--txt)}

.step{border-color:var(--edge);border-top-color:var(--txt-accent);background:var(--surface)}  /* border-color is a shorthand and was repainting the 3px aqua top away - same defect
     already fixed on .call and .scard. .step only acquired a top rule today, so it hit
     the same rule the moment it did. */
.step h3{color:var(--txt-strong)}
.step p{color:var(--txt)}
.step__n{color:var(--txt-strong)}

/* border-color is a SHORTHAND - it repainted all four sides and silently killed the 3px
   aqua top these two declare, against the lock's "Cards - aqua top border". Restored with
   --txt-accent so it stays band-aware: aqua on the dark grounds, --primary on the light
   ones, where aqua is only 2.77:1 and would fail the 3:1 floor for a non-text graphic. */
.scard{border-color:var(--edge);border-top-color:var(--txt-accent);background:var(--surface)}
.scard h3{color:var(--txt-strong)}
.scard p{color:var(--txt)}

.call{border-color:var(--edge);border-top-color:var(--txt-accent);background:var(--surface)}
.call h3{color:var(--txt-strong)}
.call p{color:var(--txt)}
.call__n{color:var(--txt-strong)}

/* border-color is a SHORTHAND: it repainted all four sides and wiped the 4px aqua left
   bar off all 7 callouts on the site. Same defect already annotated on .step/.scard/
   .call and fixed on a.hubcard:hover. Re-set the accent after it, band-aware. */
.callout{border-color:var(--edge);border-left-color:var(--txt-accent);background:var(--surface)}
.callout p{color:var(--txt)}
.callout b{color:var(--txt-accent)}

/* ==========================================================================
   INDEX / ARTICLE COMPONENTS — codified 2026-09-18 for the remaining templates
   ========================================================================== */

/* blog index: post cards + pagination. The inherited post and the net-new posts
   share ONE date-sorted stream — a separate "old posts" section would signal
   second-class content to readers and crawlers alike. */
.postgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.postcard{display:flex;flex-direction:column;text-decoration:none;background:var(--surface);
  border:1px solid var(--edge);border-top:3px solid var(--aqua);border-radius:var(--r);
  padding:24px 24px 22px;transition:transform .18s,border-color .18s}
.postcard:hover{transform:translateY(-3px);border-top-color:var(--bar-hover)}
.postcard__meta{display:flex;flex-wrap:wrap;gap:8px 14px;align-items:center;margin-bottom:12px;
  font-size:.75rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--txt-accent)}
.postcard__season{border:1px solid var(--rule);border-radius:999px;padding:3px 10px;letter-spacing:.08em}
.postcard h3{font-size:1.0625rem;line-height:1.35;color:var(--txt-strong);margin-bottom:12px}
.postcard__go{margin-top:auto;padding-top:14px;font-weight:700;font-size:.875rem;color:var(--txt-accent);
  display:inline-flex;align-items:center;gap:.5em}
.postcard__go svg{transition:transform .2s}
.postcard:hover .postcard__go svg{transform:translateX(4px)}
.postcard--kept{border-top-color:var(--primary)}
.postcard__kept{border:1px dashed var(--rule);border-radius:999px;padding:3px 10px;color:var(--txt)}
/* Image-bearing post card. A MODIFIER on the grid, because the related-posts cards inside
   a blog post reuse .postcard and must stay text-only. The inner wrapper is what keeps
   .postcard__go{margin-top:auto} pinning "Read More" to the bottom: with an image as the
   card's first flex child, auto margin would push against the image instead. */
.postgrid--media .postcard{padding:0;overflow:hidden}
.postgrid--media .postcard img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block;
  transition:transform .35s cubic-bezier(.22,.61,.36,1)}
.postgrid--media .postcard:hover img{transform:scale(1.04)}
.postgrid--media .postcard__body{display:flex;flex-direction:column;flex:1;padding:20px 22px 20px}
@media (prefers-reduced-motion:reduce){.postgrid--media .postcard:hover img{transform:none}}

.pager{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:center;margin-top:38px}
.pager a,.pager span{min-width:44px;min-height:44px;display:inline-flex;align-items:center;
  justify-content:center;padding:0 14px;border:1px solid var(--edge);border-radius:var(--r);
  text-decoration:none;font-weight:700;font-size:.9375rem;color:var(--txt-strong);background:var(--surface)}
.pager a:hover{border-color:var(--txt-accent);color:var(--txt-accent)}
.pager [aria-current]{background:var(--primary);border-color:var(--primary);color:#fff}
.pager .pager__gap{border:0;background:none;min-width:auto;padding:0 4px;color:var(--txt)}

/* resources index: grouped guide lists */
.guidegroup{margin-bottom:8px}
.guidelist{display:grid;grid-template-columns:repeat(2,1fr);gap:18px;margin-top:6px}
/* Three across for a three-tile group, so it fills one row instead of wrapping 2 + 1.
   Scoped: the four-tile "Before You Buy" group is a tidy 2x2 and the two-tile
   Brands / Project Gallery group needs no change. */
.guidelist--3{grid-template-columns:repeat(3,1fr)}
@media (max-width:1000px){ .guidelist--3{grid-template-columns:repeat(2,1fr)} }
.guidelist a{display:block;text-decoration:none;background:var(--surface);border:1px solid var(--edge);
  border-top:3px solid var(--aqua);border-radius:var(--r);padding:22px 24px;
  transition:background-color .18s,border-top-color .18s}
.guidelist a:hover{border-top-color:var(--bar-hover)}
.guidelist b{display:block;color:var(--txt-strong);font-size:1rem;margin-bottom:5px}
.guidelist span{display:block;color:var(--txt);font-size:.875rem;line-height:1.55}
/* Image-bearing guide tile. Ties with .guidelist a on specificity (0,1,1), so it has to
   sit after it to win the padding reset.
   3:2, NOT 16:9: two of the seven sources are portrait (liner-install, equipment-service)
   and at 1.78 the technician's head is cut off in both. 1.50 is the shortest ratio that
   keeps them whole -- checked against the actual files, not assumed.
   max-height:215px then caps BOTH groups, so 3:2 is now only the ceiling, not what renders:
   the two-column tiles land near 2.8 and the three-column ones near 1.9. Chosen on the
   rendered page rather than derived -- if it is ever raised, re-check the two portrait
   tiles in the three-column group first, since they are what the ratio floor was for.
   The Brands / Project Gallery grid keeps the plain text tile. */
.guidelist--media a{padding:0;overflow:hidden;display:flex;flex-direction:column}
.guidelist--media img{width:100%;aspect-ratio:3/2;max-height:215px;object-fit:cover;display:block;
  transition:transform .35s cubic-bezier(.22,.61,.36,1)}
.guidelist--media a:hover img{transform:scale(1.04)}
.guidelist--media .guidelist__txt{padding:18px 20px 20px}
@media (prefers-reduced-motion:reduce){.guidelist--media a:hover img{transform:none}}

/* long-form article body (inherited blog post) */
.article{max-width:100%}
.article__meta{display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center;margin-bottom:26px;
  font-size:.8125rem;font-weight:700;letter-spacing:.06em;color:var(--txt-accent)}
.article__body{font-size:1.0625rem;line-height:1.72;color:var(--txt)}
.article__body > * + *{margin-top:1.1em}
.article__body h2{font-size:1.75rem;color:var(--txt-strong);margin-top:1.8em;margin-bottom:.1em}
.article__body h3{font-size:1.1875rem;color:var(--txt-strong);margin-top:1.4em;margin-bottom:.1em;
  font-family:var(--ff);font-weight:700}
.article__body strong{color:var(--txt-strong)}
/* 1.3em only cleared the marker, so the bullets sat flush with the paragraph edge and the
   list did not read as a block (operator 2026-09-23). 2.5em sets the marker about 1.5em in
   and the list text about 2.5em, which indents the run without orphaning it from the prose.
   Lives on .article__body so every migrated post inherits it, not just this one. */
.article__body ul,.article__body ol{padding-left:2.5em}
.article__body li{margin-top:.5em}
.article__body a:not(.btn){color:var(--link-on-light);font-weight:700}

@media (max-width:1000px){ .postgrid{grid-template-columns:repeat(2,1fr)} }
@media (max-width:760px){ .postgrid,.guidelist,.guidelist--3{grid-template-columns:1fr} }

/* ==========================================================================
   BUTTON LABEL GUARD — last rule in the sheet, deliberately.

   This defect family has now produced FIVE separate failures on this build:
     .quote__by span  -> .quote__av        (avatar, 1.74:1)
     .nav a           -> .mega a           (whole mega layer, 1.00:1)
     .reach span      -> .reach__ic        (icon could not centre)
     .footer a        -> footer buttons    (caught pre-emptively)
     .article__body a -> .btn--primary     (button label, 1.00:1)

   Every one is a container rule repainting something placed inside it. Container
   rules keep being written, and each new one is a chance to repaint a button, so
   the containers are ALSO scoped with :not(.btn) where they exist. This block is
   the belt to that braces: a two-class button selector, last in source order, so
   a button label is correct wherever a button is dropped.
   ========================================================================== */
/* ── .glance — the blog post's "in one glance" list.
   It lives inside .callout, whose `b` IS the big-stat treatment (2.125rem/800/aqua,
   used once on the locked homepage for `$150`). Left unscoped, these five prose
   labels rendered at 34px. `.glance li b` is (0,1,2) and beats `.callout b` (0,1,1)
   on SPECIFICITY, so the fix does not depend on source order (L-047).
   Colours come from the band-declared roles, so the list is ground-agnostic (L-124). */
.glance{list-style:none;margin:0;padding:0}
.glance li{padding:10px 0;border-top:1px solid var(--rule);color:var(--txt);
  font-size:1rem;line-height:1.55}
.glance li:first-child{border-top:0;padding-top:0}
.glance li b{display:inline;margin-right:7px;font-family:var(--ff);font-size:1rem;
  font-weight:700;letter-spacing:0;line-height:inherit;color:var(--txt-strong)}

/* ── .hero__lede — the hero's heading block (eyebrow/meta + h1 + sub).
   It MUST share a left edge with the .crumbs breadcrumb directly above it. Capping this
   block's own max-width does not do that: .container carries margin-inline:auto, so a
   narrower box re-centres itself and the heading renders indented from the breadcrumb;
   adding margin-inline:0 then pins it to the viewport gutter, 80px LEFT of it at 1440.
   So the block stays a full .container and the MEASURE is capped on the CHILDREN.
   --post comes second deliberately: it ties with the base rule on specificity (0,1,0)
   and wins on source order, which is the one place that is safe to rely on (L-047). */
.hero__lede{position:relative;z-index:2;padding-top:18px;padding-bottom:40px}
.hero__lede > *{max-width:820px}
.hero__lede--post{padding-top:14px;padding-bottom:0}
.hero__lede--post > *{max-width:950px}   /* 900 -> 950, operator 2026-09-27: the H1 broke
   'Outdoor Investments?' onto a line of its own at 900. Only /blog/winterizing-your-backyard/
   carries --post, and it applies in both builds. */

/* .napcard--center — the centred NAP block. Ties with .napcard on specificity (0,1,0),
   so it MUST sit after it in the sheet; it does, being appended near the end.
   2026-09-23: the card chrome is REMOVED here on operator instruction - "do not add a box
   over text/paragraphs that do not need it". Full width and centred, the border, fill and
   aqua bar were decoration around a plain paragraph rather than a marker for an address
   block. The 95 narrower .napcard instances keep theirs, where the box does separate the
   NAP from the content beside it. max-width is released too, or the centred text would
   still be corralled into a 520px column. */
.napcard--center{margin-inline:auto;text-align:center;
  border:0;border-radius:0;background:none;padding:0;max-width:none}

/* .pvbar a — the L-058 index chip. ONE underline mechanism only: text-decoration,
   never a border-bottom as well, which is the pair that produced the double underline
   inside .napcard. Aqua on the bar's #141428 is far above AA. */
.pvbar a{color:var(--txt-accent);text-decoration:underline;text-underline-offset:2px}
.pvbar a:hover{color:var(--white)}

/* ══════════════════════════════════════════════════════════════════════════
   ROUND 4 (2026-09-22) — Nicole's QA round: imagery, section variety,
   animation, interactivity. Colours come from the band-declared roles only.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── media split — copy one side, a 16:10 image the other, sides alternating.
   This is the workhorse for the 78 prose-only sections and where most of the
   new imagery lands. On mobile the flip modifier is reset so the text ALWAYS
   leads: an alternating layout that alternates at 400px just looks random. */
/* align-items:START, not center. Centred, a short body floats to the vertical
   middle of a taller image and opens a gap between the section heading and its
   own first paragraph - ~200px on /services/. Both columns now begin at the top,
   level with each other. --fill overrides this with stretch. */
/* Heading + copy on the left, one action hugging the right. flex-wrap means the button
   drops under the text on narrow screens without a second breakpoint. */
.ctarow{display:flex;align-items:center;justify-content:space-between;gap:38px;flex-wrap:wrap}
.ctarow__body{flex:1 1 460px;min-width:0}
.ctarow__body > :last-child{margin-bottom:0}
.ctarow > .btn{flex:none}
@media (max-width:760px){.ctarow{align-items:flex-start}}
.mediasplit{display:grid;grid-template-columns:1fr 1fr;gap:clamp(26px,4vw,60px);align-items:center}
.mediasplit__media img{width:100%;height:auto;aspect-ratio:16/10;object-fit:cover;
  border-radius:var(--r-sig);border:1px solid var(--edge)}
.mediasplit__cap{margin-top:10px;font-size:.8125rem;line-height:1.5;color:var(--txt)}
/* A portrait source crops badly in the default 16:10 slot -- at 1.60 the team photo loses
   the tops of both heads. 4:3 keeps the faces whole while still reading as a landscape
   panel beside the copy. */
.mediasplit--tallmedia .mediasplit__media img{aspect-ratio:4/3}
/* A grid item defaults to min-width:auto, so it refuses to shrink below its content's
   min-content width. With .psteps inside, that floor came out at 462px and the body burst
   56px out of a 382px container at 430. min-width:0 is the standard release valve -- the
   same one .trust__col already carries. */
.mediasplit__body,.mediasplit__media{min-width:0}
.mediasplit__body > :last-child{margin-bottom:0}
.mediasplit--flip .mediasplit__media{order:-1}
/* Applies ONLY to the two top/stretch-aligned variants. Everything else now centres, and a
   centred column needs no lead-in offset at all.
   Image top aligns to the CAP HEIGHT of the h2, not to the top of the column. Flush to the
   column the image starts above the wavemark and reads as floating; sat on the eyebrow it
   still reads high against the headline. 71px is the sum of three measured parts:
       30px  wavemark block  (34px wide, 15.58px tall at its 48x22 viewBox, + 14px margin)
       33px  eyebrow block   (13px text in a 21.45px line box, + the locked 12px eyebrow gap)
        8px  h2 leading      ((line-height - cap height) / 2, so the edge meets the letters
                              rather than the line box sitting above them)
   The h2 is clamp()-sized so the last part drifts: measured 71px at 1440 and 1280, 70px at
   1100 / 1000 / 950. One pixel across the whole desktop range, so a constant is honest here.
   :has() scopes it to splits that actually lead with a wavemark -- the homepage card-grid
   split has no sechead and is correctly flush. No :has() support just keeps the old flush. */
.mediasplit--sticky .mediasplit__body:has(> .sechead > .wavemark) + .mediasplit__media,
.mediasplit--fill .mediasplit__body:has(> .sechead > .wavemark) + .mediasplit__media{margin-top:71px}
@media (max-width:880px){
  .mediasplit{grid-template-columns:1fr;gap:24px}
  /* stacked: the image follows the text, so the offset is just a double gap */
  .mediasplit--sticky .mediasplit__body:has(> .sechead > .wavemark) + .mediasplit__media,
  .mediasplit--top .mediasplit__body:has(> .sechead > .wavemark) + .mediasplit__media,
  .mediasplit--fill .mediasplit__body:has(> .sechead > .wavemark) + .mediasplit__media{margin-top:0}
  .mapwrap--fill{margin-top:0}
  .mediasplit--flip .mediasplit__media{order:0}
}

/* ── steps — the `01 |` numbered process strip (M7 #4), one image per step */
/* --media is the image variant of the EXISTING .steps component (line ~807, a 3-col
   text-only strip used on /contact/). Declared as a MODIFIER, not a second base rule:
   my first pass redeclared .steps{} later in the sheet and silently repainted contact
   from 3 columns to 4. One component, variants - never two bases with one name. */
.steps--media{grid-template-columns:repeat(4,1fr);gap:20px}
.steps--media.steps--3{grid-template-columns:repeat(3,1fr)}
.steps--media .steps__item{display:flex;flex-direction:column;gap:11px}
.steps--media .steps__item img{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;
  border-radius:var(--r);border:1px solid var(--edge)}
.steps--media .steps__n{font-family:var(--ff-display);font-weight:700;font-size:.8125rem;letter-spacing:.1em;
  color:var(--txt-accent);display:flex;align-items:center;gap:9px}
.steps--media .steps__n::after{content:"";flex:1;height:1px;background:var(--rule)}
.steps--media .steps__item h3{font-size:1.0625rem;margin:0;color:var(--txt-strong)}
.steps--media .steps__item p{margin:0;font-size:.9375rem;line-height:1.6;color:var(--txt)}
@media (max-width:900px){.steps--media{grid-template-columns:repeat(2,1fr)}}
/* This rule only ever matched (0,1,0). `.steps--media.steps--3` above is (0,2,0) and a
   media query adds NO specificity, so a 3-step rail never collapsed - it held three
   columns all the way down, 96px wide each at 375px. The override has to carry the same
   two classes. Left-aligned once stacked: centred text reads badly at one column width. */
@media (max-width:760px){
  .steps--media,
  .steps--media.steps--3{grid-template-columns:1fr}
  .steps--media.steps--rail .steps__item{align-items:flex-start;text-align:left}
}

/* ── slider — scroll-snap FIRST, so it swipes with no JavaScript at all.
   app.js adds the buttons and dots as enhancement; if it never runs you still
   have a horizontally swipeable, fully readable sequence. No autoplay, ever. */
/* min-width:0 / max-width:100% — a flex-row track has a large min-content, so
   inside a grid or flex column (this lands in a .container.split on the buying
   guide) the column refuses to shrink below it and drags the whole page wider.
   Measured: 462px inside a 430px viewport, 56px of page overflow. */
.slider{position:relative;min-width:0;max-width:100%}
.slider__track{display:flex;min-width:0;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;
  scrollbar-width:none;border-radius:var(--r-sig);border:1px solid var(--edge)}
.slider__track::-webkit-scrollbar{display:none}
.slider__track:focus-visible{outline:3px solid var(--txt-accent);outline-offset:3px}
.slider__slide{flex:0 0 100%;scroll-snap-align:start}
.slider__slide img{width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;display:block}
.slider__cap{padding:15px 20px;font-size:.9375rem;line-height:1.55;color:var(--txt);
  background:var(--surface);border-top:1px solid var(--edge)}
.slider__cap b{color:var(--txt-strong)}
.slider__nav{display:flex;align-items:center;gap:10px;margin-top:15px}
.slider__btn{width:42px;height:42px;flex:none;display:grid;place-items:center;cursor:pointer;
  border-radius:999px;border:1px solid var(--edge);background:var(--surface);color:var(--txt-strong)}
.slider__btn:hover{background:var(--txt-accent);color:var(--btn-on-accent);border-color:transparent}
.slider__btn[disabled]{opacity:.4;cursor:default}
.slider__btn[disabled]:hover{background:var(--surface);color:var(--txt-strong);border-color:var(--edge)}
.slider__count{font-size:.8125rem;font-weight:700;letter-spacing:.08em;color:var(--txt)}
.slider__dots{display:flex;gap:7px;margin-left:auto}
.slider__dot{width:9px;height:9px;padding:0;border:0;border-radius:999px;cursor:pointer;
  background:var(--rule)}
.slider__dot[aria-current="true"]{background:var(--txt-accent);width:26px}

/* ── clickable cards. NOT <a> wrapping the whole card: a .card contains a
   price and a bulleted list, and an anchor around all of it makes the link's
   accessible name the entire list. Instead the HEADING carries the real anchor
   and a ::after overlay stretches it across the card - whole card clickable,
   link name is just the heading, keyboard focus lands on a real <a>.
   .hubcard is already an <a> in the markup, so it only needs the hover. */
.card--link{position:relative}
.card--link .card__link{color:inherit;text-decoration:none}
.card--link .card__link::after{content:"";position:absolute;inset:0;z-index:1}
.card--link:hover{border-color:var(--txt-accent);border-top-color:var(--bar-hover)}
.card--link,a.hubcard{overflow:hidden}
.card--link .card__img,a.hubcard .hubcard__media img{transition:transform .5s ease}
.card--link:hover .card__img,a.hubcard:hover .hubcard__media img{transform:scale(1.045)}
a.hubcard:hover{border-color:var(--txt-accent);border-top-color:var(--bar-hover)}
.card--link:focus-within{outline:3px solid var(--txt-accent);outline-offset:3px}
a.hubcard:focus-visible{outline:3px solid var(--txt-accent);outline-offset:3px}
.card__go{margin-top:auto;padding-top:14px;font-size:.8125rem;font-weight:700;letter-spacing:.08em;
  text-transform:uppercase;color:var(--txt-accent);display:flex;align-items:center;gap:7px;
  transition:gap .2s ease}
.card--link:hover .card__go{gap:12px}

/* ── media hover on the split, matching the cards */
a.mediasplit__media{display:block;overflow:hidden;border-radius:var(--r-sig)}
a.mediasplit__media img{transition:transform .5s ease}
a.mediasplit__media:hover img{transform:scale(1.04)}

/* ── scroll reveal. Gated on `.js` AND no-preference: with JavaScript off, or
   reduced motion on, the hiding rule never applies and content simply renders.
   Fires once - there is no exit animation and nothing re-hides on scroll up. */
@media (prefers-reduced-motion:no-preference){
  .js .reveal{opacity:0;transform:translateY(10px)}
  .js .reveal.is-in{opacity:1;transform:none;
    transition:opacity .55s cubic-bezier(.22,.61,.36,1),transform .55s cubic-bezier(.22,.61,.36,1)}
  /* .statcard carries .reveal, and this rule's transition (0,3,0) outranked the component's
     own (0,1,0) - so the hover changed colour but snapped instead of easing. The reveal
     properties are kept verbatim and the two hover properties appended, at a specificity that
     clears it. .call did not need this because those cards are not reveal elements. */
  .js .statcard.reveal.is-in{transition:opacity .55s cubic-bezier(.22,.61,.36,1),
    transform .55s cubic-bezier(.22,.61,.36,1)}
  .js .reveal--d1.is-in{transition-delay:.07s}
  .js .reveal--d2.is-in{transition-delay:.14s}
  .js .reveal--d3.is-in{transition-delay:.21s}
}

/* .artfig — an inline article figure. The blog post's h2s live inside
   .article__body prose, not in sections with their own sechead, so the media
   split cannot apply there; a post-width figure is the right pattern anyway. */
/* Fills its container. The earlier 920px cap left dead space to the right of the
   figure inside a 1232px section, which is the L-068 complaint again - so the cap
   is gone and the ASPECT carries the height instead: 16/9 gives 1232x693 in a
   section and 860x484 in the blog post's article column. */
.artfig{margin:30px 0}
.artfig img{width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;
  border-radius:var(--r-sig);border:1px solid var(--edge)}
.artfig figcaption{margin-top:10px;font-size:.8125rem;line-height:1.5;color:var(--txt)}

/* ══ ROUND 5a (2026-09-22) — animation tier 2 ═══════════════════════════════
   Staggered grid entrances, a draw-on for the wave mark, count-up on stat
   numbers. All of it inside the no-preference gate and behind `.js`, so the
   page is complete and static without either. Nothing loops. */

/* tabular figures so a counting number does not change width as it climbs */
.statcard b,.statstrip b,.bigstat b,.hero__badge b,.pair__badge b{font-variant-numeric:tabular-nums}

@keyframes wavedraw{to{stroke-dashoffset:0}}

@media (prefers-reduced-motion:no-preference){
  /* ── staggered children. The container reveals, then its children arrive in
     sequence rather than as one block. Delays are on the CHILD, keyed by
     nth-child, so no JavaScript is needed to sequence them. */
  .js .reveal.reveal--stagger > *{opacity:0;transform:translateY(12px)}
  .js .reveal.reveal--stagger.is-in > *{opacity:1;transform:none;
    transition:opacity .5s cubic-bezier(.22,.61,.36,1),
               transform .5s cubic-bezier(.22,.61,.36,1)}
  .js .statcard.reveal.reveal--stagger.is-in > div{
    transition:opacity .5s cubic-bezier(.22,.61,.36,1),
               transform .5s cubic-bezier(.22,.61,.36,1),
               background-color .18s,border-top-color .18s}
  .js .reveal.reveal--stagger.is-in > *:nth-child(2){transition-delay:.07s}
  .js .reveal.reveal--stagger.is-in > *:nth-child(3){transition-delay:.14s}
  .js .reveal.reveal--stagger.is-in > *:nth-child(4){transition-delay:.21s}
  .js .reveal.reveal--stagger.is-in > *:nth-child(5){transition-delay:.28s}
  .js .reveal.reveal--stagger.is-in > *:nth-child(6){transition-delay:.35s}
  .js .reveal.reveal--stagger.is-in > *:nth-child(n+7){transition-delay:.42s}

  /* ── wave mark draws itself in. dasharray 100 is longer than the path, so at
     offset 100 the stroke sits entirely off the path and at 0 it is fully drawn;
     that holds for any path shorter than 100 units, which all three are. */
  /* Gate the hidden state on .reveal. app.js only ever adds `is-in` to elements
     matching `.reveal, .reveal--stagger` (including its 2500ms safety net), so a
     .sechead WITHOUT .reveal never received it and its wavemark sat at dashoffset
     100 -- fully invisible -- for the life of the page. 25 secheads across 12
     pages were in that state. A non-revealing sechead now renders its wavemark
     statically; the 82 that do reveal still draw themselves in unchanged. */
  .js .sechead.reveal .wavemark path{stroke-dasharray:100;stroke-dashoffset:100}
  .js .sechead.reveal.is-in .wavemark path{animation:wavedraw .85s ease forwards}
  .js .sechead.reveal.is-in .wavemark path:nth-of-type(2){animation-delay:.11s}
  .js .sechead.reveal.is-in .wavemark path:nth-of-type(3){animation-delay:.22s}
}

/* A grid inside a .mediasplit__body only has HALF the container, so it must
   collapse to a single column. Left at 3 columns it produced three 85px-wide
   cards in a staircase - the defect in the round-5 screenshot. These are the
   grid classes that actually occur inside a split body on this build. */
.mediasplit__body .stagger,
.mediasplit__body .calls,
.mediasplit__body .brands,
.mediasplit__body .contrast,
.mediasplit__body .coverage,
.mediasplit__body .vs,
.mediasplit__body .psteps,
.mediasplit__body .claims,
.mediasplit__body .cards,
.mediasplit__body .nexttiles,
.mediasplit__body .guidelist{grid-template-columns:1fr}
/* .stagger offsets each card down the row; stacked in one column that offset
   becomes dead space, so it is reset. */
.mediasplit__body .stagger > *{margin-top:0}

/* --fill: the media column stretches to the height of the body beside it rather
   than sitting at a fixed ratio and leaving a gap under it. */
/* The claims list runs far taller than a 16:10 image, which left a few hundred px of
   empty column. Sticky keeps the photo beside the list instead of stranded at the top.
   top:96px clears .header.is-stuck. Browsers without sticky just pin it at the top,
   which is exactly the old behaviour, so nothing needs a fallback. */
/* sticky needs a tall grid area to travel in, so this one stays top-aligned */
.mediasplit--sticky{align-items:start}
.mediasplit--sticky .mediasplit__media{position:sticky;top:96px}
@media (max-width:900px){.mediasplit--sticky .mediasplit__media{position:static}}
/* Text taller than the image. Centring then leaves the image floating mid-column with the
   heading stranded above it, so these top-align and cap-align instead. Whether a split
   qualifies depends on how long its copy runs, which CSS cannot test -- hence a markup
   modifier. If a split's copy is cut down so the body becomes the SHORTER column, drop
   this class and it goes back to centring. */
.mediasplit--top{align-items:start}
.mediasplit--top .mediasplit__body:has(> .sechead > .wavemark) + .mediasplit__media{margin-top:71px}
/* Operator-tuned 2026-09-24 for /services/pool-safety-covers/: a wider, shorter panel
   whose top starts level with the body copy instead of on the H2 cap. Deliberately NOT
   combined with --top: that rule is (0,4,0) and a plain (0,2,0) override would lose to
   it, so this carries its own align-items and ONE total offset (71 + 102) rather than
   stacking a second margin on the img. 173px is the two rendered H2 lines plus margin;
   that heading measures two lines at every width from 901 to 1600, and the offset is
   dropped below 900 where the split is a single column. */
.mediasplit--copytop{align-items:start}
.mediasplit--copytop .mediasplit__media{margin-top:173px}
.mediasplit--copytop .mediasplit__media img{aspect-ratio:16/8.5}
@media (max-width:900px){.mediasplit--copytop .mediasplit__media{margin-top:0}}
.mediasplit--fill{align-items:stretch}
.mediasplit--fill .mediasplit__media{display:flex;flex-direction:column}
.mediasplit--fill .mediasplit__media img{flex:1;aspect-ratio:auto;min-height:340px;object-fit:cover}
/* A GBP map can stand in for the photo in a --fill split. The rule above only reaches
   an <img>; .mapwrap is a positioned div, so it needs the same flex:1, and its iframe is
   pinned to 380px by .mapwrap iframe (0,1,1) and has to be told to fill as well. */
.mediasplit--fill .mediasplit__media > .mapwrap{flex:1;min-height:0}
.mediasplit--fill .mediasplit__media > .mapwrap iframe{height:100%}
/* For a --fill split whose BODY is the shorter column: centre the copy against the
   full-height media, and drop the 71px cap offset, which only means something while the
   copy starts at the top. Must come after the --fill offset rule -- both are (0,5,0), so
   this wins on source order. */
.mediasplit--bodycentre .mediasplit__body{display:flex;flex-direction:column;justify-content:center}
.mediasplit--bodycentre .mediasplit__body:has(> .sechead > .wavemark) + .mediasplit__media{margin-top:0}

/* L-047 again: .hero--inner .hero__grid (0,2,0) outranks .hero__grid--solo (0,1,0), so a
   hero that lost its statcard kept a 2-column grid with an empty right column. The override
   has to match that specificity AND sit after it, which is why it lives down here rather
   than beside the component. */
.hero--inner .hero__grid--solo,.hero .hero__grid--solo{grid-template-columns:1fr}

a.btn.btn--primary{color:var(--white)}
/* the gradient CTA carries a WHITE label (Brand Guide v12 s.4). These four rules all
   predate it, from when this button was a solid aqua fill that needed dark text. */
a.btn.btn--secondary{color:var(--white)}
a.btn.btn--inverted{color:var(--primary)}
/* ghost-dark is the Sunshine fill now, so its label is Deep Indigo (10.7:1). White here
   was 1.51:1 - the same stale override that had the gradient CTA showing dark text. */
a.btn.btn--ghost-dark{color:var(--ink-navy)}
a.btn.btn--ghost-dark:hover{color:var(--ink-navy)}
a.btn.btn--outline{color:var(--white)}
a.btn.btn--outline:hover{color:var(--primary)}
.band-light a.btn.btn--outline,.band-white a.btn.btn--outline{color:var(--primary)}
.band-light a.btn.btn--outline:hover,.band-white a.btn.btn--outline:hover{color:var(--white)}

/* ══════════════════════════════════════════════════════════════════════════════
   .theme-light — LIGHT-DOMINANT VARIANT (operator instruction 2026-09-25)
   Same content, layout and copy as the dark-dominant build; only the tone balance
   changes. hero + cta-band + band-deep stay dark as feature bands; band-navy and
   band-lift flip to White. 33% dark, against 73% in the dark-dominant version.

   SCOPING: every rule below is a DESCENDANT of .theme-light, which lives on <html>.
   A page without that class cannot match any of them, so the 91 placeholder pages
   and the dark-dominant build are untouched by construction, not by convention.

   No colour here is outside the Brand Guide: the grounds are White and Deep Indigo,
   the accents Brand Violet and Sky Blue, and every surface/hairline is one of those
   at alpha.
   ═════════════════════════════════════════════════════════════════════════════ */
/* :not(.band--keepdark): a section marked keep-dark is left OUT of the flip, so it keeps
   every dark-band rule it already has. Promoting a .band-light section to dark instead
   meant all 25 `.band-light X` rules still targeted it, and each component inside had to
   have its dark colours restored by hand - four rounds of whack-a-mole. This costs none. */
/* .band-deep was deliberately left OUT of the flip as a dark "feature band", but on 11
   of the 13 pages it sits directly beneath a .cta-band - which is also dark - so the light
   build stacked two dark blocks. Operator 2026-09-25: flip it too, leave the CTA alone.
   :is(.band-navy,.band-deep) rather than a second selector per rule: :is() takes the
   specificity of its most specific argument, and both are one class, so every (0,3,0) /
   (0,5,0) tie these ~100 mirrors depend on is preserved exactly. Dark build untouched -
   nothing here is outside .theme-light. */
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark),
.theme-light .band-lift:not(.band--keepdark){
  background:var(--white);
  --btn-on-accent:var(--white);
  --txt:var(--ink);
  --txt-strong:var(--ink-strong);
  --txt-accent:var(--primary);
  --rule:rgba(30,30,58,.26);
  --surface:var(--white);
  --surface-hover:var(--tint);
  --bar-hover:var(--primary-hover);
  --edge:var(--hair-dark);
}
/* the hero's divider now meets a white section instead of a dark one */
.theme-light .hero .wavediv{color:var(--white)}
/* a flipped band sitting next to a band that was already light needs the light rule,
   and must NOT keep the white-at-alpha rule meant for dark grounds */
/* Every mirrored rule below has to carry :not(.band--keepdark) too. The token rule
   above excluded the marked section from the FLIP, but these 86 `.band-light X`
   mirrors still repainted it white - so the marker looked applied and rendered as
   if it were not. Narrowing them is mechanical and cannot widen anything. */
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) + :is(.band-navy,.band-deep):not(.band--keepdark),.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) + .band-lift:not(.band--keepdark),
.theme-light .band-lift:not(.band--keepdark) + :is(.band-navy,.band-deep):not(.band--keepdark),.theme-light .band-lift:not(.band--keepdark) + .band-lift:not(.band--keepdark),
.theme-light .band-light + :is(.band-navy,.band-deep):not(.band--keepdark),.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) + .band-light,
.theme-light .band-light + .band-lift:not(.band--keepdark),.theme-light .band-lift:not(.band--keepdark) + .band-light{
  --seamline:var(--hair-dark)}

and
   .theme-light adds a class so they also out-specify. */
.theme-light .band-lift:not(.band--keepdark),
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark){background:var(--wash);color:var(--ink)}.theme-light .band-lift:not(.band--keepdark) + .band-light,
.theme-light .band-lift:not(.band--keepdark) + .band-white,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) + .band-light,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) + .band-white{--seamline:var(--hair-dark)}.theme-light .band-lift:not(.band--keepdark) h1,
.theme-light .band-lift:not(.band--keepdark) h2,
.theme-light .band-lift:not(.band--keepdark) h3,
.theme-light .band-lift:not(.band--keepdark) h4,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) h1,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) h2,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) h3,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) h4{color:var(--ink-strong)}.theme-light .band-lift:not(.band--keepdark) a,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) a{color:var(--link-on-light)}.theme-light .band-lift:not(.band--keepdark),
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark){background:var(--white);color:var(--ink)}.theme-light .band-lift:not(.band--keepdark) h2,
.theme-light .band-lift:not(.band--keepdark) h3,
.theme-light .band-lift:not(.band--keepdark) h4,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) h2,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) h3,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) h4{color:var(--ink-strong)}.theme-light .band-lift:not(.band--keepdark) .eyebrow,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .eyebrow{color:var(--ink-strong)}.theme-light .band-lift:not(.band--keepdark) .wavemark,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .wavemark{color:var(--primary)}.theme-light .band-lift:not(.band--keepdark) .sechead p,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .sechead p{color:var(--ink)}.theme-light .band-lift:not(.band--keepdark) .steps--media.steps--rail .steps__n,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .steps--media.steps--rail .steps__n{background:var(--surface)}.theme-light .band-lift:not(.band--keepdark) .btn--outline,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .btn--outline{color:var(--primary);border-color:var(--primary)}.theme-light .band-lift:not(.band--keepdark) .btn--outline:hover,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .btn--outline:hover{background:var(--primary);color:var(--white);border-color:var(--primary)}.theme-light .band-lift:not(.band--keepdark) .tlink,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .tlink{color:var(--link-on-light)}.theme-light .band-lift:not(.band--keepdark) .claims__row:hover .claims__mark,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .claims__row:hover .claims__mark{border-color:var(--primary-hover);color:var(--primary-hover)}.theme-light .band-lift:not(.band--keepdark) .claims__row:hover h3,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .claims__row:hover h3{color:var(--primary-hover)}.theme-light .band-lift:not(.band--keepdark) .pstep:hover::before,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .pstep:hover::before{color:var(--primary-hover)}.theme-light .band-lift:not(.band--keepdark) .pstep:hover h3,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .pstep:hover h3{color:var(--primary-hover)}.theme-light .band-lift:not(.band--keepdark) .nexttiles a,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .nexttiles a{background:var(--white);
  border-color:var(--hair-dark);border-top-color:var(--primary)}.theme-light .band-lift:not(.band--keepdark) .nexttiles a:hover,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .nexttiles a:hover{background:var(--tint)}.theme-light .band-lift:not(.band--keepdark) .nexttiles b,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .nexttiles b{color:var(--ink-strong)}.theme-light .band-lift:not(.band--keepdark) .nexttiles span,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .nexttiles span{color:var(--ink)}.theme-light .band-lift:not(.band--keepdark),
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark){--btn-on-accent:var(--white);
  --txt:var(--ink);
  --txt-strong:var(--ink-strong);
  --txt-accent:var(--primary);
  --rule:rgba(0,0,0,.26);
  --surface:var(--white);
  --surface-hover:var(--tint);
  /* white would be invisible on a light card, so the light band darkens instead.
     --txt-strong/--txt-accent/--primary all collapse to #4B4DED here, which is the
     REST colour of most of these bars, so --primary-hover is the only value that
     actually reads as a change. Same collapse that bit .claims and .pstep. */
  --bar-hover:var(--primary-hover);
  --edge:var(--hair-dark);}.theme-light .band-lift:not(.band--keepdark) a.btn.btn--outline,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) a.btn.btn--outline{color:var(--primary)}.theme-light .band-lift:not(.band--keepdark) a.btn.btn--outline:hover,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) a.btn.btn--outline:hover{color:var(--white)}.theme-light .band-lift:not(.band--keepdark) + .band-lift:not(.band--keepdark),
.theme-light .band-lift:not(.band--keepdark) + :is(.band-navy,.band-deep):not(.band--keepdark),
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) + .band-lift:not(.band--keepdark),
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) + :is(.band-navy,.band-deep):not(.band--keepdark){--seamline:var(--hair-dark)}

/* ── .theme-light: DARK ISLANDS ───────────────────────────────────────────────
   Measured, not assumed: of 17 candidates only these three still paint an opaque
   Deep Indigo surface when the band around them flips to White. The other 14 use
   rgba(255,255,255,.03x) or --surface and follow the band, so forcing dark roles
   on them produced white-on-white. They re-assert the dark roles so their text is
   what it is in the dark-dominant build. */
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .revcard,
.theme-light .band-lift:not(.band--keepdark) .revcard,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .quote,
.theme-light .band-lift:not(.band--keepdark) .quote,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .trustline,
.theme-light .band-lift:not(.band--keepdark) .trustline{
  --txt:rgba(255,255,255,.80);
  --txt-strong:var(--white);
  --txt-accent:var(--accent-on-dark);
  --rule:rgba(255,255,255,.22);
  --edge:var(--hair);
}

/* ── .theme-light: corrections to the two blocks above ────────────────────────
   Both problems are self-inflicted and have the same shape: a rule that sets
   `color` directly beats a rule that only sets a TOKEN.

   1. The mirrored `.band-light X` rules were copied onto the flipped bands, but a
      dark island sitting inside a flipped band must not receive them - it put
      Brand Violet on Deep Indigo at 2.75:1. Dark colours are restored here.
   2. .band-light--feature keeps its `band-light` class in the markup, so the
      original `.band-light X` rules still target it even though the theme paints
      it Deep Indigo. Same restoration.
   .mapfall joins the island list: it is a Deep Indigo placeholder behind the map. */
.theme-light .revcard,.theme-light .quote,.theme-light .trustline,.theme-light .mapfall{
  --txt:rgba(255,255,255,.80);
  --txt-strong:var(--white);
  --txt-accent:var(--accent-on-dark);
  --ink:rgba(255,255,255,.80);
  --ink-strong:var(--white);
  --rule:rgba(255,255,255,.22);
  --edge:var(--hair);
}
.theme-light .revcard,.theme-light .revcard p,.theme-light .quote,.theme-light .quote p,
.theme-light .mapfall,.theme-light .mapfall span{color:var(--txt)}
.theme-light .revcard b,.theme-light .quote b,.theme-light .mapfall b{color:var(--txt-strong)}
.theme-light .revcard__score,.theme-light .revcard__score b,.theme-light .revcard__stars{color:var(--txt-accent)}
.theme-light .revcard .btn--outline{
  color:var(--txt-strong);border-color:rgba(255,255,255,.55)}
.theme-light .revcard .btn--outline:hover{
  background:var(--white);color:var(--primary);border-color:var(--white)}

/* The island colour restorations must match the specificity of the MIRRORED rules they
   are correcting - those carry a band class, so (0,3,0) beats the (0,2,0) island rule. */
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .revcard,.theme-light .band-lift:not(.band--keepdark) .revcard,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .quote,.theme-light .band-lift:not(.band--keepdark) .quote{color:var(--txt)}
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .revcard b,.theme-light .band-lift:not(.band--keepdark) .revcard b,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .revcard__score,.theme-light .band-lift:not(.band--keepdark) .revcard__score,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .revcard__stars,.theme-light .band-lift:not(.band--keepdark) .revcard__stars{
  color:var(--accent-on-dark)}
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .revcard__meta,.theme-light .band-lift:not(.band--keepdark) .revcard__meta,
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .revcard p,.theme-light .band-lift:not(.band--keepdark) .revcard p{color:var(--txt)}
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .revcard .btn--outline,.theme-light .band-lift:not(.band--keepdark) .revcard .btn--outline{
  color:var(--white);border-color:rgba(255,255,255,.55)}
/* ...and the HOVER has to be restored at the same level. That rest rule is (0,5,0) and
   was out-ranking both `.revcard:hover .revcard__go` (0,3,0) and
   `.theme-light .revcard .btn--outline:hover` (0,4,0), so in the light build the card's
   hover filled the button white and the label stayed white: invisible. Six classes
   (:hover counts as one) is what it takes. Violet on white is 5.86:1. */
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .revcard:hover .btn--outline,
.theme-light .band-lift:not(.band--keepdark) .revcard:hover .btn--outline{
  background:var(--white);color:var(--primary);border-color:var(--white)}


/* ===================== PREVIEW THEME TOGGLE (r9) ==========================
   One bundle, one URL, both directions. The class goes on <html> so the
   .theme-light layer above applies; a boot script in <head> re-applies the
   stored choice BEFORE first paint, so there is no flash of the wrong theme.
   Only the 13 designed pages carry that boot script - which is the whole
   containment story: a stored 'light' is unreadable to the other 91 pages.

   Fixed, and deliberately NOT in the site header. This is preview chrome, like
   .pvbar: a theme switch inside the client-facing header would read as a
   feature of the finished site. Fixed also keeps it reachable mid-page, which
   is the point when you are comparing two treatments of one section.

   Colours are guide-only and stated outright rather than inherited, because
   the control sits outside every band and has no --txt/--surface to read.
   White on Deep Indigo and Deep Indigo on White are both 16.1:1. */
.themetog{
  /* bottom-RIGHT: at bottom-left the pill landed on the first characters of the
     hero disclaimer, which is the worst place to occlude - that is where the eye
     starts each line. On the right it overlaps imagery and trailing whitespace. */
  position:fixed;right:18px;bottom:18px;z-index:9998;
  display:inline-flex;align-items:center;gap:8px;min-height:40px;padding:0 15px;
  border-radius:999px;cursor:pointer;
  font-family:var(--ff);font-size:.6875rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  background:var(--navy-deep);color:var(--white);border:2px solid var(--primary);
  box-shadow:0 8px 24px rgba(30,30,58,.34);
  transition:background-color .2s,color .2s,border-color .2s,box-shadow .2s
}
.themetog:hover{border-color:var(--primary-hover);box-shadow:0 8px 24px rgba(30,30,58,.34),0 0 0 3px rgba(118,102,248,.35)}
.themetog:focus-visible{outline:3px solid var(--sunshine);outline-offset:2px}
.themetog span{display:inline-flex;align-items:center;gap:8px}
.themetog svg{width:15px;height:15px;flex:none}
.theme-light .themetog{background:var(--white);color:var(--ink-navy);box-shadow:0 8px 24px rgba(30,30,58,.20)}
/* The label swap is CSS, not JS - nothing to keep in sync. These carry the parent
   class on purpose: `.themetog span` is (0,1,1) and a bare `.themetog__d` is (0,1,0),
   so the layout rule would win the tie and BOTH labels would render. */
.themetog .themetog__d{display:none}
.theme-light .themetog .themetog__l{display:none}
.theme-light .themetog .themetog__d{display:inline-flex}
/* the sticky mobile CTA bar owns the bottom edge below 769px - sit above it */
@media (max-width:768px){
  .themetog{right:12px;bottom:80px;padding:0 12px;font-size:.625rem;letter-spacing:.07em;min-height:36px}
}
/* ---- the dark version, hidden (2026-09-29) --------------------------------
   The client signed off on the light version, so the toggle that offers the dark
   one is hidden. Nothing dark is deleted: the whole dark build is still the
   default layer of this sheet, and .theme-light is still the flip on top of it.

   .theme-locked is written statically onto <html> on the 13 designed pages. Drop
   it there and the dark version is back - the boot script resumes reading the
   stored choice, the toggle returns, and this rule goes inert by itself. There
   is nothing here to undo.

   (0,2,0) against the .themetog layout rule's (0,1,0), so source order is not
   load-bearing - the same tie that already forced .theme-light .themetog .themetog__d
   to carry its parent class a few lines above. */
.theme-locked .themetog{display:none}


/* ===================== CONTACT: THE FORM, AS A SPLIT (2026-09-25) =========
   Operator: "Form is too large ... form on the left, image on the right."
   Reuses .mediasplit--fill, so the photo stretches to the form's height rather
   than sitting beside it as a fixed 16:10 panel.

   NOT 1fr 1fr. Measured against the live GHL embed: the form keeps its paired
   fields (Name/Email, Phone/Address, City/ZIP) down to about 650px and stacks
   them below that, which takes it from 804px tall to 1111px. An even split gives
   a 587px column - stacked, and a 1268px card. 1.55fr leaves 670px, so the form
   stays two-column and the card comes in at 961px: narrower AND 307px shorter,
   which is what "too large" actually meant. */
.mediasplit--form{grid-template-columns:minmax(0,1.55fr) minmax(0,1fr)}
.mediasplit--form .formcard{height:100%}
/* ---- cropping the embed's own chrome -------------------------------------
   Measured off the rendered iframe, not assumed: 81px of side padding, 104px
   above the first field (the wave graphic sits at 4-60), 149px below Send.

   Vertical padding is a fixed pixel value, so a negative margin inside an
   overflow:hidden card genuinely removes it. The SIDES cannot be cropped: the
   form is a centred block capped at 508px, so widening the iframe widens its
   padding by exactly the same amount - measured at +0/+120/+200px, padding went
   81 -> 141 -> 181 and the fields never moved. Visible gutter is always
   (card width - 508)/2, and holding the fields two-across needs >=650px of
   iframe, i.e. >=71px a side. The only side saving available is our own 22px,
   so the panel now bleeds to the card edge.

   The crop leaves 30px above the first field and 43px below Send. It assumes the
   post-submit state keeps the same template padding; if GHL renders a short
   thank-you instead, 110px off the bottom could bite - that cannot be tested
   without sending the client a real lead. */
/* Measured, not eyeballed. Inside the embed: a wave graphic from 4 to ~72px, the
   first label at 80px, and 154px of nothing below the Send pill (its own bottom
   waves live in there too).

   Operator picked the variant that keeps the panel's OWN breathing room rather than
   buying it back with card padding: no top crop at all, so the wave graphic keeps the
   4px of clearance it was drawn with and 80px sits above the first label; -96 leaves
   58px under the button and takes the bottom waves. Card padding stays at its default
   22, so the visible gaps are 102px and 80px. (-8px was tried and did shave the top
   arc flush to the panel edge - measured, the wave ink started exactly at it.)

   An earlier pass cropped -76/-140 and made up the difference with card padding -
   it measured correctly and still read as unchanged, because white frame is not the
   same thing as room inside the panel.

   The clip box needs its OWN overflow:hidden. Hung on the card instead, the negative
   margin only pulled the consent note upward and it painted over live form area
   rather than the form being clipped. */
.mediasplit--form .embedclip{overflow:hidden}
.mediasplit--form .embedclip .ghl{margin-bottom:-96px}
/* On a flipped band the white card sits on white with nothing but its 3px top rule.
   Three sides, never the `border` shorthand: that repaints all four and wipes the
   accent - the trap already logged on .step / .scard / .call / a.hubcard. */
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .formcard,
.theme-light .band-lift:not(.band--keepdark) .formcard{
  border-right:1px solid var(--hair-dark);
  border-bottom:1px solid var(--hair-dark);
  border-left:1px solid var(--hair-dark)
}
@media (max-width:900px){
  /* one column again: the photo has no tall neighbour to match, so it goes back to
     a landscape panel instead of a 1200px-tall strip */
  .mediasplit--form{grid-template-columns:1fr}
  .mediasplit--form .mediasplit__media img{min-height:0;aspect-ratio:16/10}
}


/* ---- .reach--2: the two-box NAP pair on /twinsburg-oh/ (2026-09-25) -------
   Two boxes instead of three, centred under the map split rather than run to the
   full container - "side-by-side, centered at the bottom".

   The right-hand box carries TWO links, and that is the trap: `.reach a` is
   (0,1,1) and matches ANY descendant anchor, not just a box-level one, so the
   phone and email would each have rendered as their own bordered, padded card
   inside the card. `.reach--2 div a` is (0,1,2) - same class count, one more
   element - so it wins the tie and strips the box styling back off. */
.reach--2{grid-template-columns:repeat(2,1fr);max-width:880px;margin-inline:auto}
/* No box-level hover here. On /contact/ each .reach box IS the link, so lighting the
   whole card is the right feedback; these two are plain <div>s - the left one is not
   interactive at all - and pointing at the phone link lit the entire card and flipped
   its 3px bar to --bar-hover, which is #fff on a dark band. Pinning both properties to
   their rest values kills it; the links keep their underline. (0,3,1) beats the
   component's own (0,2,1) `.reach div:hover`. */
.reach.reach--2 > div:hover{background:var(--surface);border-top-color:var(--aqua)}
/* ...and the feedback lives on the icon and the bold line instead (operator request).
   Both move together, with the panel and the bar still pinned. */
.reach--2 b,.reach--2 .reach__ic{transition:color .18s,background-color .18s,border-color .18s}
.reach.reach--2 > div:hover b{color:var(--txt-accent)}
.reach.reach--2 > div:hover .reach__ic{color:var(--white);
  background:rgba(40,145,243,.28);border-color:rgba(40,145,243,.75)}
/* On a flipped band --txt-accent, --txt-strong and --primary all collapse to #4B4DED,
   which is ALREADY the rest colour here, so the rule above would move nothing in the
   light build. Violet Glow was the obvious substitute and it MEASURED at 4.18:1 on
   white - the label is 17px/700, under the 18.66px-bold threshold, so it needs 4.5 and
   fails. Deep Indigo is 16.1:1 and darkening on hover reads correctly on a light card;
   the circle still lifts with Violet Glow at alpha, which only has a 3:1 floor. */
.theme-light .band-navy .reach.reach--2 > div:hover b,
.theme-light .band-lift .reach.reach--2 > div:hover b{color:var(--ink-navy)}
.theme-light .band-navy .reach.reach--2 > div:hover .reach__ic,
.theme-light .band-lift .reach.reach--2 > div:hover .reach__ic{color:var(--ink-navy);
  background:rgba(118,102,248,.16);border-color:rgba(118,102,248,.55)}
/* color:inherit, NOT --txt-accent. As accent, the phone and email were the only
   coloured text in the pair and the two boxes read as different components - the
   left one white-on-dark, the right one blue-on-dark. Inheriting means the <a> in
   the <b> takes --txt-strong and the one in the <span> takes --txt, so both boxes
   match line for line. The hover underline carries the affordance instead, which is
   also how the whole-box links on /contact/ behave. */
.reach--2 div a{display:inline;border:0;padding:0;border-radius:0;background:none;
  color:inherit;text-decoration:none}
/* `.reach a:hover` is a DESCENDANT selector, so it paints the inner <a> as well as
   the box - a tinted panel behind the phone number itself. The rest-state reset above
   is (0,1,2) and loses to it; adding the same properties to this (0,2,2) hover rule is
   what actually clears it. Underline only. */
.reach--2 div a:hover{text-decoration:underline;background:none;border-top-color:transparent}
/* The light theme's mirrored `.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) a` rule is
   (0,3,1) and repainted the email link --link-on-light, so in that build the address
   box read ink and the contact box read violet - the same mismatch, mirrored. Three
   classes plus two elements is (0,3,2), which is the smallest selector that wins. */
.theme-light .band-navy .reach--2 div a,
.theme-light .band-lift .reach--2 div a{color:inherit}
@media (max-width:640px){ .reach--2{grid-template-columns:1fr} }


/* The slot's dashed edge and its diagonal hatching are both white-at-alpha, so on a
   flipped band they disappeared along with the title - the placeholder read as floating
   text with no frame. Deep Indigo at the same alphas on white. NOT caught by the
   invisible-text sweep: that walker gives up when an ancestor has a background-image,
   and the hatching is exactly that. */
.theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .slot,
.theme-light .band-lift:not(.band--keepdark) .slot{
  border-color:rgba(30,30,58,.30);
  background:repeating-linear-gradient(45deg,rgba(30,30,58,.045) 0 12px,transparent 12px 24px),
    rgba(30,30,58,.035)}


/* The Brand Violet tint that used to sit here is gone. Every designed page now
   drives its light-build grounds off .band--tint - Sky Blue, and placed by the
   white/tint alternation rather than by which sections happened to carry
   .band-light--bg. No page uses .band-light--bg any more. */


/* The wave divider between sections was tried here on 2026-09-26 and removed the
   same day - at 18px between every section it read as a repeated motif rather
   than a transition. Sections fall back to the container-width 1px seam.
   The hero keeps its own full-size .wavediv; that was never part of this. */

/* Flat tint, no pattern (operator 2026-09-26: the ripple read as geometric
   wallpaper). Sky Blue at 7% over Brand Guide White, laid down as a gradient pair
   over a solid base because <body> is still Deep Indigo in the light build and a
   translucent section background would let that through.
   .band--tint, not .band-light--bg: the marker has to be light-theme-only,
   because half these sections are DARK bands in the dark build and
   .band-light--bg would paint them white there too.
   .pg-home only: the other 12 pages keep the Brand Violet tint until this
   is approved. */
/* :not(.band--keepdark) is not decoration - it is there for SPECIFICITY. The light
   flip rules are `.theme-light :is(.band-navy,.band-deep):not(.band--keepdark)` and
   `.theme-light .band-lift:not(.band--keepdark)`, both (0,3,0). A bare
   `.theme-light .band--tint` is (0,2,0) and quietly LOST on every section that is a
   dark band in the dark build - which is most of them - so the marker was present
   and the tint never painted. Matching their shape ties at (0,3,0) and wins on
   source order, since this block is at the end of the file. */
.theme-light .band--tint:not(.band--keepdark){
  background:linear-gradient(rgba(40,145,243,.07),rgba(40,145,243,.07)),var(--white)}


/* The hero disclaimer runs to a second line at 96ch and fits on one at 131ch
   (operator, measured in DevTools). Light build only, as asked - the dark build
   keeps 96ch. (0,2,0) beats the base rule's (0,1,0). */
.theme-light .herotrust__note{max-width:131ch}


/* ---- "Repair or Replace": close the two holes --------------------------
   The section was sechead (capped at 62ch, so ~665px of empty container beside
   it) stacked above a 2-column .diag whose left column ran 173px shorter than
   the right. Two blank rectangles, top-right and bottom-left.

   Reflowed without touching the markup (operator asked for it in BOTH builds
   on 2026-09-26, having first taken it light-only):
   .diag becomes display:contents and the CONTAINER becomes the grid, which lets
   the sechead sit in column 1 above the copy while the numbered rows span both
   rows of column 2 - starting level with the heading instead of below it. The
   rows then stretch and space-between, so whichever column is shorter absorbs
   the difference instead of leaving a gap.

   :has() is already load-bearing in this sheet (.mediasplit--fill uses it), and
   .diag exists on exactly one section of one page. */
/* .9fr, not the original .82fr: once the heading moved into column 1 it had a
   482px measure and broke across three lines. 529px puts it back to two, and the
   right column stays the dominant one. */
.container:has(> .diag){
  display:grid;grid-template-columns:.9fr 1.1fr;column-gap:56px;align-items:start}
.container:has(> .diag) > .sechead{
  grid-column:1;grid-row:1;max-width:none;margin-bottom:26px;
  /* the sechead carries an inline margin-top:34px, which pushed the heading down
     while the rows column started at the grid's top edge - !important is the only
     thing that outranks an inline style, and both columns have to start level */
  margin-top:0!important}
.container:has(> .diag) > .diag{display:contents}
.container:has(> .diag) > .diag > div:first-child{grid-column:1;grid-row:2}
.container:has(> .diag) > .diag > .drows{
  grid-column:2;grid-row:1 / span 2;align-self:stretch;align-content:space-between}
@media (max-width:900px){
  .container:has(> .diag){grid-template-columns:1fr}
  .container:has(> .diag) > .sechead,
  .container:has(> .diag) > .diag > div:first-child,
  .container:has(> .diag) > .diag > .drows{grid-column:1;grid-row:auto}
}


/* The "theirs" card's 3px top bar is white-at-.28 - invisible on a flipped band,
   which left that card with no edge of any kind while its "ours" twin still read
   (its Sky Blue tint and bar survive on white). Deep Indigo at the same alpha. */
.theme-light .contrast__col--theirs{border-top-color:rgba(30,30,58,.30)}
/* ...and that rest rule is (0,2,0), the same as .contrast__col:hover, so on source
   order it was winning during hover too and the bar never moved in the light build.
   (0,3,0) hands the hover back. */
.theme-light .contrast__col--theirs:hover{border-top-color:var(--bar-hover)}


/* Reviews, /about/: drop the score column so it sits against the body copy rather
   than riding high beside the heading (operator, set in DevTools and confirmed for
   both builds). It keeps its inline align-self:center, and a top margin on a centred
   grid item shifts the box by half the margin, so 100px moves it ~50px down.
   Put here rather than inline so it can be zeroed where .split collapses, at 900px -
   stacked, a 100px gap above the figure would just be a hole. .bigstat exists once
   in the whole build, on this page. */
.split--wide-l > div:has(> .bigstat){margin-top:100px}
@media (max-width:900px){ .split--wide-l > div:has(> .bigstat){margin-top:0} }


/* ---- a seam is only earned when both sides share a ground -------------
   Operator 2026-09-26: the dividers between a white section and a tinted one are
   redundant - the colour change already separates them - while the one between two
   whites is doing real work. Audited: the light build drew 84 seams, only 13 of them
   between matching grounds; the dark build drew 36, all 36 matching (so nothing there
   changes, and these rules are .theme-light scoped anyway).

   Painted on the ::after directly rather than by clearing --seamline: the adjacency
   rules that set that token run as high as (0,5,0), and matching them would mean
   restating every one. Targeting the pseudo-element's background only has to beat
   `.section::after` at (0,1,1), which (0,3,0) does comfortably. */
.theme-light .band--tint + :not(.band--tint)::after,
.theme-light :not(.band--tint) + .band--tint::after,
.theme-light .band--keepdark + :not(.band--keepdark)::after,
.theme-light :not(.band--keepdark) + .band--keepdark::after{background:transparent}


/* ---- one review-card height everywhere ---------------------------------
   Operator 2026-09-26: the rating box must be the same height on every page as it
   is on /contact/. It was `.revcard{flex:1}`, so the card absorbed whatever slack
   the column had after the quote - which made its height a function of how long
   that page's testimonial happens to be: 178 on /contact/, 216 on the homepage,
   244 on /about/, 328 on /resources/.

   Inverted: the card is content-sized and the QUOTE takes the slack, so the card
   is 178 on all of them and the column still fills its media. Both builds. */
.trust__col .revcard{flex:0 0 auto}
.trust__col > .quote{flex:0 0 auto}

/* ---- the PHOTO follows the reviews, not the other way round ------------
   Operator 2026-09-26: keep /contact/'s internal spacing everywhere and let the
   image shrink to the review column instead of the column stretching to the image.

   .trustmedia img was a fixed 4:3, which gave the media column a definite height
   (516px on most pages) and made IT the tallest thing in the row - so a short
   testimonial left the slack inside the review boxes. Taking the image out of flow
   leaves the media column with no intrinsic height, so the row is sized by
   .trust__col - quote at its natural height + 30px + the 178px card - and the photo
   covers whatever that comes to.

   .trustmedia--short's 420px cap existed to stop the image being the tallest thing;
   that is now structural, so the cap is released or it would hold the photo 3px
   short of the row on /contact/. Below 900px the grid is one column and there is no
   sibling to measure against, so the image goes back to its own 4:3. */
.trust__grid > .trustmedia{align-self:stretch;min-height:0}
.trust__grid > .trustmedia--short{max-height:none}
.trust__grid > .trustmedia > img{position:absolute;inset:0;width:100%;height:100%;
  aspect-ratio:auto;object-fit:cover}
@media (max-width:900px){
  .trust__grid > .trustmedia > img{position:static;width:100%;height:auto;aspect-ratio:4/3}
}

/* ============ DECISION CARDS (buying guide, Decisions One & Two) =========
   Operator layout reference 2026-09-27. Two parallel decisions, each as a
   wave-capped card: eyebrow + heading, then one block per option, then the
   "which one do you actually want" note pinned to the card's bottom edge.

   Palette note: the reference tinted one option block GREEN. Green is not in the
   guide's six, so the two option blocks are Deep Indigo at 5% (the neutral /
   not-the-one option) and Sky Blue at 9% (the water-relevant one). That keeps the
   reference's "one tinted, one plain" rhythm inside the palette.

   Its own grid, not .split — .split is a TEXT pair and centres its columns, which
   is what threw Decisions One/Two out of alignment in the first place. Cards
   stretch to equal height instead, so the caps and the notes line up by
   construction rather than by a modifier. */
.dgrid{display:grid;grid-template-columns:1fr 1fr;gap:34px;align-items:stretch}
@media (max-width:900px){.dgrid{grid-template-columns:1fr;gap:26px}}

.dcard{
  /* the two tint levels are declared HERE, per band, so the card cannot be wrong
     about its ground. This section is band-light in BOTH builds today; the dark
     override is what keeps that true if it is ever re-banded. */
  --dt-neutral:rgba(30,30,58,.05);
  --dt-water:rgba(40,145,243,.09);
  --dt-note:rgba(75,77,237,.07);
  --dt-strong:var(--navy-deep);
  --dt-edge:rgba(40,145,243,.34);
  display:flex;flex-direction:column;
  background:var(--surface);border:1px solid var(--edge);
  border-radius:var(--r-sig);overflow:hidden;
  box-shadow:0 10px 30px rgba(30,30,58,.07)
}
.band-navy .dcard,.band-lift .dcard,.band-deep .dcard{
  --dt-neutral:rgba(255,255,255,.06);
  --dt-water:rgba(40,145,243,.16);
  --dt-note:rgba(118,102,248,.16);
  --dt-strong:var(--white);
  --dt-edge:rgba(40,145,243,.42);
  box-shadow:none
}
/* Sky Blue band with the hero's own three-layer wave as its lower edge, painted in
   the card's surface colour exactly as .wavediv does. No new wave was drawn. */
.dcard__cap{position:relative;height:44px;background:var(--aqua);color:var(--surface);flex:none}
.dcard__cap svg{position:absolute;left:0;right:0;bottom:-1px;width:100%;height:36px;display:block}
.dcard__in{display:flex;flex-direction:column;flex:1 1 auto;padding:22px 24px 24px;gap:14px}

/* The card's title block IS .sechead - the same one Decisions Three..Six use, so the
   wavemark size, the stacked order and the heading scale come from one definition and
   the six decisions cannot drift apart. Only the outer spacing is the card's own:
   .sechead's own 44px bottom margin would double up with .dcard__in's flex gap, and
   its 62ch measure is wider than the card. */
.dcard__in > .sechead{margin-bottom:0;max-width:none}

/* one block per option */
.dopt{display:grid;grid-template-columns:auto 1fr;gap:14px;align-items:start;
  padding:16px 18px;border-radius:var(--r);background:var(--dt-neutral)}
.dopt--water{background:var(--dt-water)}
.dopt--plain{background:transparent;border:1px solid var(--dt-edge)}
.dopt svg{flex:none;width:18px;height:18px;color:var(--txt-accent);margin-top:2px}
.dopt p{margin:0;font-size:.9375rem;line-height:1.62;color:var(--txt)}
.dopt strong{color:var(--dt-strong)}

/* the verdict, held to the bottom edge so both cards' notes sit on one line */
.dnote{margin:auto 0 0;padding:16px 18px;background:var(--dt-note);
  border-left:4px solid var(--primary);border-radius:0 var(--r) var(--r) 0;
  font-size:.9375rem;line-height:1.62;color:var(--txt)}
.dnote strong{color:var(--txt-strong)}
.dnote a{color:var(--txt-accent);font-weight:700}
@media (max-width:520px){
  .dcard__in{padding:20px 18px 20px}
  .dopt{padding:14px 15px;gap:12px}
}

/* ===== DECISIONS FIVE & SIX (buying guide) ==============================
   Operator layout reference 2026-09-27. Text styling is deliberately untouched:
   the paragraphs keep their own inline --txt/--txt-accent colours and the
   .sechead title block is unchanged. Only new LAYOUT objects are added.

   These sit on .band-navy, which is Deep Indigo in the dark build and flips to
   white in the light one, so each tint is declared twice in the house pattern:
   the base rule is the dark build, the .theme-light mirror is the light one. */

/* the season contrast strip - August on one side, October on the other */
.seasons{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:16px;
  margin:22px 0;padding:16px 20px;border-radius:var(--r);background:rgba(40,145,243,.16)}
.theme-light .band-navy:not(.band--keepdark) .seasons{background:rgba(40,145,243,.09)}
.seasons__m{display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center}
.seasons__m svg{width:22px;height:22px;color:var(--txt-accent)}
.seasons__m span{font-size:.875rem;font-weight:700;color:var(--txt-strong)}
.seasons__a{color:var(--txt-accent)}
.seasons__a svg{display:block;width:84px;height:20px}
@media (max-width:420px){.seasons__a svg{width:54px}}

/* the priced callout. .callout paints a translucent white fill and an aqua left bar,
   which is a card ON a dark band; this one has to BE the dark block on either ground,
   so it declares the dark roles itself. border (shorthand) also clears .callout's
   left bar - it must stay after .callout in the sheet, which appending guarantees. */
.callout--deep{
  margin-top:22px;background:var(--navy-deep);border:1px solid var(--hair);
  --txt:rgba(255,255,255,.80);
  --txt-strong:var(--white);
  --txt-accent:var(--accent-on-dark)
}
/* on the flipped band the card is dark on white, so it needs no edge of its own */
.theme-light .band-navy:not(.band--keepdark) .callout--deep{border-color:transparent}
.callout--deep b{color:var(--sunshine);flex:none}     /* 10.67:1 on Deep Indigo */
/* .callout is a wrapping flex row and .callout p has no basis, so the paragraph's
   max-content width pushed it onto its own line and the figure sat above the copy
   instead of beside it. A basis narrow enough to share the column fixes the order. */
.callout--deep p{color:var(--txt);flex:1 1 230px;min-width:0}
.callout--deep strong{color:var(--txt-accent)}
.callout--deep a.tlink{color:var(--txt-accent)}
/* .callout--deep is a DARK island on a band that the light build flips to white, so the
   band's light-build mirrors reach inside it and are wrong there. The .tlink mirror
   is .theme-light :is(.band-navy,.band-deep):not(.band--keepdark) .tlink = (0,4,0);
   this is (0,5,0). Measured: --link-on-light on Deep Indigo is 2.75:1, Sky Blue 4.94:1. */
.theme-light :is(.band-navy,.band-deep,.band-lift):not(.band--keepdark) .callout--deep .tlink{
  color:var(--accent-on-dark)}

/* the six things that move the quote, as a numbered grid rather than a prose run */
.qgrid{counter-reset:q;list-style:none;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:12px;margin:14px 0 18px;padding:0}
.qgrid li{display:grid;grid-template-columns:auto 1fr;gap:12px;align-items:start;
  padding:14px 16px;border-radius:var(--r);background:rgba(40,145,243,.16);
  font-size:.9375rem;line-height:1.5;color:var(--txt)}
.theme-light .band-navy:not(.band--keepdark) .qgrid li{background:rgba(40,145,243,.09)}
.qgrid li::before{
  counter-increment:q;content:counter(q);
  display:grid;place-items:center;width:24px;height:24px;border-radius:50%;
  background:var(--primary);color:var(--white);   /* 5.86:1 */
  font-size:.8125rem;font-weight:700;line-height:1
}
@media (max-width:900px){.qgrid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:560px){.qgrid{grid-template-columns:1fr}}

/* ===== THE DECISION RUN (buying guide) =================================
   Operator layout reference v3, 2026-09-27. The six decisions stop being four
   separate sections and become one numbered run: a dashed rule down the left
   with a numbered disc at each decision, and each decision's content in a single
   full-width column beside it.

   Text styling untouched - every decision keeps the .sechead block (wavemark,
   eyebrow, heading) exactly as it is, and the paragraphs keep their own inline
   colour styles. Only the numerals move: they leave the headings, because the
   disc on the rail states them. */
.decrun{counter-reset:dec;position:relative;padding-left:62px}
.decrun::before{content:"";position:absolute;left:18px;top:12px;bottom:12px;
  border-left:2px dashed var(--rule)}
.dec{position:relative;padding-bottom:52px}
.dec:last-child{padding-bottom:0}
.dec::before{
  counter-increment:dec;content:counter(dec);
  position:absolute;left:-62px;top:-3px;width:38px;height:38px;border-radius:50%;
  display:grid;place-items:center;
  background:var(--primary);color:var(--white);       /* 5.86:1 */
  font-family:var(--ff);font-size:.9375rem;font-weight:700;line-height:1;
  /* the disc is opaque and spans the rule.s x, so it masks the line by itself; the halo only makes it read as a stop rather than a dot */
  box-shadow:0 0 0 9px rgba(75,77,237,.13)
}
/* the run is wider than .sechead's 62ch measure, and at 62ch Decision Three's
   heading broke over three lines. Released to the column; text-wrap:balance (set
   on every h1-h4) then splits it evenly over two. */
.dec > .sechead{margin-bottom:16px;max-width:none}

/* the two decisions that carry a photo: copy left, photo right */
.decsplit{display:grid;grid-template-columns:minmax(0,1.32fr) minmax(0,.68fr);
  gap:34px;align-items:stretch}
.decsplit .decgrid{grid-template-columns:1fr;gap:16px}
/* The photo column matches the copy column's height. The grid stretch gives .decmedia
   a definite height; the frame takes whatever the caption leaves and the image is
   absolutely positioned inside it, so height:100% has something to resolve against -
   on an img that is a flex item, flex-basis would win over height and it would not.
   Same pattern as the review block's photo. */
.decsplit .decmedia{display:flex;flex-direction:column;height:100%;margin-top:5px}
.decsplit .decmedia__frame{flex:1 1 auto;min-height:190px;position:relative;
  overflow:hidden;border-radius:14px}
.decsplit .decmedia__frame img{position:absolute;inset:0;
  width:100%;height:100%;object-fit:cover;border-radius:0}
.decsplit .decmedia p{flex:none;margin-top:10px}
@media (max-width:900px){
  .decsplit{grid-template-columns:1fr;gap:22px}
  /* stacked: nothing to match, so the photo goes back to a natural frame */
  .decsplit .decmedia{height:auto}
  .decsplit .decmedia__frame{flex:none;min-height:0;aspect-ratio:16/10}
}

/* a decision's body: two columns of either labelled blocks or plain paragraphs */
.decgrid{display:grid;grid-template-columns:1fr 1fr;gap:20px 30px;align-items:start;margin-bottom:18px}
.decgrid > p{margin:0;font-size:.9375rem;line-height:1.62;color:var(--txt)}
.decblk{border-left:2px solid var(--aqua);padding-left:16px}
.decblk--b{border-left-color:var(--primary)}
.decblk--bare{border-left:0;padding-left:0}
/* No colour here. .decblk p{color:var(--txt)} is (0,2,1) and this is (0,1,0), so the
   accent it used to ask for never applied - the label has always rendered in --txt,
   which is what the reference shows and what measures 16.13:1. Stating it is better
   than leaving a declaration that does nothing. The icon inherits it. */
.declab{display:flex;align-items:center;gap:8px;margin:0 0 8px;
  font-size:.8125rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase}
/* a real mark, not a CSS square - a hollow box reads as a missing glyph */
.declab svg{flex:none;width:15px;height:15px}
.decblk p{margin:0;font-size:.9375rem;line-height:1.62;color:var(--txt)}
.decblk strong,.decgrid > p strong{color:var(--txt-strong)}
/* the line that opens a decision, and the one that closes it */
.declede{margin:0 0 16px;font-size:1rem}
.decsum{margin:0;padding-top:16px;border-top:1px solid var(--edge);
  font-size:.9375rem;line-height:1.62;color:var(--txt)}
.decsum strong{color:var(--txt-strong)}
/* the pulled-out figure */
.decfig{display:flex;align-items:center;gap:26px;flex-wrap:wrap;
  margin-top:18px;padding-top:18px;border-top:1px solid var(--edge)}
.decfig b{flex:none;font-family:var(--ff-display);font-size:2.375rem;font-weight:800;
  line-height:1;letter-spacing:-.02em;color:var(--txt-accent)}
.decfig p{margin:0;flex:1 1 260px;min-width:0;font-size:.9375rem;line-height:1.62;color:var(--txt)}
.decfig strong{color:var(--txt-strong)}
/* Inside .decgrid the figure is not a closing row but a card in a column, so it drops
   the flex row and the top rule and stacks: wave corner, figure, copy.
   The wave is the hero divider's own three paths, painted in the card's surface so the
   tinted corner reads as water rather than as a coloured block. */
/* the card sits in the same column as the photos on Decisions Three and Four, so it
   takes .decsplit's tracks and column gap verbatim - anything else and its left edge
   lands a few pixels off theirs down the right-hand side of the run. */
.decgrid:has(> .decfig){grid-template-columns:minmax(0,1.32fr) minmax(0,.68fr);column-gap:34px}
.decstack > p + p{margin-top:18px;padding-top:18px;border-top:1px solid var(--edge)}
.decgrid > .decfig{
  position:relative;overflow:hidden;display:block;align-self:start;
  margin:0;padding:24px 26px 26px;border:1px solid var(--edge);border-radius:14px;
  background:var(--surface)}
/* the figure and copy sit ABOVE the decoration. .decfig__wave is absolutely
   positioned, so without this it would paint over any glyph it reached. */
.decgrid > .decfig b{position:relative;z-index:1;display:block;margin-bottom:12px;font-size:2.75rem}
.decgrid > .decfig p{position:relative;z-index:1;margin:0}
.decfig__wave{position:absolute;top:0;right:0;z-index:0;width:44%;height:38px;
  background:rgba(40,145,243,.13);color:var(--surface);pointer-events:none}
.decfig__wave svg{position:absolute;left:0;right:0;bottom:-1px;width:100%;height:24px;display:block}
@media (max-width:900px){.decgrid:has(> .decfig){grid-template-columns:1fr}}
/* the two decisions that carry a photo keep it, full width under their content */
.decmedia{margin-top:22px}
.decmedia img{display:block;width:100%;height:auto;border-radius:var(--r-sig)}
.decmedia p{margin:10px 0 0;font-size:.875rem;line-height:1.5;color:var(--txt)}
@media (max-width:780px){
  .decrun{padding-left:44px}
  .decrun::before{left:12px}
  .dec::before{left:-44px;width:30px;height:30px;font-size:.8125rem;
    box-shadow:0 0 0 7px rgba(75,77,237,.13)}
  .decgrid{grid-template-columns:1fr}
}

/* ===== TALLER SLIDER - pool-safety-cover-buying-guide only =============
   The six-step list beside this slider runs 655px while the slider column ran
   466px, so the card floated with a gap under it. A fixed taller ratio only
   balances at one width (1.13:1 at 1440, 0.97:1 at 1100 - portrait), so the
   image takes the slack instead and the two columns stay level at any width.
   Scoped with a modifier: /services/pool-safety-covers/ has a slider too and
   that one is not meant to change. */
.mediasplit:has(.slider--tall){align-items:stretch}
/* 50px, operator 2026-09-27: the stretched column started level with the top of the
   six-step list, which put the image above the first step's text. Dropping it sets the
   image top against step 01 and still lands the nav level with the last step. */
.mediasplit__media:has(> .slider--tall){display:flex;margin-top:50px}
.slider--tall{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
/* .slider__track carries the border and the signature radius, and its overflow-y was
   left to compute - so a flex-sized image could push past the rounded frame. Clip it
   explicitly, and keep overflow-x:auto so the track still snaps and scrolls.
   The slide becomes a grid: the image row takes the slack, so height:100% has a
   definite row to resolve against and no min-height can force it out of the box. */
.slider--tall .slider__track{flex:1 1 auto;min-height:0;overflow-y:hidden}
.slider--tall .slider__slide{display:grid;grid-template-rows:minmax(0,1fr) auto;min-height:0}
.slider--tall .slider__slide img{
  width:100%;height:100%;min-height:0;aspect-ratio:auto;object-fit:cover}
.slider--tall .slider__cap{flex:none}
.slider--tall .slider__nav{flex:none}
@media (max-width:900px){
  .mediasplit:has(.slider--tall){align-items:center}
  /* stacked - there is no column to drop against, so the offset would just be a gap */
  .mediasplit__media:has(> .slider--tall){margin-top:0}
  .slider--tall .slider__slide img{flex:none;min-height:0;aspect-ratio:16/9}
}
