/* ── 🔤 SELF-HOSTED TYPE ───────────────────────────────────────────────────
   🔴 11 Sep 2026. These four demos were the only ones still pulling Bricolage
   Grotesque, Figtree and IBM Plex Mono from fonts.googleapis.com, and it is why
   they were never in sync-demo.mjs: the external-resource guard refuses a demo
   that loads a CDN, so all four were being hand-copied into public/demo instead
   — and then they drifted, serving a phone number the owner had already changed.
   ⚠️ A demo is served in an IFRAME on another domain. A webfont that is slow,
   blocked or privacy-filtered renders the whole page in the fallback face with
   no error anywhere, which is the worst way for a sales tool to fail.
   ⭐ Latin subset only — 10 faces, ~388 KB. The Cyrillic, Greek and Vietnamese
   subsets Google serves are megabytes these pages will never draw a glyph from. */
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url("fonts/bricolage-grotesque-500.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url("fonts/bricolage-grotesque-600.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url("fonts/bricolage-grotesque-700.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Bricolage Grotesque';
  font-style: normal;
  font-weight: 800;
  font-stretch: 100%;
  font-display: swap;
  src: url("fonts/bricolage-grotesque-800.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Figtree';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/figtree-400.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Figtree';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/figtree-500.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Figtree';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/figtree-600.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-400.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-500.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-700.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root{
  /* DARK FIRST. The shop floor at night, lit by a worklight — and the service
     symbols are dashboard telltales, in the colors a real dash uses. */
  --paper:#0A0C0F; --surface:#12161B; --surface-2:#181D24;
  --ink:#EDF1F4; --ink-2:#9AA6B1; --ink-3:#6B7681;
  --rule:#242C34; --rule-soft:#1A1F26;
  --accent:#25C4C9; --accent-ink:#04231F; --accent-soft:#07211F; --heat:#FFB020; --heat-soft:#2A1E06;
  --pass:#3FD07A; --pass-soft:#122419;
  --todo:#E0B860; --todo-soft:#2A2113; --todo-line:#6B5322;
  /* telltale palette — red critical, amber caution, green ok, blue info */
  --tt-red:#FF4438; --tt-amber:#FFB020; --tt-green:#3FD07A;
  --tt-blue:#54A8FF; --tt-cyan:#2BD4D9;
  --shadow:0 1px 2px rgba(0,0,0,.5), 0 14px 40px -18px rgba(0,0,0,.85);
  --f-display:"Bricolage Grotesque","Helvetica Neue",Arial,sans-serif;
  --f-body:"Figtree","Helvetica Neue",Arial,sans-serif;
  --f-mono:"IBM Plex Mono",ui-monospace,"SF Mono",Menlo,monospace;
  --step-0:1.0625rem; --step-1:1.25rem;
  --step-2:clamp(1.5rem,1.15rem + 1.4vw,2rem);
  --step-3:clamp(1.9rem,1.35rem + 2.4vw,2.85rem);
  --step-4:clamp(2.5rem,1.5rem + 4.2vw,4.5rem);
  --rail:12.5rem; --gutter:clamp(1.25rem,4vw,3rem); --measure:64ch;
}
@media (prefers-color-scheme:light){
  :root:not([data-theme="dark"]){
    --paper:#E9EBEE; --surface:#FFFFFF; --surface-2:#F4F5F7;
    --ink:#131A21; --ink-2:#4C5761; --ink-3:#7A848E;
    --rule:#C6CDD4; --rule-soft:#DCE1E6;
    --accent:#0E7C8A; --accent-ink:#FFFFFF; --accent-soft:#DCF0F1; --heat:#9A6B00; --heat-soft:#FBF0DA;
    --pass:#1F7A4D; --pass-soft:#DFF0E7;
    --todo:#8A5A00; --todo-soft:#FBF0DA; --todo-line:#E0B860;
    --tt-red:#C81E14; --tt-amber:#9A6B00; --tt-green:#1F7A4D;
    --tt-blue:#1C63C4; --tt-cyan:#0E7C8A;
    --shadow:0 1px 2px rgba(19,26,33,.06), 0 8px 24px -12px rgba(19,26,33,.18);
  }
}
:root[data-theme="light"]{
    --paper:#E9EBEE; --surface:#FFFFFF; --surface-2:#F4F5F7;
    --ink:#131A21; --ink-2:#4C5761; --ink-3:#7A848E;
    --rule:#C6CDD4; --rule-soft:#DCE1E6;
    --accent:#0E7C8A; --accent-ink:#FFFFFF; --accent-soft:#DCF0F1; --heat:#9A6B00; --heat-soft:#FBF0DA;
    --pass:#1F7A4D; --pass-soft:#DFF0E7;
    --todo:#8A5A00; --todo-soft:#FBF0DA; --todo-line:#E0B860;
    --tt-red:#C81E14; --tt-amber:#9A6B00; --tt-green:#1F7A4D;
    --tt-blue:#1C63C4; --tt-cyan:#0E7C8A;
    --shadow:0 1px 2px rgba(19,26,33,.06), 0 8px 24px -12px rgba(19,26,33,.18);
}
*{box-sizing:border-box}
body{margin:0; background:var(--paper); color:var(--ink); font-family:var(--f-body); font-size:var(--step-0); line-height:1.6; -webkit-font-smoothing:antialiased}
h1,h2,h3,h4{font-family:var(--f-display); margin:0; text-wrap:balance; letter-spacing:-.02em; line-height:1.06}
p{margin:0}
a{color:inherit}
:focus-visible{outline:2.5px solid var(--accent); outline-offset:3px; border-radius:2px}
.mono{font-family:var(--f-mono); font-variant-numeric:tabular-nums}
.lbl{font-family:var(--f-mono); font-size:.6875rem; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--ink-3)}
.shell{max-width:78rem; margin-inline:auto; padding-inline:var(--gutter)}


.top{border-bottom:1px solid var(--rule); background:var(--surface)}
.top__in{display:flex; align-items:center; gap:1.25rem; padding-block:.85rem; flex-wrap:wrap}
/* the logo is the home link — standard, and people reach for it before the nav */
.brand{display:flex; align-items:center; gap:.7rem; text-decoration:none; margin-right:auto; cursor:pointer; border-radius:.3rem}
.brand:hover .brand__logo{opacity:.85}
.brand__logo{transition:opacity .12s ease}
/* ⭐ His own logo, 240x127 and the largest that exists — shown at 132px so the
   source still has ~1.8x density on a retina screen. It is a real RGBA PNG, so
   it sits on the black ground without a plate behind it. On a light theme the
   silver wordmark would disappear, so there it gets a dark plate. */
.brand__logo{width:8.25rem; height:auto; flex:none; display:block}
.brand__txt{display:none}
@media (prefers-color-scheme:light){:root:not([data-theme="dark"]) .brand__logo{background:var(--ink); border-radius:.3rem; padding:.3rem .45rem}}
:root[data-theme="light"] .brand__logo{background:var(--ink); border-radius:.3rem; padding:.3rem .45rem}
@media (max-width:30rem){ .brand__logo{width:6.75rem} }
.brand__nm{font-family:var(--f-display); font-weight:800; font-size:1.0625rem; letter-spacing:-.03em; line-height:1.1}
.brand__sub{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.13em; text-transform:uppercase; color:var(--ink-3)}

/* dropdown — the same telltales as the service grid, so the menu teaches the
   symbol set before you ever reach a service page */
.nav{position:relative}
.nav>ul{display:flex; gap:1.15rem; flex-wrap:wrap; list-style:none; margin:0; padding:0}
.nav li{position:relative}

/* ⭐ MOBILE MENU. Below 60rem the nav becomes a panel and the button becomes an X.
   The bars are three spans so the animation is a real transform, not a glyph swap —
   a font character cannot morph, and a morph is what makes it feel built. */
.burger{display:none; width:2.9rem; height:2.9rem; flex:none; align-items:center; justify-content:center;
  background:transparent; border:1px solid var(--rule); border-radius:.35rem; cursor:pointer; padding:0}
.burger:hover{border-color:var(--accent)}
.burger i{display:block; position:relative; width:1.15rem; height:2px; background:var(--ink);
  border-radius:2px; transition:transform .22s cubic-bezier(.3,1.2,.4,1), background-color .18s ease}
.burger i::before,.burger i::after{content:""; position:absolute; left:0; width:100%; height:2px;
  background:var(--ink); border-radius:2px; transition:transform .22s cubic-bezier(.3,1.2,.4,1)}
.burger i::before{top:-6px} .burger i::after{top:6px}
.burger[aria-expanded="true"] i{background:transparent}
.burger[aria-expanded="true"] i::before{transform:translateY(6px) rotate(45deg); background:var(--accent)}
.burger[aria-expanded="true"] i::after{transform:translateY(-6px) rotate(-45deg); background:var(--accent)}

@media (max-width:60rem){
  .burger{display:inline-flex}
  .top__in{flex-wrap:nowrap; gap:.6rem}
  .nav{order:10; flex-basis:100%; display:none}
  .nav.open{display:block; padding-block:.6rem .2rem; border-top:1px solid var(--rule); margin-top:.7rem}
  .nav>ul{flex-direction:column; gap:0}
  .nav>ul>li{border-bottom:1px solid var(--rule-soft)}
  .nav>ul>li>a{display:flex; padding:.75rem .1rem; font-size:1rem}
  .nav .sub{display:grid; position:static; opacity:1; visibility:visible; transform:none;
    grid-template-columns:1fr 1fr; border:0; box-shadow:none; background:transparent;
    margin:0 0 .5rem; gap:.15rem}
  .nav .sub a{background:transparent; padding:.45rem .1rem; font-size:.8125rem}
  .nav .sub a:hover{background:transparent}
  .top__act .btn--go{padding:.55rem .8rem}
}

/* a menu that opens should say so — chevron flips when the panel is open */
.nav a[aria-haspopup]{display:inline-flex; align-items:center; gap:.4rem}
.nav a[aria-haspopup]::after{content:""; width:.4rem; height:.4rem; flex:none;
  border-right:2px solid currentColor; border-bottom:2px solid currentColor;
  transform:translateY(-2px) rotate(45deg); opacity:.7;
  transition:transform .15s ease, opacity .15s ease}
.nav li:hover>a[aria-haspopup]::after,
.nav li:focus-within>a[aria-haspopup]::after{transform:translateY(1px) rotate(-135deg); opacity:1}
.nav .sub{position:absolute; top:calc(100% + .55rem); left:-.75rem; z-index:30;
  display:grid; grid-template-columns:repeat(2,minmax(11.5rem,1fr)); gap:1px;
  background:var(--rule); border:1px solid var(--rule); border-radius:.45rem;
  box-shadow:var(--shadow); list-style:none; margin:0; padding:0; overflow:hidden;
  opacity:0; visibility:hidden; transform:translateY(-4px); transition:opacity .12s ease, transform .12s ease, visibility .12s}
.nav li:hover>.sub, .nav li:focus-within>.sub{opacity:1; visibility:visible; transform:none}
.nav .sub a{display:flex; align-items:center; gap:.6rem; background:var(--surface);
  padding:.6rem .8rem; border-bottom:0; font-size:.875rem; white-space:nowrap; color:var(--ink)}
.nav .sub a:hover{background:var(--surface-2); border-bottom:0}
.nav .sub .sym{width:1.85rem; height:1.85rem; border-radius:.3rem}
.nav .sub .sym svg{width:1.05rem; height:1.05rem}
@media (max-width:60rem){
  .nav .sub{position:static; opacity:1; visibility:visible; transform:none; grid-template-columns:1fr;
    box-shadow:none; margin:.4rem 0 .2rem; display:none}
  .nav li:focus-within>.sub{display:grid}
}

.nav a{text-decoration:none; font-size:.9375rem; font-weight:500; color:var(--ink-2); padding-block:.25rem; border-bottom:2px solid transparent}
.nav a:hover{color:var(--ink); border-bottom-color:var(--accent)}
.nav a[aria-current="page"]{color:var(--ink); border-bottom-color:var(--accent)}
.top__act{display:flex; align-items:center; gap:.85rem}
/* 🔴 MEASURED, NOT GUESSED (7 Sep 2026): at 375px — an iPhone — `.top__act`
   ran from x=194 to x=398 on EVERY page, so the site's primary call to action
   sat 23px off the right edge and the whole document scrolled sideways.
   ⭐ Found by walking every element's getBoundingClientRect() inside an iframe
   at 375, not by looking: the header still LOOKS fine because the overflow is
   off-screen, which is exactly why it survived.
   ⛔ The phone is what goes, not the button. A shop's number is one tap away in
   the menu; "Book a slot" is the whole point of the header. */
@media (max-width: 460px){
  .top__act .tel{display:none}
  .top__act{gap:.5rem}
}
.tel{text-decoration:none; font-family:var(--f-mono); font-weight:700; font-size:.9375rem; white-space:nowrap}
.tel>span:first-child{display:block; font-size:.625rem; letter-spacing:.13em; color:var(--ink-3); font-weight:400}
.btn{display:inline-flex; align-items:center; justify-content:center; gap:.5rem; min-height:2.75rem; padding:.6rem 1.15rem; border-radius:.25rem; font-family:var(--f-display); font-weight:700; font-size:.9375rem; text-decoration:none; border:1.5px solid transparent; cursor:pointer; transition:transform .12s ease, background-color .12s ease}
.btn--go{background:var(--accent); color:var(--accent-ink)}
.btn--go:hover{transform:translateY(-1px)}
.btn--line{border-color:var(--rule); color:var(--ink); background:transparent}
.btn--line:hover{border-color:var(--ink); background:var(--surface-2)}

.bay{display:grid; grid-template-columns:var(--rail) minmax(0,1fr); gap:clamp(1.5rem,4vw,3.5rem)}
@media (max-width:60rem){ .bay{grid-template-columns:minmax(0,1fr)} .rail{display:none} }
.rail{border-right:1px solid var(--rule); padding-right:1.25rem; padding-top:.4rem}
.rail dl{margin:0; display:grid; gap:1.35rem}
.rail dt{font-family:var(--f-mono); font-size:.625rem; font-weight:700; letter-spacing:.13em; text-transform:uppercase; color:var(--ink-3)}
.rail dd{margin:.25rem 0 0; font-family:var(--f-mono); font-size:.9375rem; font-weight:500; line-height:1.35}
.rail dd em{font-style:normal; color:var(--ink-3); font-size:.75rem; display:block; margin-top:.15rem}


/* ⭐ THE HERO IS AN INSTRUMENT CLUSTER. No stock photograph of a smiling mechanic —
   the shop has no photography yet, and a borrowed garage is worse than none. The
   gauge sweep is the one moment of motion on the page and it settles into a
   resting state that is fully legible with JS off and with motion reduced. */
.hero{position:relative; isolation:isolate; overflow:hidden}
.hero__bg{position:absolute; inset:0; z-index:-1; pointer-events:none}
/* ⭐ TWO LIGHTS, NOT ONE. A single accent wash reads as a gradient; a cool one
   high on the right and a warm one low on the left reads as a room with a booth
   lamp in it — which is exactly what the photograph beside it shows. */
.hero__bg::before{content:""; position:absolute; inset:0;
  background:radial-gradient(58% 62% at 78% 8%, color-mix(in srgb, var(--accent) 26%, transparent) 0%, transparent 70%),
             radial-gradient(48% 52% at 6% 96%, color-mix(in srgb, var(--heat) 18%, transparent) 0%, transparent 72%);}
.hero__bg::after{content:""; position:absolute; inset:0; opacity:.5;
  background-image:radial-gradient(currentColor .6px, transparent .6px);
  background-size:22px 22px; color:var(--rule);
  -webkit-mask-image:linear-gradient(to bottom, #000 0%, transparent 78%);
  mask-image:linear-gradient(to bottom, #000 0%, transparent 78%);}
.gauge{position:absolute; right:-6%; top:-14%; width:min(38rem,64vw); aspect-ratio:1;
  z-index:-1; opacity:.5; pointer-events:none}
.gauge svg{width:100%; height:100%; overflow:visible}
.gauge .arc{fill:none; stroke:var(--rule); stroke-width:1.5; stroke-linecap:round}
.gauge .arc--live{stroke:var(--accent); stroke-dasharray:var(--dash); stroke-dashoffset:var(--off)}
.gauge .tick{stroke:var(--ink-3); stroke-width:1.5; stroke-linecap:round}
.gauge .tick--hot{stroke:var(--accent)}
.gauge .needle{stroke:var(--ink); stroke-width:2.5; stroke-linecap:round;
  transform-origin:120px 120px; transform:rotate(var(--ang,128deg))}
@media (prefers-reduced-motion:no-preference){
  .gauge .needle{animation:sweep 2.1s cubic-bezier(.22,1.1,.3,1) .25s both}
  @keyframes sweep{ 0%{transform:rotate(-38deg)} 55%{transform:rotate(150deg)} 100%{transform:rotate(var(--ang,128deg))} }
}
@media (max-width:52rem){ .gauge{display:none} }

/* ═══════════════════════════════════════════════════════════════════════════
   MOTION. Two pieces, both cheap, both specific to this subject.

   ⛔ WHY THERE IS NO WebGL POOL. The best-known Three.js showcases measure
      around 47s to first paint and Apple ships ZERO canvas on its product
      pages. A slow hero loses more customers than a clever one wins, and the
      water panel says more than a rendered pool ever would.
   ⭐ So the depth is real but costs nothing: layered transforms, no library,
      no 3D model, no canvas.
   ═══════════════════════════════════════════════════════════════════════════ */

/* 1 ── THE BULB CHECK. Every instrument panel built since the nineties lights every lamp for
      about two seconds on power-up, then drops them one by one as each system
      reports in. A pool controller does the same thing. The service grid does exactly that when it comes into
   view. Nobody else can do this, because nobody else made the icons telltales. */
.svc .sym{transition:box-shadow .35s ease, background-color .35s ease}
@media (prefers-reduced-motion:no-preference){
  .svc.bulbcheck .sym{
    animation:bulb 2.1s ease-out both;
    animation-delay:calc(var(--i,0) * 90ms);
  }
  @keyframes bulb{
    0%   {background:color-mix(in srgb, currentColor 13%, transparent); box-shadow:none}
    8%   {background:color-mix(in srgb, currentColor 46%, transparent);
          box-shadow:0 0 0 1px color-mix(in srgb, currentColor 55%, transparent), 0 0 22px -2px currentColor}
    62%  {background:color-mix(in srgb, currentColor 46%, transparent);
          box-shadow:0 0 0 1px color-mix(in srgb, currentColor 55%, transparent), 0 0 22px -2px currentColor}
    100% {background:color-mix(in srgb, currentColor 13%, transparent); box-shadow:none}
  }
}

/* ⭐ THE STEPS REPORT IN AS YOU PASS THEM. Each number goes green the moment its
   step reaches reading position — the same language as the bulb check: a system
   confirming it is good. It is a five-step process and this makes the page feel
   like it is being worked through rather than scrolled past.
   ⛔ Resting state is the plain number, fully legible with JS off. */
.step__n{position:relative; transition:color .35s ease}
.step__n::after{content:""; position:absolute; left:0; bottom:-.3rem; width:1.6rem; height:2px;
  border-radius:2px; background:var(--pass); transform:scaleX(0); transform-origin:left;
  transition:transform .4s cubic-bezier(.2,.9,.3,1)}
.step.done{--n-col:var(--pass)}
.step.done .step__n::after{transform:scaleX(1)}
@media (prefers-reduced-motion:reduce){
  .step__n,.step__n::after{transition:none}
}

/* 2 ── HERO DEPTH. The gauge, the figure and the dot grid sit on three planes
   and move at three rates against the pointer. It is a flat cut-out; this is
   what makes it read as standing in a room. Scroll adds a slow vertical drift.
   ⛔ Everything is at its resting position with JS off — nothing is parked at
      opacity 0 waiting for an observer. */
.hero__bg,.gauge,.hero__fig img{will-change:transform}
@media (prefers-reduced-motion:no-preference){
  .hero__bg{transform:translate3d(calc(var(--px,0) * 6px), calc(var(--py,0) * 5px + var(--sy,0) * 14px), 0)}
  .gauge{transform:translate3d(calc(var(--px,0) * -16px), calc(var(--py,0) * -12px + var(--sy,0) * 34px), 0) rotate(calc(var(--px,0) * 1.1deg))}
  .hero__fig img{transform:translate3d(calc(var(--px,0) * 9px), calc(var(--sy,0) * -10px), 0)}
  .hero__bg,.gauge,.hero__fig img{transition:transform .45s cubic-bezier(.2,.8,.3,1)}
  .hero.tracking .hero__bg,.hero.tracking .gauge,.hero.tracking .hero__fig img{transition:none}
}

/* ⭐ THE HERO FIGURE SLOT HOLDS THE WATER PANEL, not a photograph.
     ⛔ There is no photography for this trade yet. ⏭️ If one ever exists these
     rules are what it inherits: flat on the ground, no fake drop shadow. */
.shots .shot--cut{background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 10%, transparent), transparent)}
.shots .shot--cut img{aspect-ratio:auto; object-fit:contain; max-height:15rem; margin-inline:auto; padding-top:.75rem}

/* ⭐ THE HERO FIGURE — a cut-out PNG with real alpha, so he stands on the black
   ground with the gauge glow behind rather than sitting in a box.
   ⚠️ This image is AI-GENERATED. It is the most prominent place on the site, so
   it is the FIRST one to replace with a photograph of Sandhill. The flag says so. */
.hero__fig{position:relative; display:grid; justify-items:center; align-self:end}
.hero__fig{position:relative; align-self:center; justify-items:stretch}
/* ⚠️ The glow under the figure stays — it is what stops the panel looking
   pasted onto a very dark page. */
/* ⚠️ A hero photo shorter than the headline beside it reads as an
   afterthought. A 5:4 crop holds the column. */
.hero__fig img{width:100%; height:auto; display:block; border-radius:1rem;
  aspect-ratio:5/4; object-fit:cover;
  border:1px solid color-mix(in srgb, var(--accent) 28%, var(--rule));
  box-shadow:0 26px 70px -28px rgba(0,0,0,.95),
             0 0 0 1px color-mix(in srgb, var(--accent) 8%, transparent);
}
/* ⭐ THE CAPTION SITS ON THE PHOTOGRAPH, not under it — it keeps the hero one
   object instead of two stacked things, and it lands where the eye already is. */
.hero__figcap{position:absolute; left:.85rem; bottom:.85rem; z-index:2;
  font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.1em;
  text-transform:uppercase; color:var(--ink);
  background:color-mix(in srgb, var(--paper) 74%, transparent);
  border:1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  backdrop-filter:blur(6px); padding:.3rem .6rem; border-radius:.35rem}
/* ⚠️ The photo is the brightest thing on a very dark page, so it needs a floor
   under it or it looks pasted on. */
.hero__fig::before{content:""; position:absolute; inset:10% -8% -14% -8%; z-index:-1;
  border-radius:50%; filter:blur(48px);
  background:radial-gradient(60% 60% at 50% 50%,
    color-mix(in srgb, var(--accent) 30%, transparent), transparent 72%)}
.hero__fig .shot__flag{position:absolute; top:.4rem; right:.4rem; z-index:2}
.hero__fig::after{content:""; position:absolute; left:50%; bottom:-2px; translate:-50% 0;
  width:76%; height:2px; background:linear-gradient(90deg,transparent,var(--accent),transparent); opacity:.75}
@media (max-width:56rem){
  .hero__fig{display:block; margin-top:1.5rem}
  .hero__fig img{aspect-ratio:16/10}
}
.ticketband{display:grid; grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);
  gap:clamp(1.5rem,4vw,3rem); align-items:center}
@media (max-width:56rem){ .ticketband{grid-template-columns:minmax(0,1fr)} }
.hero{padding-block:clamp(2.25rem,5vw,4rem) clamp(1.75rem,4vw,3rem); border-bottom:1px solid var(--rule)}
.eyebrow{display:flex; align-items:center; gap:.6rem; margin-bottom:1.1rem; flex-wrap:wrap}
.eyebrow .dot{width:.5rem; height:.5rem; border-radius:50%; background:var(--pass); flex:none}
.hero h1{font-size:var(--step-4); font-weight:800; max-width:16ch; margin-bottom:1.1rem}
.hero h1 u{text-decoration:none; color:var(--accent)}
.hero__lede{font-size:var(--step-1); color:var(--ink-2); max-width:46ch; margin-bottom:1.75rem}
.hero__act{display:flex; gap:.75rem; flex-wrap:wrap; align-items:center}
.hero__note{font-size:.8125rem; color:var(--ink-3); max-width:36ch; margin-top:1rem}
.split{display:grid; grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr); gap:clamp(1.75rem,4vw,3rem); align-items:start}
@media (max-width:56rem){ .split{grid-template-columns:minmax(0,1fr)} }

/* page head for interior routes */
.phead{padding-block:clamp(2rem,4vw,3.25rem) clamp(1.5rem,3vw,2.25rem); border-bottom:1px solid var(--rule)}
.crumb{font-family:var(--f-mono); font-size:.75rem; color:var(--ink-3); margin-bottom:.9rem}
.crumb a{text-decoration:none}
.crumb a:hover{color:var(--accent)}
.phead h1{font-size:var(--step-3); font-weight:800; max-width:20ch; margin-bottom:.85rem}
.phead p{font-size:var(--step-1); color:var(--ink-2); max-width:52ch}

.ticket{background:var(--surface); border:1px solid var(--rule); border-radius:.4rem; box-shadow:var(--shadow); overflow:hidden}
.ticket__cap{display:flex; align-items:center; gap:.5rem; padding:.5rem 1.1rem; font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.11em; text-transform:uppercase}
.ticket__cap{background:var(--surface-2); color:var(--ink-2); border-bottom:1px solid var(--rule)}
.ticket__hd{display:flex; align-items:baseline; gap:.75rem; padding:.85rem 1.1rem; border-bottom:1px solid var(--rule); background:var(--surface-2); flex-wrap:wrap}
.ticket__no{font-family:var(--f-mono); font-weight:700; font-size:.8125rem}
.ticket__veh{font-size:.8125rem; color:var(--ink-2); margin-left:auto; font-family:var(--f-mono)}
.ticket__body{padding:1.1rem}
.line{display:grid; grid-template-columns:auto minmax(0,1fr) auto; gap:.6rem .9rem; align-items:baseline; padding-block:.7rem; border-bottom:1px dashed var(--rule-soft)}
.line:last-of-type{border-bottom:0}
.line__code{font-family:var(--f-mono); font-weight:700; font-size:.8125rem; color:var(--accent); background:var(--accent-soft); padding:.1rem .4rem; border-radius:.2rem; white-space:nowrap}
.line__what b{display:block; font-weight:600; font-size:.9375rem}
.line__what span{display:block; font-size:.8125rem; color:var(--ink-2); margin-top:.1rem}
.line__amt{font-family:var(--f-mono); font-weight:700; font-size:.9375rem; white-space:nowrap; font-variant-numeric:tabular-nums}
.line__amt em{display:block; font-style:normal; font-weight:400; font-size:.6875rem; color:var(--ink-3); text-align:right}
.ticket__ft{display:flex; align-items:center; gap:.75rem; padding:.9rem 1.1rem; border-top:1px solid var(--rule); background:var(--surface-2); flex-wrap:wrap}
.stamp{display:inline-flex; align-items:center; gap:.4rem; font-family:var(--f-mono); font-size:.75rem; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:var(--pass); background:var(--pass-soft); border:1px solid color-mix(in srgb, var(--pass) 30%, transparent); padding:.28rem .55rem; border-radius:.2rem}
.ticket__tot{margin-left:auto; font-family:var(--f-mono); font-weight:700; font-size:1.0625rem}
.ticket__tot em{font-style:normal; font-size:.6875rem; color:var(--ink-3); font-weight:400; letter-spacing:.1em; text-transform:uppercase; margin-right:.4rem}

.sec{padding-block:clamp(2.5rem,5vw,4rem); border-bottom:1px solid var(--rule)}
.sec:last-child{border-bottom:0}
.sec__hd{max-width:var(--measure); margin-bottom:2rem}
.sec__hd h2{font-size:var(--step-3); font-weight:800; margin-block:.6rem .7rem}
.sec__hd p{color:var(--ink-2); font-size:var(--step-1)}
.prose{max-width:var(--measure); display:grid; gap:1.15rem}
.prose h3{font-size:1.25rem; font-weight:700; margin-top:.75rem}
.prose p{color:var(--ink-2)}
.prose ul{margin:0; padding-left:1.15rem; color:var(--ink-2); display:grid; gap:.5rem}
.prose strong{color:var(--ink); font-weight:600}

.steps{display:grid}
.step{display:grid; grid-template-columns:3.25rem minmax(0,1fr); gap:1.25rem; padding-block:1.35rem; border-top:1px solid var(--rule-soft); align-items:start}
.step:first-child{border-top:0; padding-top:0}
.step__n{font-family:var(--f-mono); font-weight:700; font-size:.8125rem; color:var(--n-col,var(--ink-3)); padding-top:.2rem}
.step h3{font-size:1.1875rem; font-weight:700; margin-bottom:.35rem}
.step p{color:var(--ink-2); max-width:58ch; font-size:.9375rem}
.step__meta{margin-top:.5rem; font-family:var(--f-mono); font-size:.75rem; color:var(--ink-3)}


/* ⭐ THE SPECIAL IS A WORK ORDER, NOT A COUPON. Every pool company runs a
   first-month offer and every one of them prints the same starburst. This one
   is set as a torn-off ticket so it belongs to the same world as the water
   report in the hero — the page has one idea, not two. */
/* ⭐ A COUPON THAT IS ACTUALLY A COUPON. Perforated edge, tear-off stub, and it
   prints on its own — @media print hides the entire site and leaves this one card
   on the page, so "print" produces a coupon and not 26 pages of website.
   ⛔ The dashes are a repeating-linear-gradient border, not an image: it stays
   crisp at any zoom and costs nothing. */
/* ⭐ THE SPEC PLATE. Every service page gets a drawing instead of a stock photo,
   because we have three usable photographs and fifteen service pages — and the
   same borrowed garage repeated fifteen times is worse than no picture at all.
   Each plate is the service's own telltale on an engineering grid, with that
   service's real figures set as dimension callouts. It teaches something a stock
   image cannot, and it costs about 2 KB. */
.plate{position:relative; border:1px solid var(--rule); border-radius:.45rem;
  background:var(--surface); overflow:hidden; display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1.05fr); align-items:center; gap:0}
@media (max-width:44rem){ .plate{grid-template-columns:minmax(0,1fr)} }
.plate__draw{position:relative; display:grid; place-items:center; padding:clamp(1.5rem,4vw,2.5rem);
  background:
    linear-gradient(to right, color-mix(in srgb,var(--rule) 45%,transparent) 1px, transparent 1px) 0 0/28px 28px,
    linear-gradient(to bottom, color-mix(in srgb,var(--rule) 45%,transparent) 1px, transparent 1px) 0 0/28px 28px,
    var(--surface-2)}
.plate__draw .sym{width:7.5rem; height:7.5rem; border-radius:.9rem;
  background:color-mix(in srgb, currentColor 12%, transparent);
  border:1px solid color-mix(in srgb, currentColor 34%, transparent)}
.plate__draw .sym svg{width:4.2rem; height:4.2rem; stroke-width:1.5}
.plate__specs{padding:clamp(1.25rem,3vw,2rem); display:grid; gap:0; align-content:center}
.plate__row{display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:baseline;
  gap:.6rem; padding-block:.7rem; border-bottom:1px dashed var(--rule-soft)}
.plate__row:last-child{border-bottom:0}
.plate__k{font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.11em;
  text-transform:uppercase; color:var(--ink-3); white-space:nowrap}
.plate__lead{height:1px; background:repeating-linear-gradient(to right,
  color-mix(in srgb,var(--rule) 90%,transparent) 0 4px, transparent 4px 8px); align-self:center}
.plate__v{font-family:var(--f-mono); font-weight:700; font-size:.9375rem; white-space:nowrap}
.plate__cap{position:absolute; left:.7rem; top:.7rem; font-family:var(--f-mono);
  font-size:.625rem; letter-spacing:.12em; text-transform:uppercase; color:var(--ink-3)}

.coupon{position:relative; background:var(--surface); color:var(--ink);
  border-radius:.5rem; box-shadow:var(--shadow); overflow:hidden;
  display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:stretch;
  border:2px dashed var(--rule)}
@media (max-width:46rem){ .coupon{grid-template-columns:minmax(0,1fr)} }
.coupon__body{padding:clamp(1.25rem,3vw,2rem)}
.coupon__tag{display:inline-flex; align-items:center; gap:.45rem; font-family:var(--f-mono);
  font-size:.6875rem; font-weight:700; letter-spacing:.13em; text-transform:uppercase;
  color:var(--accent); margin-bottom:.7rem}
.coupon h2{font-size:var(--step-2); font-weight:800; max-width:20ch; margin-bottom:.6rem}
.coupon__lede{color:var(--ink-2); max-width:46ch; margin-bottom:1rem}
.coupon__list{list-style:none; margin:0 0 1.1rem; padding:0; display:grid; gap:.4rem;
  font-size:.9375rem; color:var(--ink-2)}
.coupon__list li{display:grid; grid-template-columns:1.1rem minmax(0,1fr); gap:.55rem; align-items:start}
.coupon__list svg{width:1.1rem; height:1.1rem; stroke:var(--pass); fill:none; stroke-width:2;
  stroke-linecap:round; stroke-linejoin:round; margin-top:.25rem}
.coupon__fine{font-size:.75rem; color:var(--ink-3); max-width:54ch}

/* the tear-off stub, divided by a real perforation */
.coupon__stub{position:relative; display:grid; align-content:center; justify-items:center;
  gap:.2rem; text-align:center; padding:clamp(1.25rem,3vw,2rem) clamp(1.5rem,3vw,2.25rem);
  background:var(--surface-2); min-width:13rem}
.coupon__stub::before{content:""; position:absolute; left:0; top:.75rem; bottom:.75rem; width:2px;
  background:repeating-linear-gradient(to bottom, var(--rule) 0 6px, transparent 6px 12px)}
@media (max-width:46rem){
  .coupon__stub::before{left:.75rem; right:.75rem; top:0; bottom:auto; width:auto; height:2px;
    background:repeating-linear-gradient(to right, var(--rule) 0 6px, transparent 6px 12px)}
}
.coupon__price{font-family:var(--f-mono); font-weight:700; line-height:1;
  font-size:clamp(2.6rem,1.8rem + 3vw,3.75rem); color:var(--accent)}
.coupon__cap{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3)}
/* ONE PRICE ON THIS STUB, not the auto build's two tiers.
   ⛔ Do not re-add a second tier without a second real price to put in it. */
.coupon__tiers{display:grid; gap:.7rem; width:100%}
.coupon__tier{display:grid; gap:.1rem; justify-items:center}
.coupon__tier + .coupon__tier{padding-top:.7rem;
  border-top:2px dashed color-mix(in srgb, var(--rule) 80%, transparent)}
.coupon__tier .coupon__price{font-size:clamp(2.1rem,1.5rem + 2.2vw,2.9rem)}
.coupon__act{display:flex; gap:.5rem; flex-wrap:wrap; justify-content:center; margin-top:.9rem}
.coupon__act .btn{padding:.5rem .85rem; min-height:2.5rem; font-size:.875rem}

@media print{
  body > *:not(main), .top, .foot, .draft, .launch, .chat, .rail, .sec:not(.coupon-sec){display:none!important}
  .coupon-sec, .coupon-sec *{visibility:visible}
  .coupon{box-shadow:none; border-color:#000; color:#000; background:#fff}
  .coupon__stub{background:#f4f4f4}
  .coupon__act{display:none!important}
  .coupon__price{color:#000}
  @page{margin:12mm}
}

.offer{display:grid; grid-template-columns:minmax(0,1fr) auto; gap:clamp(1.25rem,3vw,2.5rem);
  align-items:center; background:var(--surface); border:1px solid var(--rule);
  border-left:4px solid var(--accent); border-radius:.45rem; padding:clamp(1.25rem,3vw,2rem);
  box-shadow:var(--shadow); position:relative; overflow:hidden}
@media (max-width:46rem){ .offer{grid-template-columns:minmax(0,1fr)} }
.offer__tag{display:inline-flex; align-items:center; gap:.45rem; font-family:var(--f-mono);
  font-size:.6875rem; font-weight:700; letter-spacing:.13em; text-transform:uppercase;
  color:var(--accent); margin-bottom:.7rem}
.offer h2{font-size:var(--step-2); font-weight:800; max-width:18ch; margin-bottom:.65rem}
.offer__lede{color:var(--ink-2); max-width:48ch; margin-bottom:1.1rem}
.offer__list{list-style:none; margin:0 0 1.25rem; padding:0; display:grid; gap:.4rem;
  font-size:.9375rem; color:var(--ink-2)}
.offer__list li{display:grid; grid-template-columns:1.1rem minmax(0,1fr); gap:.55rem; align-items:start}
.offer__list svg{width:1.1rem; height:1.1rem; stroke:var(--pass); fill:none; stroke-width:2;
  stroke-linecap:round; stroke-linejoin:round; margin-top:.25rem}
.offer__fine{font-size:.75rem; color:var(--ink-3); max-width:52ch}
.offer__price{display:grid; justify-items:center; gap:.15rem; text-align:center;
  border:1px dashed var(--rule); border-radius:.4rem; padding:1.1rem 1.4rem;
  background:var(--surface-2); min-width:11rem}
.offer__price .amt{font-family:var(--f-mono); font-weight:700; font-size:clamp(2rem,1.4rem+2vw,2.75rem); line-height:1}
.offer__price .cap{font-family:var(--f-mono); font-size:.625rem; letter-spacing:.13em;
  text-transform:uppercase; color:var(--ink-3)}
.offer__act{margin-top:.9rem; display:flex; gap:.6rem; flex-wrap:wrap; justify-content:center}
.svc{display:grid; grid-template-columns:repeat(auto-fill,minmax(15.5rem,1fr)); gap:1px; background:var(--rule); border:1px solid var(--rule); border-radius:.4rem; overflow:hidden}
.svc a{background:var(--surface); padding:1.15rem; text-decoration:none; align-content:start; transition:background-color .12s ease}
.svc a:hover{background:var(--surface-2)}
.svc a{grid-template-columns:auto minmax(0,1fr); column-gap:.85rem}
.svc h3{font-size:1.0625rem; font-weight:700}
.sym{width:2.5rem; height:2.5rem; flex:none; border-radius:.45rem; display:grid; place-items:center;
     background:color-mix(in srgb, currentColor 13%, transparent);
     border:1px solid color-mix(in srgb, currentColor 30%, transparent)}
.sym svg{width:1.4rem; height:1.4rem; stroke:currentColor; fill:none; stroke-width:1.7;
     stroke-linecap:round; stroke-linejoin:round}
.svc a{display:flex; align-items:flex-start; gap:.9rem}
.svc .txt{display:grid; gap:.3rem; min-width:0}
.sym--lg{width:3.25rem; height:3.25rem; border-radius:.55rem}
.sym--lg svg{width:1.85rem; height:1.85rem}
.svc p{font-size:.875rem; color:var(--ink-2)}
.svc .spec{font-family:var(--f-mono); font-size:.6875rem; color:var(--ink-3); margin-top:.4rem}


/* ⭐ PHOTOGRAPHY BAND. A real photograph earns its place by showing the WORK, not
   by decorating the page. Sized and cropped here so a replacement drops in without
   the layout moving — the aspect ratio is fixed, the image covers it. */
.shots{display:grid; grid-template-columns:repeat(auto-fit,minmax(13rem,1fr)); gap:1px;
  background:var(--rule); border:1px solid var(--rule); border-radius:.45rem; overflow:hidden}
.shot{position:relative; background:var(--surface); display:grid}
.shot img{width:100%; aspect-ratio:5/4; object-fit:cover; display:block}
.shot figcaption{padding:.7rem .9rem .85rem; font-size:.8125rem; color:var(--ink-2)}
.shot figcaption b{display:block; color:var(--ink); font-weight:600; font-size:.875rem; margin-bottom:.15rem}
.shot__flag{position:absolute; top:.6rem; left:.6rem; font-family:var(--f-mono); font-size:.625rem;
  font-weight:700; letter-spacing:.11em; text-transform:uppercase; color:var(--todo);
  background:color-mix(in srgb, var(--todo-soft) 92%, transparent); border:1px solid var(--todo-line);
  padding:.25rem .5rem; border-radius:.2rem}
.shot--empty{display:grid; place-items:center; text-align:center; gap:.3rem; padding:1.5rem 1rem;
  aspect-ratio:5/4; border:1.5px dashed var(--rule); background:var(--surface-2)}
.shot--empty b{font-family:var(--f-mono); font-size:.75rem; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-2)}
.shot--empty span{font-size:.8125rem; color:var(--ink-3); max-width:26ch}

/* ⛔ THE DEMONSTRATION STRIP — see the comment in build.py's SHELL.
   Uses the accent so it reads as part of the design rather than as a browser
   warning, but it is the first thing on every page and it is never hidden. */
/* 🔴 TWO CHAT LAUNCHERS IN ONE CORNER (7 Sep 2026). This site is framed on
   botsonstaff.com/see-a-repair-site, and that page has its own fixed chat pill
   in the bottom-right. Measured in page coordinates: ours landed at x1292-1348
   / y1083-1139 and theirs at x1245-1380 / y822-880 — not overlapping, but two
   chat bubbles in the same corner region, one just inside a bordered frame and
   one just outside it. A visitor cannot tell which is the shop's.
   ⭐ SO WHEN FRAMED, THE SHOP'S CHAT MOVES LEFT. Both stay usable, the demo
   keeps the feature we are actually selling, and which is which becomes
   obvious. ⛔ Do not hide it instead: "booking and the chat both live" is what
   the page above the frame promises. */
html.framed .launch{right:auto; left:1rem}
html.framed .chat{right:auto; left:1rem}
/* ═══ THE WATER REPORT ════════════════════════════════════════════════════
   ⭐ THE HERO OF THIS SITE IS A DOCUMENT, not a photograph of a pool. Every
   competitor shows a blue rectangle and the word "sparkling"; none of them
   shows the six numbers, which is the only thing that distinguishes one pool
   company from another.
   ⚠️ The row that is OUT OF RANGE is colored, and the five that are fine are
   not. ⛔ Do not color all six — a readout where everything glows says
   nothing, and the whole point is that the eye lands on the one line that
   matters before a word has been read. */
.wr{position:relative; background:var(--surface); border:1px solid var(--rule);
  border-radius:.75rem; overflow:hidden; box-shadow:var(--shadow);
  font-family:var(--f-mono)}
.wr__hd{display:flex; justify-content:space-between; gap:1rem; align-items:baseline;
  padding:.75rem 1rem; border-bottom:1px solid var(--rule); background:var(--surface-2);
  font-size:.625rem; letter-spacing:.12em; text-transform:uppercase; color:var(--ink-3)}
.wr__when{white-space:nowrap}
.wr__row{display:grid; grid-template-columns:3.1rem minmax(0,1fr) 4.5rem auto;
  grid-template-areas:"code name bar val" "code rng rng rng";
  gap:.15rem .7rem; align-items:center; padding:.6rem 1rem;
  border-bottom:1px dashed var(--rule-soft)}
.wr__code{grid-area:code; font-size:.6875rem; font-weight:700; letter-spacing:.06em;
  color:var(--ink-3)}
.wr__name{grid-area:name; font-family:var(--f-body); font-size:.875rem; color:var(--ink)}
.wr__bar{grid-area:bar; height:.3rem; border-radius:99px; background:var(--rule); overflow:hidden}
.wr__bar i{display:block; height:100%; border-radius:99px; background:var(--pass)}
.wr__val{grid-area:val; font-size:1rem; font-weight:700; text-align:right; white-space:nowrap}
.wr__val em{font-style:normal; font-size:.625rem; color:var(--ink-3); margin-left:.2rem}
.wr__rng{grid-area:rng; font-size:.625rem; letter-spacing:.08em; color:var(--ink-3)}
/* the one line that is wrong */
.wr__row--high{background:color-mix(in srgb, var(--todo) 9%, transparent)}
.wr__row--high .wr__bar i{background:var(--todo)}
.wr__row--high .wr__val{color:var(--todo)}
.wr__row--high .wr__code{color:var(--todo)}
.wr__note{padding:.85rem 1rem; font-family:var(--f-body); font-size:.8125rem;
  line-height:1.5; color:var(--ink-2); border-bottom:1px solid var(--rule);
  background:color-mix(in srgb, var(--todo-soft) 60%, transparent)}
.wr__note b{color:var(--ink)}
.wr__ft{display:flex; justify-content:space-between; align-items:center; gap:1rem;
  padding:.7rem 1rem; font-size:.625rem; letter-spacing:.11em; text-transform:uppercase;
  color:var(--ink-3); background:var(--surface-2)}
.wr__ft b{color:var(--ink)}
.wr__chip{color:var(--pass); border:1px solid color-mix(in srgb, var(--pass) 45%, transparent);
  background:color-mix(in srgb, var(--pass) 12%, transparent);
  padding:.25rem .5rem; border-radius:.25rem; font-weight:700}
@media (max-width:30rem){ .wr__row{grid-template-columns:2.8rem minmax(0,1fr) auto;
  grid-template-areas:"code name val" "code bar bar" "code rng rng"} }

.demostrip{display:flex; align-items:flex-start; gap:.6rem; padding:.6rem clamp(1rem,4vw,2rem);
  background:var(--accent-soft); border-bottom:1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  font-family:var(--f-mono); font-size:.75rem; line-height:1.45; color:var(--ink-2)}
.demostrip b{color:var(--ink); font-weight:700}
.demostrip a{color:var(--accent); font-weight:600}
.demostrip__dot{flex:none; width:.5rem; height:.5rem; margin-top:.4rem; border-radius:50%;
  background:var(--accent)}
@media print{.demostrip{display:none}}
.mapcard{display:flex; align-items:center; gap:.9rem; text-decoration:none;
  background:var(--surface); border:1px solid var(--rule); border-left:3px solid var(--accent);
  border-radius:.45rem; padding:1.1rem 1.25rem; min-height:9rem}
.mapcard:hover{background:var(--surface-2)}
.mapcard__pin{width:2.75rem; height:2.75rem; flex:none; border-radius:.45rem; display:grid; place-items:center;
  background:color-mix(in srgb, var(--accent) 16%, transparent); color:var(--accent)}
.mapcard__pin svg{width:1.5rem; height:1.5rem; fill:currentColor}
.mapcard b{display:block; font-family:var(--f-display); font-size:1.0625rem; font-weight:700}
.mapcard span{font-size:.9375rem; color:var(--ink-2)}
.mapcard em{display:block; font-style:normal; font-family:var(--f-mono); font-size:.6875rem;
  letter-spacing:.11em; text-transform:uppercase; color:var(--accent); margin-top:.35rem}
.mount{border:1px solid var(--rule); border-radius:.45rem; background:var(--surface-2);
  display:grid; place-items:center; text-align:center; gap:.3rem; padding:2.25rem 1rem; min-height:10rem}
.mount b{font-family:var(--f-mono); font-size:.75rem; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-2)}
.mount span{font-size:.8125rem; color:var(--ink-3); max-width:34ch}
.slot{border:1.5px dashed var(--rule); border-radius:.4rem; background:var(--surface-2); display:grid; place-items:center; text-align:center; padding:1.5rem 1rem; gap:.3rem; min-height:11rem}
.slot b{font-family:var(--f-mono); font-size:.75rem; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-2)}
.slot span{font-size:.8125rem; color:var(--ink-3); max-width:32ch}

.cta{color:var(--ink); border-radius:.5rem; padding:clamp(1.75rem,4vw,2.75rem); display:grid; gap:1.25rem}
.cta{background:var(--surface-2); border:1px solid var(--rule)}
.cta h2{font-size:var(--step-2); font-weight:800; max-width:22ch}
.cta p{max-width:52ch}
.cta p{color:var(--ink-2)}
.cta__row{display:flex; gap:.75rem; flex-wrap:wrap; align-items:center}

/* faq */
.faq{border-top:1px solid var(--rule-soft); max-width:var(--measure)}
.faq details{border-bottom:1px solid var(--rule-soft)}
.faq summary{cursor:pointer; padding:1rem 0; font-family:var(--f-display); font-weight:700; font-size:1.0625rem; list-style:none; display:flex; gap:.75rem; align-items:flex-start; min-height:2.75rem}
.faq summary::-webkit-details-marker{display:none}
.faq summary::before{content:"+"; font-family:var(--f-mono); color:var(--accent); font-weight:700; flex:none; line-height:1.4}
.faq details[open] summary::before{content:"\2212"}
.faq p{padding:0 0 1.1rem 1.6rem; color:var(--ink-2); max-width:58ch}

/* tables */
.tbl-wrap{overflow-x:auto; border:1px solid var(--rule); border-radius:.4rem; background:var(--surface)}
table{border-collapse:collapse; width:100%; min-width:34rem; font-size:.9375rem}
th,td{text-align:left; padding:.75rem .9rem; border-bottom:1px solid var(--rule-soft); vertical-align:top}
th{font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.11em; text-transform:uppercase; color:var(--ink-3); background:var(--surface-2)}
tr:last-child td{border-bottom:0}
td.num{font-family:var(--f-mono); font-variant-numeric:tabular-nums; white-space:nowrap}

.areas{display:flex; flex-wrap:wrap; gap:.5rem; list-style:none; margin:0; padding:0}
.areas li{font-family:var(--f-mono); font-size:.8125rem; border:1px solid var(--rule); border-radius:1.25rem; padding:.35rem .75rem; background:var(--surface)}


/* his own channels — ⛔ Yelp is deliberately absent, it is barred estate-wide */
.social{display:flex; gap:.5rem; flex-wrap:wrap; margin-top:.9rem}
.social a{display:inline-flex; align-items:center; gap:.4rem; text-decoration:none;
  border:1px solid var(--rule); border-radius:1.25rem; padding:.35rem .75rem;
  font-size:.8125rem; color:var(--ink-2); background:var(--surface)}
.social a:hover{border-color:var(--accent); color:var(--ink)}
.social svg{width:.95rem; height:.95rem; fill:currentColor}
.foot{padding-block:2.5rem 3rem; font-size:.875rem; color:var(--ink-2); border-top:1px solid var(--rule)}
.foot__grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(12rem,1fr)); gap:1.75rem; margin-bottom:2rem}
.foot h4{font-family:var(--f-mono); font-size:.6875rem; letter-spacing:.13em; text-transform:uppercase; color:var(--ink-3); margin-bottom:.6rem}
.foot ul{list-style:none; margin:0; padding:0; display:grid; gap:.35rem}
.foot a{text-decoration:none; color:var(--ink-2)}
.foot a:hover{color:var(--ink); text-decoration:underline}

/* ⭐ A CIRCLE, NOT A PILL. 3.5rem is comfortably over the 44px touch-target
   floor, and the label moves to aria-label so a screen reader still gets
   "Describe a symptom" while the corner stays quiet. The tooltip on hover
   keeps the meaning discoverable for everyone else. */
.launch{position:fixed; right:1.15rem; bottom:1.15rem; z-index:40;
  width:3.5rem; height:3.5rem; border-radius:50%; padding:0;
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--accent); color:var(--accent-ink); border:0; cursor:pointer;
  box-shadow:0 6px 20px -4px rgba(0,0,0,.55), 0 0 0 1px color-mix(in srgb,var(--accent) 55%,transparent);
  transition:transform .15s cubic-bezier(.3,1.3,.5,1), box-shadow .15s ease}
.launch:hover{transform:scale(1.06)}
.launch:active{transform:scale(.96)}
.launch svg{width:1.55rem; height:1.55rem; fill:none; stroke:currentColor;
  stroke-width:1.9; stroke-linecap:round; stroke-linejoin:round}
.launch::after{content:attr(data-label); position:absolute; right:calc(100% + .6rem);
  white-space:nowrap; background:var(--surface); color:var(--ink);
  border:1px solid var(--rule); border-radius:.3rem; padding:.35rem .6rem;
  font-family:var(--f-body); font-size:.8125rem; box-shadow:var(--shadow);
  opacity:0; pointer-events:none; transform:translateX(4px); transition:opacity .14s ease, transform .14s ease}
.launch:hover::after,.launch:focus-visible::after{opacity:1; transform:none}
@media (max-width:30rem){ .launch::after{display:none} }
.chat{position:fixed; right:1rem; bottom:1rem; z-index:41; width:min(23rem, calc(100vw - 2rem)); background:var(--surface); border:1px solid var(--rule); border-radius:.5rem; box-shadow:var(--shadow); overflow:hidden; display:grid; grid-template-rows:auto 1fr auto auto; max-height:min(32rem, calc(100vh - 2rem))}
.chat__hd{display:flex; align-items:center; gap:.6rem; padding:.75rem .9rem; border-bottom:1px solid var(--rule); background:var(--surface-2)}
.chat__hd b{font-family:var(--f-display); font-size:.9375rem}
.chat__hd .lbl{margin-left:auto}
.chat__x{background:none; border:0; cursor:pointer; color:var(--ink-2); font-size:1.15rem; line-height:1; padding:.2rem .3rem; min-height:2rem}
.chat__log{padding:.9rem; display:grid; gap:.7rem; overflow-y:auto; align-content:start}
.msg{max-width:85%; padding:.55rem .75rem; border-radius:.45rem; font-size:.875rem; line-height:1.45}
.msg--bot{background:var(--surface-2); border:1px solid var(--rule-soft); justify-self:start; border-bottom-left-radius:.1rem}
.msg--me{background:var(--accent); color:var(--accent-ink); justify-self:end; border-bottom-right-radius:.1rem}
.chat__opts{display:flex; flex-wrap:wrap; gap:.4rem; padding:0 .9rem .9rem}
.chip{background:transparent; border:1.5px solid var(--rule); color:var(--ink); border-radius:1.25rem; padding:.4rem .75rem; font-size:.8125rem; font-family:var(--f-body); cursor:pointer; min-height:2rem}
.chip:hover{border-color:var(--accent); color:var(--accent)}
.chat__ft{padding:.6rem .9rem .75rem; border-top:1px solid var(--rule); font-size:.6875rem; color:var(--ink-3); font-family:var(--f-mono)}
[hidden]{display:none!important}
@media (prefers-reduced-motion:reduce){ *{animation:none!important; transition:none!important} }


/* ══════════════════════════════════════════════════════════════════════════
   ⭐ THE PROFESSIONAL PASS — 11 Sep 2026.
   Measured against the reference client rebuild the owner holds this bar to,
   this demo was missing five things, and not one of them was a photograph:
     1. no brand MARK — the header carried a stacked logo IMAGE that needed a
        dark plate painted behind it just to survive the light theme
     2. no single decisive accent — the page borrowed six telltale colours
     3. no offer strip, so nothing said what the customer actually GETS
     4. the headline colour was the generic --accent, not a brand signal
     5. the primary button was the same green/blue as every chip on the page
   ⛔ Do NOT close this gap with a photograph or an AI image. These sites are
   drawn, not photographed, and that is the demo's honesty, not a limitation.

   ⚠️ THE GROUND IS BOTH. This site is dark-first and ALSO ships a light theme
   (prefers-color-scheme AND [data-theme="light"]). So the accent is declared
   as THREE values, not one: the brand hex, a lifted end for text on the dark
   ground, and a darker end for text on the light ground. --signal-text is the
   one that flips; everything that renders as TYPE uses it. The button fill is
   constant in both themes because it carries its own ink.
   ══════════════════════════════════════════════════════════════════════════ */
:root{
  --signal:#12A8A8;           /* the accent, as specified */
  --signal-deep:#0A6A6A;        /* darker end — accent TYPE on the light ground */
  --signal-lift:#35D6D6;        /* lighter end — accent TYPE on the dark ground */
  --signal-fill:#12A8A8;        /* the primary button */
  --signal-ink:#062A2A;          /* type ON the primary button */
  --signal-text:var(--signal-lift);
}
@media (prefers-color-scheme:light){
  :root:not([data-theme="dark"]){ --signal-text:var(--signal-deep); }
}
:root[data-theme="light"]{ --signal-text:var(--signal-deep); }

/* ── the mark ─────────────────────────────────────────────────────────── */
/* ⭐ Inline, in currentColor, so it inherits the theme instead of needing a
   plate painted behind it the way the old lockup image did. */
.brand{gap:.65rem}
.brand__mark{width:1.625rem; height:1.625rem; flex:none; display:block;
  color:var(--signal-text);
  transition:transform .18s cubic-bezier(.3,1.2,.5,1), color .18s ease}
.brand:hover .brand__mark{transform:translateY(-1px)}
.brand__txt{display:flex; flex-direction:column; gap:.1rem; min-width:0}
/* 🔴 A TEXT WORDMARK IS WIDER THAN THE 132px LOCKUP IT REPLACED, and .top__in is
   a WRAPPING flex row — so the phone and the button dropped to a second row at
   widths where they used to sit beside the nav. Measured, not guessed: at a
   1100px viewport the brand went 132px → 277px against a 267px budget, and
   .top__act moved from y=80 to y=125. ⛔ Never swap a logo for type in a
   wrapping header without re-measuring the row: nothing about it looks broken,
   it just quietly costs a row of height on every page.
   ⭐ The SECOND line is the widest part of the block, not the name — so it is
   the first thing to go, and the trade and city it repeats are both printed in
   the hero eyebrow a couple of inches below it. */
.brand{gap:.55rem}
.brand__nm{font-size:.9375rem}
.brand__sub{font-size:.5625rem; letter-spacing:.1em}
/* ⚠️ MEASURED AT 1024px, NOT GUESSED. With the second line already dropped, the
   header still needed 963px of a 942px content box on the longest of the four
   names — a 21px overrun that costs a whole extra row. The 35px comes off in
   four places rather than one so no single element gets conspicuously small:
   the row gap, the brand gap, the mark, and the wordmark. Below 60rem the nav
   collapses to the burger and none of this is load-bearing any more. */
@media (max-width:75rem){
  .top__in{gap:.875rem}
  .brand{gap:.45rem}
  .brand__mark{width:1.45rem; height:1.45rem}
  .brand__sub{display:none}
  .brand__nm{font-size:.8125rem}
}

/* ── the headline carries the colour ──────────────────────────────────── */
/* ⚠️ <em> so it still reads as emphasis with styles off; the slant comes off
   because the display face already leans on weight, not italics. */
h1 .hl{font-style:normal; color:var(--signal-text)}
.hero h1 u{color:var(--signal-text)}   /* the inner pages use <u>; same colour */

/* ── the offer strip ──────────────────────────────────────────────────── */
/* ⭐ The reference site's coupon bar, in this trade's own words, and every word of it is
   taken from copy already published elsewhere on this site.
   ⛔ It must never carry a price, a rating or a review count. */
.offerbar{background:var(--surface-2); border-bottom:1px solid var(--rule)}
.offerbar__in{display:flex; align-items:center; gap:.85rem; flex-wrap:wrap;
  padding-block:.55rem}
.offerbar__tag{flex:0 0 auto; font-family:var(--f-mono); font-size:.625rem;
  font-weight:700; letter-spacing:.14em; text-transform:uppercase;
  color:var(--signal-text); border-radius:999px; padding:.18rem .55rem;
  border:1px solid color-mix(in srgb, var(--signal-text) 42%, transparent)}
.offerbar__msg{margin:0; flex:1 1 22rem; min-width:0; font-size:.8125rem;
  line-height:1.45; color:var(--ink-2)}
.offerbar__msg b{color:var(--ink); font-weight:600}
.offerbar__go{flex:0 0 auto; font-family:var(--f-mono); font-size:.6875rem;
  font-weight:700; letter-spacing:.09em; text-transform:uppercase;
  color:var(--ink); text-decoration:none; white-space:nowrap; padding-bottom:2px;
  border-bottom:1px solid color-mix(in srgb, var(--signal-text) 60%, transparent)}
.offerbar__go:hover{color:var(--signal-text)}
@media (max-width:40rem){
  .offerbar__go{display:none}          /* the hero button is a screen below it */
  .offerbar__msg{font-size:.78125rem}
}

/* ── the primary button is the signal, not the generic accent ─────────── */
.btn--go{background:var(--signal-fill); color:var(--signal-ink);
  box-shadow:0 1px 0 color-mix(in srgb, #000 18%, transparent) inset,
             0 8px 22px -14px color-mix(in srgb, var(--signal-fill) 90%, #000)}
.btn--go:hover{background:color-mix(in srgb, var(--signal-fill) 86%, #fff);
  color:var(--signal-ink)}

/* 🔴 AND THE GHOST HAS TO BE RESTATED AFTER IT. `.btn--line` is declared ~500
   lines above. This stylesheet is unlayered, so a later rule of EQUAL
   specificity wins — the appended `.btn--go` block above is why this one has
   to exist below it rather than being left where it was. Two primaries is no
   primary. ⛔ Any future .btn rule appended past this point must restate the
   ghost again. */
.btn--line{background:transparent; border-color:var(--rule); color:var(--ink);
  box-shadow:none}
.btn--line:hover{background:var(--surface-2); border-color:var(--signal-text);
  color:var(--ink)}

/* ── depth on the drawn hero ──────────────────────────────────────────── */
/* ⭐ What separates the reference site's instrument from a flat vector is one light and a
   real shadow. These heroes had neither, so the art sat ON the page instead
   of above it. */
.hero__fig img{filter:drop-shadow(0 26px 44px rgba(0,0,0,.45)) contrast(1.05) saturate(1.04)}
.hero__fig .wr{box-shadow:0 1px 2px rgba(0,0,0,.32), 0 30px 56px -22px rgba(0,0,0,.72);
  filter:contrast(1.02)}
@media (prefers-color-scheme:light){
  :root:not([data-theme="dark"]) .hero__fig img{filter:drop-shadow(0 22px 36px rgba(19,26,33,.28)) contrast(1.04) saturate(1.03)}
  :root:not([data-theme="dark"]) .hero__fig .wr{box-shadow:0 1px 2px rgba(19,26,33,.08), 0 26px 48px -20px rgba(19,26,33,.34)}
}
:root[data-theme="light"] .hero__fig img{filter:drop-shadow(0 22px 36px rgba(19,26,33,.28)) contrast(1.04) saturate(1.03)}
:root[data-theme="light"] .hero__fig .wr{box-shadow:0 1px 2px rgba(19,26,33,.08), 0 26px 48px -20px rgba(19,26,33,.34)}
@media (prefers-reduced-motion:reduce){ .brand:hover .brand__mark{transform:none} }


/* ── 🗓 THE DAY, AS THE CUSTOMER HAS IT — 11 Sep 2026 ─────────────────────
   ⭐ THE HERO ABOVE STATES WHAT THIS COMPANY DOCUMENTS. That is our fact, not
   the customer's. What a person actually buys from a trade is the day they do
   not lose — so the headline names the trade and the town, the accent carries
   the hook, and this section walks the day beat by beat in THEIR time.
   ⛔ Every beat is something this company could really do. Nothing here claims
   a booking is held, confirmed or reserved, no price appears that is not
   already published elsewhere on this site, and ONE BEAT IS A REFUSAL — the
   beat a real customer repeats to somebody else.
   ⭐ It borrows the page's own tokens, so it is this trade's accent and this
   trade's rules. ⛔ Do not paste another demo's colour in here. */
.sec--day .day{list-style:none; margin:1.75rem 0 0; padding:0; display:grid; gap:0}
.day__i{display:grid; grid-template-columns:5.5rem minmax(0,1fr); gap:0 1.25rem;
  padding:1.15rem 0; border-top:1px solid var(--rule); align-items:start}
.day__i:first-child{border-top:0}
/* 🔴 THE THREE CHILDREN MUST BE PLACED, NOT AUTO-FLOWED. A grid with two columns
   and three children puts the THIRD one back in column 1 — so the paragraph lands
   under the timestamp and wraps at about ten characters, breaking words in half.
   It looks like a rendering fault and it is only visible once you actually open
   the page: the markup is correct, the CSS is short, and every gate passes.
   ⛔ Never leave these three rules implicit. */
.day__t{grid-column:1; grid-row:1 / span 2;
  font-family:var(--f-mono); font-size:.8125rem; font-weight:500; letter-spacing:.06em;
  text-transform:uppercase; color:var(--signal-text); padding-top:.2rem; white-space:nowrap}
.day__i h3{grid-column:2; grid-row:1; margin:0 0 .3rem; font-size:1.0625rem; font-weight:700; line-height:1.25}
.day__i p{grid-column:2; grid-row:2; margin:0; font-size:.9375rem; line-height:1.55; color:var(--ink-2); max-width:62ch}
/* ⭐ ONE beat is marked, and it is the one where the promise in the HEADLINE
   actually happens. ⛔ Never the sale — the sale is not the customer's story. */
.day__i--live .day__t::after{content:""; display:inline-block; width:6px; height:6px;
  border-radius:50%; background:var(--signal-text); margin-left:6px; vertical-align:middle}
.day__foot{margin:1.5rem 0 0; padding-top:1.15rem; border-top:1px solid var(--rule);
  font-size:.9375rem; line-height:1.6; color:var(--ink-2); max-width:62ch}
.day__foot b{color:var(--ink)}
@media (max-width:560px){
  .day__i{grid-template-columns:1fr; gap:.35rem}
  /* ⛔ and release the explicit placement with it, or every beat stacks in one cell */
  .day__t{grid-column:1; grid-row:auto; padding-top:0}
  .day__i h3,.day__i p{grid-column:1; grid-row:auto}
}


/* ── 📷 PHOTOGRAPHS ───────────────────────────────────────────────────────
   🔴 11 Sep 2026. Owner: "need photos!!" and "no stick diagrams". This demo
   shipped with none, and a page of line drawings reads as a prototype rather
   than as a business's site.

   ⭐ PORTABLE ON PURPOSE. Thirteen demos, thirteen different token names —
   --soil/--panel/--hair here, --paper/--rule/--gr there, light and dark both.
   A component written against one demo's variables is a component that has to
   be rewritten twelve times and will be wrong in four of them. So this uses
   `currentColor` and plain rgba, and inherits the page's own ink.

   ⛔ Every CC BY / CC BY-SA frame carries its credit IN the frame. Attribution
   is a licence term, not a courtesy, and "we'll add the credits later" is
   exactly how a licence gets breached. */
.photo{position:relative;margin:0;line-height:0;border-radius:4px;overflow:hidden;
  border:1px solid rgba(128,128,128,.28)}
.photo img{width:100%;display:block;object-fit:cover;filter:saturate(1.03) contrast(1.02)}
.photo--hero img{height:clamp(260px,34vw,430px)}
.photo--band img{height:clamp(190px,26vw,320px)}
.photo--tall img{height:clamp(240px,30vw,380px)}
/* ⚠️ the badge carries its OWN scrim rather than the frame carrying a gradient.
   A gradient tuned for a dark ground turns a light photo muddy along the bottom,
   and half these demos are light. */
.photo__cr{position:absolute;right:8px;bottom:8px;z-index:2;
  font-size:9.5px;letter-spacing:.04em;line-height:1.45;font-weight:500;
  color:rgba(255,255,255,.88);background:rgba(0,0,0,.52);padding:3px 7px;border-radius:2px;
  text-shadow:0 1px 2px rgba(0,0,0,.5)}
.photo__cr a{color:inherit;text-decoration:underline;text-underline-offset:2px}
.photo__cap{line-height:1.55;font-size:12.5px;opacity:.72;padding-top:9px}

/* ── 🚪 THE WAY OUT STAYS ON SCREEN ───────────────────────────────────────
   🔴 11 Sep 2026, owner: "need to be able and x out of all the demo sites if
   you view on mobile dont let people get stuck somewher with out easy exit."

   This banner is the only route from a demo back to us, and it sat at the top
   of the document as a static block. On a phone, two swipes down a service page
   and it is gone — a visitor who arrived on a shared link has no way back and
   no reason to think one exists. ⚠️ The link was always there and was always
   unreachable, which is the same shape as the embed's close button being parked
   off-screen above the iframe.

   ⭐ Sticky on phones only. On a desktop the banner is in view at the top of a
   page people can see all of, and pinning it would just spend vertical room. */
@media (max-width: 640px) {
  .demostrip {
    position: sticky;
    top: 0;
    z-index: 90;
  }
  /* ⚠️ AND THE LINK HAS TO BE HITTABLE, NOT MERELY PRESENT. It is inline text
     inside a sentence, so on a 375px screen it wrapped into two fragments 15px
     tall — measured, both genuinely tappable, and both far under the 44px a
     thumb needs. ⭐ Make it a control: one line, real padding, no wrap. */
  .demostrip a {
    display: inline-block;
    margin-top: 4px;
    padding: 11px 12px;
    white-space: nowrap;
    font-weight: 700;
  }
}


/* 🔴 INSIDE OUR OWN FRAME THE BANNER IS DUPLICATE — 11 Sep 2026. Owner, looking
   at the embed on the home page: "take all of this off this is awfull they know
   that". He is right about that surface: our own chrome prints a disclosure
   directly under the frame, in our voice, so the demo's banner made two stacked
   notices — which reads as nervous rather than as honest.
   ⛔ A STANDALONE PAGE KEEPS IT. There the banner is the ONLY notice, and these
   pages carry a live telephone line: an invented business with a working number
   and nothing saying so is a trap, not a demo.
   ⚠️ Fails in the honest direction — with JavaScript off, nothing is hidden.
   ⛔ If the note under the frame is ever removed, delete this rule first. */
html[data-embedded] .demostrip { display: none !important; }

/* 🔴 HERO WITHOUT THE REFERENCE RAIL — 13 Sep 2026, owner: "still have the side
   menu on hero take these off". .bay is `var(--rail) 1fr`, so removing the aside
   alone would leave the first column empty and the copy indented into column 2.
   ⛔ The rail stays on INNER pages — this collapses the grid on the home hero
   only, which is the one the botsonstaff hero photographs. */
.bay--solo{grid-template-columns:minmax(0,1fr)}


/* ── 🧭 THE UTILITY BAR — 15 Sep 2026 ─────────────────────────────────
   Written by tools/demo-utilbar.mjs. See that file for why the disclosure
   banner became a chip and why hours are absent on the demos that publish none.
   ⛔ Self-contained colour on purpose: every demo means something different by
   --ink, and borrowing it painted this a different thing on each site. */
.utilbar{background:#0f1b2a;color:rgba(255,255,255,.92);
  border-bottom:1px solid rgba(255,255,255,.14);font-size:.78rem;line-height:1.4;
  font-family:system-ui,-apple-system,"Segoe UI",sans-serif}
.utilbar__in{display:flex;align-items:center;justify-content:space-between;
  gap:.9rem;min-height:38px;padding:.3rem 20px;flex-wrap:wrap;
  max-width:1180px;margin:0 auto}
.utilbar__hours{margin:0;display:flex;align-items:center;gap:.45rem;min-width:0}
.utilbar__hours b{font-weight:600;color:#fff}
.utilbar__sep{opacity:.45}
.utilbar__hours>span:last-child{opacity:.72}
.utilbar__right{display:flex;align-items:center;gap:.9rem;flex-wrap:wrap;min-width:0}
.utilbar__lnk{color:rgba(255,255,255,.88);text-decoration:none;
  border-bottom:1px solid rgba(255,255,255,.26);padding-bottom:1px}
.utilbar__lnk:hover{color:#fff;border-bottom-color:#fff}
/* ⭐ colour is never the only carrier — the words say Open or Closed outright */
.utilbar__dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto;background:#7fe0a8}
.utilbar__hours[data-state="open"] .utilbar__dot{background:#7fe0a8;animation:utilpulse 3s ease-out infinite}
.utilbar__hours[data-state="soon"] .utilbar__dot{background:#ffc861}
.utilbar__hours[data-state="shut"] .utilbar__dot{background:#9aa6b1}
@keyframes utilpulse{0%{box-shadow:0 0 0 0 rgba(127,224,168,.5)}
  70%{box-shadow:0 0 0 6px rgba(127,224,168,0)}100%{box-shadow:0 0 0 0 rgba(127,224,168,0)}}
@media (prefers-reduced-motion:reduce){.utilbar__dot{animation:none !important}}
.utilbar .demostrip{display:inline-flex;align-items:center;gap:.4rem;
  padding:.16rem .55rem;border-radius:999px;background:rgba(255,255,255,.10);
  border:1px solid rgba(255,255,255,.22);font-size:.72rem;color:#fff;
  text-decoration:none;cursor:pointer}
.utilbar .demostrip:hover{background:rgba(255,255,255,.18);border-color:rgba(255,255,255,.38)}
.utilbar .demostrip b{text-transform:uppercase;letter-spacing:.08em;font-size:.68rem;font-weight:700}
.utilbar .demostrip__dot{width:7px;height:7px;border-radius:50%;background:#ff6a55;display:block}
@media (max-width:860px){
  /* ⛔ the TAIL hides, never the notice — and the chip becomes the link to it */
  .utilbar__demotail{display:none}
  .utilbar__in{gap:.55rem}
  .utilbar__right{gap:.7rem}
  .utilbar .demostrip{min-height:30px}
}
@media (max-width:560px){
  .utilbar{font-size:.72rem}
  .utilbar__hours>span:last-child,.utilbar__sep{display:none}
  .utilbar .demostrip{min-height:34px;padding-inline:.7rem}
}
/* ⛔ Inside our own iframe the surrounding chrome carries the disclosure, so the
   chip is hidden there and ONLY there. A standalone page always keeps it. */
html[data-embedded] .utilbar .demostrip{display:none !important}

/* 🔴 NO STATUS LIGHT WITHOUT A STATUS — 15 Sep 2026. Six of these demos publish
   no hours at all, and one publishes "by appointment". The dot is a status
   light: green, beside a company name with nothing behind it, it silently says
   OPEN NOW about a business whose hours nobody ever wrote. It only appears once
   the script has decided a real state from real published hours.
   ⚠️ This shipped for one sync because the fix was written into a script that
   died on an assertion BEFORE the file was saved — the edit was reported and
   never landed. Verify the artefact, not the log line. */
.utilbar__hours:not([data-state]) .utilbar__dot{display:none}


/* ── 🖼️ THE PHOTOGRAPH IS THE HERO — 15 Sep 2026 ─────────────────────────
   Written by tools/demo-photo-hero.mjs; the reasoning lives there. */
.lanaihero{position:relative;isolation:isolate;display:flex;flex-direction:column;justify-content:center;
  min-height:clamp(330px,62vh,600px);overflow:hidden;background:#07182a}
.lanaihero>img{position:absolute;inset:0;z-index:0;width:100%;height:100%;
  object-fit:cover;object-position:center 58%}
/* 🔴 TWO GRADIENTS, BOTH LOAD-BEARING. The horizontal one carries the headline;
   the vertical one holds the eyebrow and the buttons, which sit where the
   horizontal scrim has already thinned out. */
.lanaihero__scrim{position:absolute;inset:0;z-index:1;
  background:
    linear-gradient(90deg, rgba(5,18,32,.93) 0%, rgba(5,18,32,.86) 38%,
                           rgba(5,18,32,.46) 70%, rgba(5,18,32,.24) 100%),
    linear-gradient(0deg,  rgba(5,18,32,.42) 0%, rgba(5,18,32,.30) 50%,
                           rgba(5,18,32,.14) 100%)}
/* ⛔ padding-block, never the shorthand — a `padding: X 0 Y` here wipes the
   horizontal gutter the wrap class contributes and every line goes flush to the
   edge of the screen. That shipped once. */
.lanaihero__in{position:relative;z-index:2;
  padding-block:clamp(2rem,5vw,3.4rem) clamp(1.9rem,4.5vw,3rem)}
.lanaihero h1{color:#fff;max-width:20ch;text-wrap:balance}
.lanaihero .h1__where{color:#fff}
.lanaihero .hl{color:rgba(255,255,255,.74)}
.lanaihero .eyebrow,.lanaihero .lbl,.lanaihero .kicker{color:rgba(255,255,255,.88)}
.lanaihero .btnrow,.lanaihero .hero__act{margin-top:1.4rem;display:flex;flex-wrap:wrap;gap:.7rem}
/* ⛔ A ghost/outline button is a hairline on paper; over a photograph it
   disappears. WCAG 2.2 asks 3:1 for the control's own boundary, not just its
   label. */
.lanaihero .btn--ghost,.lanaihero .btn--line,.lanaihero .btn--out{
  border-color:rgba(255,255,255,.62);color:#fff;background:rgba(7,22,38,.42);
  -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}
.lanaihero .btn--ghost:hover,.lanaihero .btn--line:hover,.lanaihero .btn--out:hover{
  background:rgba(7,22,38,.66);border-color:#fff}
.lanaihero .photo__cr{position:absolute;right:10px;bottom:9px;z-index:3;
  font-size:10px;color:rgba(255,255,255,.8);background:rgba(0,0,0,.5);
  padding:3px 7px;border-radius:3px}
.lanaihero .photo__cr a{color:inherit}
@media (max-width:640px){
  /* ⚠️ At phone width the headline spans the full column, so a left-weighted
     gradient leaves its right-hand words over bright sky. Mostly vertical here. */
  .lanaihero{min-height:clamp(300px,52vh,420px)}
  .lanaihero__scrim{background:
    linear-gradient(0deg, rgba(5,18,32,.80) 0%, rgba(5,18,32,.72) 50%,
                          rgba(5,18,32,.55) 100%),
    linear-gradient(90deg, rgba(5,18,32,.55) 0%, rgba(5,18,32,.30) 100%)}
  .lanaihero h1{max-width:none}
}


/* 🔴 THE HERO'S MEASURE, MATCHED TO THE PAGE — 15 Sep 2026.
   The transform hardcoded `class="wrap"` as the gutter and on this demo `wrap`
   contributes none, so the headline sat at x=0 while the body copy below it
   started at 62. Switching to `shell` overshot to 418, because `shell` is a
   GRID here and each hero child became a cell.
   ⭐ So the measure is stated outright rather than borrowed: the same 1248/48
   the shell resolves to, which tracks the page at every width instead of at
   the one I happened to measure. */
/* 🔴 `margin-inline:auto` IS NOT CENTRING INSIDE A FLEX COLUMN — it absorbs the
   free space and stops the item stretching, so this block shrank to its own
   content (537px) and sat at x=418 while the body copy below started at 62.
   It reads as a gutter bug and is a flexbox rule. ⭐ Cap the width and centre
   it explicitly instead; `min()` keeps it honest at narrow widths too. */
.lanaihero__in{width:min(100%,1248px);align-self:center;padding-inline:48px}
@media (max-width:640px){ .lanaihero__in{padding-inline:20px} }




/* 🔴 THE HERO BUTTONS — 15 Sep 2026. Owner: "make sure the buttons are spread
   out and even across heros", "not sqished into the tiltles".
   Measured at 1280 before touching anything: rv-powersports had its buttons
   touching the headline at a gap of ZERO, because its row is `.hero__cta` and
   the rule only named `.btnrow` and `.hero__act`. The rest sat at 22px with an
   11px gap between them and heights running 40 to 53.
   ⭐ One rule, all three row names, and a floor under the height so a row of
   buttons reads as a row rather than as two different controls. */
.lanaihero .btnrow,
.lanaihero .hero__act,
.lanaihero .hero__cta{
  margin-top:clamp(1.6rem,2.6vw,2.2rem);
  display:flex; flex-wrap:wrap; align-items:center;
  gap:.85rem;
}
.lanaihero .btn{
  min-height:48px;
  display:inline-flex; align-items:center; justify-content:center;
  padding-inline:1.25rem;
}
@media (max-width:560px){
  /* ⚠️ Full width and stacked on a phone — two half-width buttons side by side
     put both under the 48px the thumb needs in the direction that matters. */
  .lanaihero .btnrow,.lanaihero .hero__act,.lanaihero .hero__cta{gap:.65rem}
  .lanaihero .btn{width:100%}
}


/* ── 👥 THE SOCIAL MARKS — 15 Sep 2026. Owner: "put fake no link social on the
   headers", after twice being told there is nowhere honest to point them.
   ⛔ THEY ARE NOT LINKS, AND THAT IS THE POINT. These companies are invented,
   so a social icon could only lead to an invented profile, to somebody else's
   real account, or to nothing — and a control that goes nowhere is worse than
   no control. They are `<span>` marks, `aria-hidden`, not focusable and not
   clickable: an illustration of where a real client's profiles sit in the bar,
   on a page that already says it is a demonstration.
   ⛔ Do not turn these into <a> on a real build without real profiles behind
   them. Bots On Staff's own Facebook and Instagram have never published once. */
.utilbar__social{display:inline-flex;align-items:center;gap:.55rem;opacity:.62}
.utilbar__social svg{width:15px;height:15px;display:block}
@media (max-width:560px){ .utilbar__social{display:none} }


/* 🔴 THE GHOST BUTTON, RESTATED BELOW `.btn` — 15 Sep 2026.
   The polish gate refused four demos for this and it was right: adding a
   `.lanaihero .btn` block after the ghost rules puts the outline variant at the
   mercy of source order in unlayered CSS. It happens that the block above sets
   no background, so nothing rendered wrong — but "it happens to be fine" is not
   a property anyone can rely on while editing this file later.
   ⛔ Any new `.btn` rule goes ABOVE this, or this moves below it. */
.lanaihero .btn--ghost,
.lanaihero .btn--line,
.lanaihero .btn--out{
  border-color:rgba(255,255,255,.62);
  color:#fff;
  background:rgba(7,22,38,.42);
  -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px);
}
.lanaihero .btn--ghost:hover,
.lanaihero .btn--line:hover,
.lanaihero .btn--out:hover{background:rgba(7,22,38,.66);border-color:#fff}

.lanaihero .btn--ghost{
  border-color:rgba(255,255,255,.62);
  color:#fff;
  background:rgba(7,22,38,.42);
}


/* ── 📣 THE OFFER BAND, ON THE PHOTOGRAPH — 15 Sep 2026 ──────────────────────
   Owner: "i dont think it needs the white section evry vist", then "figure it
   out in a great spot dont just throw it somewhere be creative".
   🔴 IT CANNOT SIMPLY GO. The polish gate requires an offer strip on every page
   and it is right to — it is the one line on a demonstration site that says what
   the shop actually promises. But as a white slab wedged between the header and
   the hero it interrupted the only picture on the page before anyone had seen it.
   ⭐ So it is pinned to the FOOT of the hero, over the photograph, on the same
   dark glass as the scrim. The promise now reads as part of the image instead of
   as an announcement above it, and the page opens header -> photograph -> story
   with nothing cutting across.
   ⛔ Its own backdrop is near-opaque on purpose: type over a photograph is the
   one thing on this estate that has been measured wrong twice, and a band that
   carries its own ground cannot drift when the picture changes. */
.lanaihero{padding-bottom:0}
.lanaihero .offerbar{
  position:absolute; left:0; right:0; bottom:0; z-index:3;
  background:rgba(6,19,34,.86);
  -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px);
  border-top:1px solid rgba(255,255,255,.16);
  border-bottom:0;
}
.lanaihero .offerbar__in{
  display:flex; align-items:center; gap:1rem; flex-wrap:wrap;
  padding-block:.7rem;
}
.lanaihero .offerbar__tag{
  color:#fff; border-color:rgba(255,255,255,.45); background:rgba(255,255,255,.10);
}
.lanaihero .offerbar__msg{color:rgba(255,255,255,.92); margin:0}
.lanaihero .offerbar__msg b{color:#fff}
.lanaihero .offerbar__go{color:#fff; border-color:rgba(255,255,255,.5)}
/* ⚠️ the hero's own content must clear the band, or the buttons sit under it */
.lanaihero__in{padding-bottom:clamp(5.5rem,9vw,7rem)}
@media (max-width:860px){
  /* ⛔ On a phone the band would eat the picture. It goes back into the flow
     directly beneath the hero — still out from between the header and the photo,
     which is the thing that was wrong. */
  .lanaihero .offerbar{position:static;backdrop-filter:none;-webkit-backdrop-filter:none}
  .lanaihero__in{padding-bottom:clamp(1.9rem,4.5vw,3rem)}
}


/* ══ 🧭 THE HEADER, 2026 — 15 Sep 2026 ══════════════════════════════════════
   Owner: "the header need to be way better on each site look like proper 2026
   header trends", "make sure everyting looks as goos as outr customer sites".
   🔴 MEASURED FIRST, ALL TWELVE AT 1280: heights ran from 62px to 223px, only
   one was sticky, and eleven carried a telephone number that the new utility bar
   was already showing a few pixels above it.
   ⭐ This layer is keyed on the `header` ELEMENT, not on a class, because these
   demos call it .site, .top, .mast, .nav and .masthead — five names for one
   thing. Anything that needs a class name is done per demo.
   ⛔ It is deliberately visual-only: sticky, glass, a hairline that appears on
   scroll, and honest focus rings. It does not re-order anybody's markup. */
header{
  position:sticky; top:0; z-index:60;
  -webkit-backdrop-filter:saturate(1.3) blur(10px);
  backdrop-filter:saturate(1.3) blur(10px);
  transition:box-shadow .22s ease, border-color .22s ease;
}
/* ⭐ The rule under the header only appears once you have scrolled — a border
   that is always there is chrome, a border that arrives is depth. */
header::after{
  content:""; position:absolute; left:0; right:0; bottom:-1px; height:1px;
  background:currentColor; opacity:0; transition:opacity .22s ease;
  pointer-events:none;
}
html[data-scrolled] header::after{opacity:.16}
html[data-scrolled] header{box-shadow:0 10px 30px -22px rgba(0,0,0,.55)}

/* ⛔ 44px is not decoration. WCAG 2.2 §2.5.8, and on a phone this is the only
   navigation there is. */
@media (max-width:900px){
  header nav a{min-height:44px;display:flex;align-items:center}
}
/* a focus ring that is visible on both a light and a dark header */
header a:focus-visible, header button:focus-visible, header label:focus-visible{
  outline:2px solid currentColor; outline-offset:3px; border-radius:6px;
}


/* 🔴 STICKY WAS LOSING ON SPECIFICITY — 15 Sep 2026. `header{position:sticky}`
   is (0,0,1) and these demos set `.top{position:relative}` at (0,1,0), so five
   of twelve silently stayed put while the other seven pinned. Measured, not
   assumed: auto-detailing, drain-cleaning, mobile-repair, plumbing-hvac and
   rv-powersports all read `relative` after the layer went in.
   ⭐ `header[class]` is (0,1,1) and beats a single class without reaching for
   !important, which would then have to be fought later. */
header[class]{position:sticky;top:0;z-index:60}

/* 🔴 THE OFF-CANVAS MENU WAS WIDENING THE PAGE. Parked at translateX(102%) the
   panel still counted toward scrollWidth — 700px against a 371px viewport — so
   the whole site scrolled sideways on a phone with nothing visible to scroll to.
   ⛔ `overflow-x:hidden` would fix it and break `position:sticky` in the same
   stroke; `clip` does not create a scroll container, so the header still pins. */
html{overflow-x:clip}


/* 🔴 THE NAV WAS WRAPPING ONTO ITS OWN ROW — 15 Sep 2026, measured at 1280.
   These headers are flex rows that ran out of width, so the navigation dropped
   beneath the brand and the header doubled in height: 134, 138, 104, 223.
   A sticky header that tall eats a quarter of a laptop screen, and it is the
   clearest difference between these and the client sites.
   ⭐ One row, and the NAV is the part allowed to give — `min-width:0` lets it
   shrink, and if it still will not fit it scrolls sideways within itself rather
   than pushing the whole header down. ⛔ `scrollbar-width:none` hides the bar,
   not the ability to scroll; the items remain reachable by wheel, trackpad,
   touch and keyboard. */
@media (min-width:901px){
  header .top__in,
  header .masthead__row,
  header .brandline{flex-wrap:nowrap}
  header nav{min-width:0}
}
@media (min-width:901px){
  header nav ul{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}
  header nav ul::-webkit-scrollbar{display:none}
  header nav a{white-space:nowrap}
}


/* 🔴 ONE HERO WAS 83vh BECAUSE ITS HEADLINE WAS 104px — 15 Sep 2026.
   Owner: "make sure desktop hero are proper sixe". Measured at 1280: nine
   heroes sat at 62-66vh and auto-detailing at 83vh, and the cause was not the
   hero box at all — its <h1> renders at 104px over three lines, 306px of type.
   That size was tuned for a hero made ONLY of words; over a photograph it is
   shouting. ⭐ Capped to the size the rest already use, so the set reads as one
   family. ⛔ Still large text for contrast purposes (>=24px), so the 3:1
   threshold the probe applies is unchanged. */
.lanaihero h1{font-size:clamp(2rem,4.4vw,3.5rem);line-height:1.06}


/* ============================================================
   HAND-EDITED, 23 SEP 2026 - AND DELIBERATELY DIVERGED
   ------------------------------------------------------------
   This demo used to be generated outside this repository and copied in.
   IT MUST NOT BE REGENERATED. The generator sources are untracked and
   unbacked and at least one provably does not reproduce its own published
   copy - a rebuild loses photo bands, fonts and the town. public/demo is in
   git, so THIS file is the source of truth for this site now. Do not run
   tools/sync-demo.mjs or any build.py against it.

   WHAT CHANGED
   1. Two stacked heroes became one; the second hero's 28-word lede is gone.
   2. THE REFUSAL BECAME THE LOUDEST THING ON THE PAGE. There is no $0.00
      here and there must never be: a water report has no money column, and
      putting a price into a table of ppm readings would be inventing a
      document. The refusal is that NOTHING WAS ADDED, because stabilizer
      read 78 ppm against a 30-50 range and chlorine stops holding above it.
      The verdict is a single word, ruled above and below, and it is the
      largest thing on the card.
   3. The offer strip left .lanaihero, where the absolutely-positioned hero
      photograph was painting over it at every width.

   AND IT IS NOT pressure-washing. The two sites were about 87% identical.
   They are not being merged - thirteen sites sharing one layout is the exact
   thing these demos exist to disprove - so the refusals now have opposite
   shapes: this one is a CENTRED STAMP and it CLOSES the document, because a
   water report ends with what was added. pressure-washing's OPENS its card,
   because a wash card starts with what was asked for.
   ============================================================ */

.lanaihero h1{font-size:clamp(2.6rem,1.4rem + 5.4vw,4.5rem);
  font-weight:800;line-height:1.03;letter-spacing:-.03em;margin:0 0 .2em}
/* Over a photograph there is no secondary text: full ink and a shadow that
   travels with the glyphs. A muted grey is hierarchy on a flat panel; on a
   picture it is just an unreadable foreground. */
.lanaihero .hl{color:rgba(255,255,255,.94)}
.lanaihero :is(h1,.verdict__k,.verdict__v,.verdict__why){
  text-shadow:0 1px 2px rgba(0,0,0,.70), 0 2px 20px rgba(0,0,0,.52)}

/* THE VERDICT - the hero's short form of the refusal. Centred, because it is a
   verdict rather than a line item. */
.verdict{margin:1.4rem 0 0;padding:.9rem 0;max-width:32rem;text-align:center;
  border-top:3px solid rgba(255,255,255,.66);
  border-bottom:3px solid rgba(255,255,255,.66)}
.verdict__k{margin:0 0 .25rem;font-family:var(--f-mono);font-size:.6875rem;
  letter-spacing:.22em;text-transform:uppercase;color:#fff}
.verdict__v{margin:0;font-family:var(--f-display);
  font-size:clamp(2rem,1.1rem + 3vw,2.9rem);font-weight:800;line-height:1;
  letter-spacing:-.03em;color:#fff}
.verdict__why{margin:.45rem 0 0;font-size:.8125rem;line-height:1.45;color:#fff;
  max-width:36ch;margin-inline:auto}

/* THE VERDICT BAND inside the report itself, where it is the last thing read
   and the largest thing on the card. */
.wr__verdict{padding:1.1rem 1rem 1.2rem;text-align:center;
  border-top:3px solid var(--ink);border-bottom:3px solid var(--ink);
  background:var(--surface-2)}
.wr__verdict-k{margin:0 0 .3rem;font-size:.625rem;letter-spacing:.18em;
  text-transform:uppercase;color:var(--ink-3)}
.wr__verdict-v{margin:0;font-family:var(--f-display);
  font-size:clamp(2.4rem,1.2rem + 4vw,3.6rem);font-weight:800;line-height:1;
  letter-spacing:-.035em;color:var(--ink)}
.wr__verdict-why{margin:.6rem auto 0;font-family:var(--f-body);
  font-size:.875rem;line-height:1.55;color:var(--ink-2);max-width:58ch}
.wr{max-width:46rem;margin-inline:auto}


/* ============================================================
   THE FULL-PICTURE HERO - 23 Sep 2026
   🔴 MEASURED FIRST. The old hero composited to band luma 39.0 against a
   floor of 60 and picture side 61.3 against 80 - and the photograph under it
   was lane-rope buoys on open water while the alt text claimed "a residential
   swimming pool behind a mesh safety fence". The picture was abstract, the
   alt was false, and the scrim was doing the rest of the damage.
   ⭐ It stands on ph-weekly-service.jpg now - a rectangular residential pool
   and deck behind a modern house, 140 / 167 - which is what a person buying
   weekly pool service is actually buying.

   COMPOSITION: type at the TOP of the frame, so the pool itself runs out
   underneath the words and fills the bottom two thirds of the fold. The scrim
   is therefore pooled at the TOP, the opposite way round to the other three.
   ⛔ The last stop is .02 and that number is the water.
   ⚠️ 90deg/0deg and % stops only - tools/hero-photo-contrast.js parses this.
   ============================================================ */
.lanaihero{justify-content:flex-start; min-height:clamp(440px,74vh,700px)}
.lanaihero>img{object-position:center 58%}
.lanaihero__in{padding-block:clamp(1.6rem,4vw,2.6rem) clamp(1.4rem,3vw,2rem)}
.lanaihero__scrim{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(0deg,  rgba(3,26,34,.02) 0%,  rgba(3,26,34,.06) 34%,
                           rgba(3,26,34,.34) 62%, rgba(3,26,34,.72) 84%,
                           rgba(3,26,34,.84) 100%),
    linear-gradient(90deg, rgba(3,26,34,.46) 0%,  rgba(3,26,34,.30) 40%,
                           rgba(3,26,34,.10) 72%, rgba(3,26,34,.02) 100%)}
@media (max-width:640px){
  /* ⭐ TALLER ON A PHONE, ON PURPOSE. At 390px the type block is about 330px
     of a 540px hero, so the pool it sits on covered almost the whole frame and
     the photograph stopped being one. Giving the hero another 120px of height
     does not change the words at all and hands the bottom third back to the
     water - which is the only reason the photograph is there. */
  .lanaihero{min-height:clamp(470px,78vh,660px)}
  /* This hero anchors its type at the TOP, so the pool belongs at the top on
     a phone as well. The first version of this branch pooled at the foot -
     the mirror image of where the words are - and the refusal's reason came
     back at 2.14 against a floor of 4.5 while the luma passed comfortably.
     A luma pass and a contrast fail on the same frame means the darkness is
     in the wrong PLACE, not the wrong amount. */
  .lanaihero__scrim{background:
    linear-gradient(0deg,  rgba(3,26,34,.04) 0%,  rgba(3,26,34,.10) 22%,
                           rgba(3,26,34,.58) 42%, rgba(3,26,34,.86) 70%,
                           rgba(3,26,34,.92) 100%),
    linear-gradient(90deg, rgba(3,26,34,.20) 0%,  rgba(3,26,34,.06) 100%)}
}


/* ============================================================
   THE MARK — A TWO-TONE PICTORIAL DISC, FILLS ONLY. 23 Sep 2026.
   Owner: "the logos can be way more creative".
   🔴 Every demo on this estate wore one idea: a 28x28 viewBox, monoline
   strokes in currentColor, no fill, no wordmark treatment — thirteen trades
   in one mark, recoloured. This one has NO STROKE ANYWHERE. It is a disc at
   22% and a waterline at full, which is a different kind of object from a
   line drawing, not a different line drawing.
   ⭐ Its three sisters in this group are deliberately other kinds again:
   auto-repair a knocked-out monogram tile, drain-cleaning a bare wordmark on
   a grade rule, pressure-washing an enclosed badge.
   ⛔ Do not harmonise them. Four companies, four designers.
   ============================================================ */
.brand--disc{align-items:center}
.brand--disc .brand__mark{width:2rem;height:2rem;border-radius:50%;
  color:var(--accent);
  transition:transform .2s cubic-bezier(.3,1.2,.5,1)}
.brand:hover .brand__mark{transform:translateY(-1px) rotate(-4deg)}
@media (max-width:30rem){ .brand--disc .brand__mark{width:1.7rem;height:1.7rem} }


/* ============================================================
   GLASS CONTROLS — 23 Sep 2026. Owner: "use glass buttons all the newest
   elements".
   🔴 A glass button on a photographic hero has its label contrast decided by
   the PHOTOGRAPH, and this hero is the brightest of the four — a pool at 141
   mean, 169 on the picture side. ⛔ The base declaration carries the
   contrast with NO blur; the @supports block adds blur and saturation and
   changes no alpha. backdrop-filter is unsupported or switched off for real
   readers, and a fill that only passes once it is blurred does not pass.
   ⭐ THE LABEL HERE IS DARK INK ON A LIGHT TINT, which flips the arithmetic:
   the worst case is not the brightest region under the button, it is the
   DARKEST one, where the teal thins toward the scrim. So the primary's tint
   sits at 88% and is measured at its dark end, not its bright end.
   ⭐ Hierarchy: primary is brand-tinted glass, secondary is a clear deep
   teal-black at 54% with a white label. Two glass buttons of the same weight
   would be two secondaries.
   ⛔ FORM: a CAPSULE. Water is the softest thing any of these four sites
   sells, and this is the only one of the four whose radius goes round.
   auto-repair is square, pressure-washing is a weighted badge edge,
   drain-cleaning is a flag. Glass is the material; the form is the brand.
   ============================================================ */
.btn{border-radius:999px; padding-inline:1.35rem}
.btn--go{
  background:color-mix(in srgb, var(--accent) 88%, transparent);
  color:var(--accent-ink);
  border:1.5px solid color-mix(in srgb, #fff 44%, transparent);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.5)}
.btn--go:hover{
  background:color-mix(in srgb, var(--accent) 96%, transparent);
  border-color:color-mix(in srgb, #fff 66%, transparent)}
.btn--line{
  background:rgba(4,26,34,.54);
  color:var(--ink);
  border:1.5px solid rgba(255,255,255,.44);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.2)}
.btn--line:hover{background:rgba(4,26,34,.7); border-color:rgba(255,255,255,.7)}
.lanaihero .btn--line{color:#fff}
@supports ((backdrop-filter:blur(2px)) or (-webkit-backdrop-filter:blur(2px))){
  .btn--go,.btn--line,.launch{
    -webkit-backdrop-filter:blur(16px) saturate(1.35);
    backdrop-filter:blur(16px) saturate(1.35)}
}
.btn:focus-visible,.launch:focus-visible{
  outline:2px solid color-mix(in srgb, var(--accent) 70%, #fff);
  outline-offset:3px}

/* ⛔ THE GHOST, RESTATED BELOW THE LAST `.btn{`. check-demo-polish refused the
   build for exactly this: the glass block above declares a bare `.btn{` for
   the radius, this stylesheet is unlayered, and at equal specificity the later
   rule wins — so `.btn--ghost` would have lost its background and both
   buttons would have rendered as one solid block. Two primaries is no primary.
   ⚠️ This site uses `.btn--line` for its secondary and `.btn--ghost` barely
   appears, which is precisely why nobody would have noticed. Any future
   `.btn{` appended below this point must restate it again. */
.btn--ghost{
  background:rgba(4,26,34,.54);
  color:var(--ink);
  border:1.5px solid rgba(255,255,255,.44);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.2)}
.btn--ghost:hover{background:rgba(4,26,34,.7); border-color:rgba(255,255,255,.7)}
.lanaihero .btn--ghost{color:#fff}

/* 🔴 "MY SELECTOR MATCHES" IS NOT "MY RULE APPLIES". The glass block above
   declares `.btn--line` at (0,1,0). The hero's own secondary was already
   declared at `.lanaihero .btn--line` (0,2,0) higher up the file, carrying
   the OLD half-glass — rgba(...,.42) behind a 3px blur — so on the one surface
   that actually needed the new material nothing changed at all. The probe
   caught it: the new fill never appeared in the computed style.
   ⛔ And .42 is not enough. Measured over the worst window a button of this
   size can occupy on this hero, the old secondary came back at 3.05 against a
   floor of 4.5 — a label you cannot read if the photograph crops differently
   at another width. Resolve by specificity, after, never by !important. */
.lanaihero .btn--line,.lanaihero .btn--ghost,.lanaihero .btn--out{
  background:rgba(4,26,34,.62);
  color:#fff;
  border:1.5px solid rgba(255,255,255,.46);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.18)}
.lanaihero .btn--line:hover,.lanaihero .btn--ghost:hover,.lanaihero .btn--out:hover{
  background:rgba(4,26,34,.78); border-color:rgba(255,255,255,.72)}
@supports ((backdrop-filter:blur(2px)) or (-webkit-backdrop-filter:blur(2px))){
  /* the hero secondary kept a 3px blur from the older rule at (0,2,0); the
     material has to be the same object as the primary beside it */
  .lanaihero .btn--line,.lanaihero .btn--ghost,.lanaihero .btn--out{
    -webkit-backdrop-filter:blur(14px) saturate(1.28);
    backdrop-filter:blur(14px) saturate(1.28)}
}


/* ============================================================
   THE SHEEN — A SHAPE BOUND TO THE TYPE, NOT A SHEET OVER THE PICTURE
   23 Sep 2026. Owner: "i think the picture should be full hero then shene out
   where you want the text", and "evry hero needs to be perfecto".
   🔴 WHAT WAS WRONG WITH THE SCRIM, and the tell was in my own numbers. A
   scrim on the SECTION is one sheet doing two jobs: it darkens the words and
   it darkens the two thirds of the frame that carry no words at all. That is
   exactly why the band luma and the picture-side luma had to be chased
   against each other, and why at phone width the picture side fell to 51-69
   against a target of 80 — the wash covered everything, so the only way to
   keep the type legible was to dim the photograph.
   ⭐ THE DARKENING IS LOCAL NOW. It lives on the ::before of the block that
   holds the type, so it moves with the words at every width and at every one
   of the four compositions. type is anchored at the TOP, so the sheen carries further up than down
   ⛔ SOFT EDGES ONLY. The ellipse is sized 50% 50% of its own box, so it
   reaches alpha 0 exactly at that box's edge — there is no step anywhere, and
   no boundary a reader could see. A visible edge where a sheen stops is worse
   than the flat wash it replaced.
   ⛔ AND THE FLOOR DOES NOT MOVE: 4.5:1 on every run of type, 3:1 on the
   display, measured over the composited result at the brightest region the
   type actually occupies. A local sheen has less area to work in, so it is
   harder than a sheet, not easier.
   ⚠️ What is left on .lanaihero__scrim is an EDGE FLOOR only — it keeps the
   extreme top and near edges from clipping against the header and the page
   ground. It is not doing the reading work any more.
   ============================================================ */
.lanaihero__scrim{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(0deg,  rgba(3,26,34,.24) 0%, rgba(3,26,34,.07) 14%,
                           rgba(3,26,34,.00) 32%, rgba(3,26,34,.00) 100%),
    linear-gradient(90deg, rgba(3,26,34,.12) 0%, rgba(3,26,34,.03) 26%,
                           rgba(3,26,34,.00) 54%, rgba(3,26,34,.00) 100%)}
.lanaihero .sheen{position:relative;display:grid;max-width:41rem;z-index:0}
.lanaihero .sheen::before{content:"";position:absolute;z-index:-1;pointer-events:none;
  top:-12rem;bottom:-9rem;left:-14rem;right:-12rem;
  /* 🔴 THE DIRECTIONAL LAYER HAD TO GO, AND THE NUMBER THAT KILLED IT WAS
     0.60 OF ALPHA IN ONE PIXEL. A 90deg linear varies only with x, so it
     ends abruptly at the top and bottom of its own box — on three of these
     four heroes that edge happened to fall off-frame and nobody would have
     seen it; on drain-cleaning it landed at y=109 as a hard horizontal line
     across the whole picture. ⛔ It also darkened the frame ABOVE the type
     just as much as the type itself, which is the wash it was meant to
     replace. An ellipse falls to zero in every direction; a linear does not.
     ⭐ So it is one ellipse, with a FLAT CORE wide enough that the block's
     own corners — where a headline's last line ends — are still inside it,
     and a long tail so the steepest step is under 0.004 of alpha per pixel.
     The inflation is what buys that: the box is the type block plus enough
     margin that its corners sit at about t=0.83 of the ellipse, not 0.90. */
  /* 🔴 THE RADII MUST FIT THE BOX FOR THE CENTRE YOU CHOSE. `50% 50% at 48%
     54%` pushes the ellipse 4% past the box's far side, so it is CUT there
     rather than fading out — alpha stepped from .30 to 0 in one pixel and
     the edge check caught it at 0.0124/px. ⛔ radius = min(centre, 1-centre)
     on each axis, or the shape you drew is not the shape that paints. */
  background:radial-gradient(50% 46% at 50% 46%,
    rgba(3,26,34,.88) 0%, rgba(3,26,34,.88) 68%, rgba(3,26,34,.68) 82%,
    rgba(3,26,34,.30) 92%, rgba(3,26,34,.08) 97%, rgba(3,26,34,.00) 100%)}
@media (max-width:640px){
  /* ⚠️ A PHONE IS NOT A NARROW DESKTOP. The block is most of the frame here,
     so the inflation that keeps the falloff soft has to shrink with it or the
     sheen simply becomes the wash again. */
  /* ⚠️ MEASURED AT 320: the type block is ~83% of the frame here, so a sheen
     that spills 5rem past it in every direction covers the photograph
     entirely — picture-outside fell to 3% of the frame. A local sheen
     cannot give much back when the type IS the frame, but the falloff
     belongs inside the viewport rather than off the edge of it. */
  /* 🔴 AT 320 THE TYPE IS THE FRAME, AND THAT IS THE WHOLE FINDING. The block
     here is ~280x500 of a 320x605 hero. An ellipse inscribed in a box can never
     reach that box's CORNERS — measured, the headline's top corner sits at
     t=1.05, outside the shape entirely — so tightening the inflation to return
     picture took the h1 from 3.21 to 2.33 and then to 1.19 against a floor of 3.
     ⛔ THE FLOOR DOES NOT MOVE, so the inflation stays wide enough that the
     falloff happens off-frame and the corners stay in the core.
     ⚠️ THE COST, PRINTED RATHER THAN HIDDEN: at 320 only about 3% of the frame
     is outside the sheen. A sheen bound to the type cannot give the picture
     back when the type occupies 83% of the frame — that is a property of the
     content at that width, not of the technique, and the honest lever for it
     is a shorter headline or a taller hero, neither of which is mine to take.
     From 641px up the ellipse does exactly what it was built to do: 32-47% of
     the frame sits outside the sheen at its own photographic luminance. */
  .lanaihero .sheen::before{top:-5rem;bottom:-5rem;left:-5rem;right:-5rem;
    /* 🔴 THE PHONE BRANCH HAD FORGOTTEN THE COMPOSITION. All four shared
       `at 50% 50%` here, which is right for the one hero whose type is
       centred and wrong for the three that anchor top or bottom — and it
       only showed at ONE width: pool-service passed at 320 (3.34) and at
       390, and failed at 375 (2.32 against 3), because that is where its
       hero grew tall enough to push the headline into the ellipse's top
       corner. ⛔ A sheen bound to the type has to follow the type at
       every width, not just the ones that were spot-checked.
       ⚠️ radius = min(centre, 1-centre) per axis, or the shape is cut. */
    background:radial-gradient(50% 42% at 50% 42%,
      rgba(3,26,34,.91) 0%, rgba(3,26,34,.91) 74%, rgba(3,26,34,.70) 85%,
      rgba(3,26,34,.30) 93%, rgba(3,26,34,.08) 97%, rgba(3,26,34,.00) 100%)}
}

/* 🔴 THE TABLET GAP — MEASURED, ATTEMPTED, AND REVERTED. At 741 the desktop
   sheen leaves only 1% of the frame outside it on auto-repair against 32% at
   1280, because the inflation is 12-14rem a side and that is the whole frame
   at 741. I capped the margin in vw to get the picture back: picture-outside
   went to 20-31% and the HEADLINE fell to 2.17 and 1.63 against a floor of 3,
   with pressure-washing's refusal reason at 2.80 against 4.5.
   ⛔ THE FLOOR DOES NOT MOVE, so the vw cap is gone and the rem inflation
   stands. ⭐ THE REASON IS GEOMETRY, NOT TUNING: an ellipse inscribed in a box
   cannot cover that box's corners, so the margin has to be about 0.37 of the
   block on each axis to keep a headline's last line inside the core — roughly
   240px for a 660px measure. At 741 the block is already the full column, so
   block + margin exceeds the frame however the margin is written.
   ⚠️ SO THE HONEST NUMBER IS PRINTED INSTEAD: 641-1023px returns 1-18% of the
   frame to the photograph, and 1024 and up returns 32-47%. Below 641 it is
   3-6%. A sheen bound to the type gives the picture back exactly in proportion
   to how much of the frame the type is NOT using. */
