/* ==========================================================================
   §0  FONTS — self-hosted
   Variable files: one request per family covers every weight the site uses.
   Serving them from our own origin removes a DNS lookup, a TLS handshake and
   a third-party dependency from the critical path, and means no visitor data
   reaches Google before the page has even rendered.
   ========================================================================== */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 900;
  font-display: swap;
  src: url('../fonts/archivo-var.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('../fonts/inter-var.woff2') format('woff2');
}
/* SELF-HOSTED BECAUSE THE CSP LEAVES NO CHOICE. netlify.toml sets
   `font-src 'self'` and `style-src 'self'`, so a Google Fonts <link> is blocked
   outright and the page falls back to a system face with no error. Both faces
   are SIL Open Font License, latin subset only — 25KB for the pair. */
@font-face {
  font-family: 'Aboreto';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/aboreto-400.woff2') format('woff2');
}
@font-face {
  font-family: 'Questrial';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/questrial-400.woff2') format('woff2');
}
/* A MONO THIRD VOICE WAS TRIED HERE AND REJECTED, 2026-09-16. IBM Plex Mono
   was self-hosted and given the labels, the figures and the step numbers.
   The owner's verdict on sight: the dotted zero is wrong for this business —
   it is a programmer's disambiguation mark, and on a phone number set large
   it is the first thing the eye goes to. Recorded so the next person does not
   reach for Plex Mono again without knowing that. If a third voice is ever
   wanted here, it needs a face with a plain lining zero.
   assets/fonts/plex-mono-400.woff2 and -600.woff2 are now unreferenced and
   should be deleted. */

/* ==========================================================================
   DZT LANDSCAPING & DESIGN — Design System
   Limestone & Forest. Light, architectural, photo-forward.
   Heavy display type carries the weight; color stays quiet and natural.
   ========================================================================== */

/* ---------- 1. Tokens ---------- */
/* Palette taken directly from the DZT badge:
   slate #374550 (the circle) · green #095E41 (the Z) · white.
   No warm accents — the mark has none. */
:root {
  /* Paper — cool neutral so it sits under slate + green without muddying */
  --paper:     #F5F4F0;
  --paper-2:   #ECEAE3;
  --paper-3:   #E0DDD3;
  --white:     #FFFFFF;

  /* Ink */
  --ink:       #1B2229;
  --stone:     #5C6771;
  --faint:     #636B73;   /* was #8C959C (2.78:1) — now 4.9:1 on paper, 4.6:1 on paper-2 */

  /* Brand green — primary accent, buttons, links */
  --x-bronze:      #095E41;
  --x-bronze-deep: #063F2C;
  --x-bronze-soft: #0E7A54;
  --sage:        #9ACBB2;   /* lightened from #7FB79A for AA on slate */

  /* Brand slate — inverted bands, so the badge sits on its own colour family */
  --slate:       #374550;
  --slate-deep:  #28313A;

  /* Secondary accent aliases: green on light, sage on dark */
  --clay:        #095E41;
  --clay-soft:   #9ACBB2;

  /* Lines */
  --line:      rgba(27, 34, 41, 0.13);
  --line-2:    rgba(27, 34, 41, 0.26);
  --line-warm: rgba(9, 94, 65, 0.24);

  /* On dark (inverted bands) */
  --bone:      #F3F5F4;
  --bone-mute: rgba(243, 245, 244, 0.72);
  --bone-line: rgba(243, 245, 244, 0.20);

  /* Shadow */
  --sh-sm: 0 1px 2px rgba(27,34,41,0.05);
  --sh:    0 1px 2px rgba(27,34,41,0.05), 0 14px 28px -22px rgba(27,34,41,0.45);
  --sh-lg: 0 2px 4px rgba(27,34,41,0.05), 0 30px 60px -35px rgba(27,34,41,0.5);

  /* Type */
  --display: 'Archivo', 'Helvetica Neue', Arial, sans-serif;
  --body:    'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --step--1: clamp(0.82rem, 0.79rem + 0.14vw, 0.9rem);
  --step-0:  clamp(1rem, 0.96rem + 0.2vw, 1.1rem);
  --step-1:  clamp(1.18rem, 1.09rem + 0.44vw, 1.46rem);
  --step-2:  clamp(1.5rem, 1.32rem + 0.9vw, 2.1rem);
  --step-3:  clamp(1.95rem, 1.6rem + 1.75vw, 3.1rem);
  --step-4:  clamp(2.5rem, 1.85rem + 3.2vw, 4.6rem);
  --step-5:  clamp(3.1rem, 1.95rem + 5.8vw, 7.2rem);

  --gutter: clamp(1.15rem, 4vw, 3.5rem);
  --max: 1320px;
  --max-narrow: 820px;
  --nav-h: 82px;

  --r-sm: 3px;
  --r:    6px;
  --r-lg: 12px;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ---------- 2. Reset & base ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 24px);
  /* DARK ON PURPOSE, AND NOT WHAT THE PAGE LOOKS LIKE — `body` paints
     var(--paper) over all of this, so no content sits on it.

     What it is for is the strip behind the phone's clock and battery. Two
     different things colour that area on iOS and only one of them is
     <meta name="theme-color">: the tag paints the browser's own toolbar, and
     the region above the document takes the background of the ROOT element.
     This said var(--paper) — #F5F4F0 — so the status bar stayed tan-white
     above a dark header no matter what the meta tag claimed, which is the
     seam that made the site look like it stopped at the header.

     It is also the colour of the overscroll at both ends, and both ends are
     already dark: .site-head is --slate-deep below 1080px and .site-foot is
     --slate-deep everywhere. Rubber-banding now reveals more of the same
     rather than a flash of white.

     NAMED --slate-deep, NOT --ink. It said var(--ink) because --ink WAS the
     near-black at the time; the dark inversion made --ink the cream text and
     quietly recreated the exact bug this rule was written to fix, with the
     status bar and the overscroll going cream above a dark header. The two
     ends it is matching are --slate-deep, so that is what it should name — a
     token that cannot invert out from under it. */
  background: var(--slate-deep);
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  font-family: var(--body);
  font-size: var(--step-0);
  line-height: 1.65;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
body.nav-open { overflow: hidden; }

img, svg, video { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }

::selection { background: var(--mark); color: var(--bone); }

:focus-visible {
  outline: 2px solid var(--mark);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ---------- 3. Typography ---------- */
h1, h2, h3, h4, h5 {
  font-family: var(--display);
  font-weight: 800;
  line-height: 1.0;
  letter-spacing: -0.026em;
  word-spacing: 0.06em;
  text-wrap: balance;
  color: var(--ink);
}
h1 { font-size: var(--step-5); font-weight: 900; letter-spacing: -0.034em; }
/* Section heads sit a clear step below the hero h1. Sizing them near-equal
   flattens hierarchy and makes every section shout at once. */
h2 { font-size: var(--step-3); font-weight: 900; letter-spacing: -0.028em; }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); letter-spacing: -0.02em; }
/* `.h3` sizes a heading DOWN without changing what it is. warranty.html uses
   <h2 class="h3"> so the "Draft — not yet contractual" callout keeps its place
   in the document outline for a screen reader while sitting visually below the
   section heading it qualifies. The class had no rule, so it rendered at full
   h2 and the disclaimer shouted over the thing it was disclaiming. */
.h3 { font-size: var(--step-2); }

p { text-wrap: pretty; }

.eyebrow {
  font-family: var(--display);
  font-size: 0.71rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--mark);
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  line-height: 1.45;
  flex-wrap: wrap;
  max-width: 100%;
}
.eyebrow::before {
  content: '';
  width: 26px; height: 2px;
  background: var(--mark);
  flex: none;
}
.eyebrow--plain::before { display: none; }

.lede {
  font-size: var(--step-1);
  line-height: 1.52;
  color: var(--stone);
  max-width: 58ch;
}
.muted { color: var(--stone); }
.acid, .forest { color: var(--mark); }
.clay { color: var(--mark); }

/* ---------- 4. Layout primitives ---------- */
.wrap {
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.wrap--narrow { max-width: var(--max-narrow); }

/* 4.1: more air between sections as the page scrolls — the old rhythm read as
   crowded. Roughly +55% on the vertical gap between content blocks. */
.section { padding-block: clamp(3.9rem, 6.8vw, 6.75rem); position: relative; }
.section--tight { padding-block: clamp(2.75rem, 4.6vw, 4rem); }

.section-head {
  display: grid;
  gap: 1.5rem;
  margin-bottom: clamp(1.5rem, 2.4vw, 2.15rem);
}
@media (min-width: 900px) {
  .section-head--split {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    align-items: end;
    gap: 3rem;
  }
}
.section-head h2 { margin-top: 0.9rem; }

.grid { display: grid; gap: clamp(1rem, 2vw, 1.5rem); }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr)); }

/* Surveyor grid backdrop — very faint on paper */
.bp-grid { position: relative; isolation: isolate; }
.bp-grid::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image:
    linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 78px 78px;
  mask-image: radial-gradient(ellipse 75% 65% at 50% 40%, #000 20%, transparent 76%);
  opacity: 0.45;
  pointer-events: none;
}

/* Survey-tape rule */
.hazard {
  height: 10px;
  width: 100%;
  background: repeating-linear-gradient(
    -45deg,
    var(--volt) 0 10px,
    var(--paper-3) 10px 20px
  );
  opacity: 1;
}
.hazard--thin { height: 4px; }

/* ---------- 5. Buttons ---------- */
.btn {
  --btn-bg: var(--clay);       /* the stand's green */
  --btn-fg: var(--bone);       /* the letters' bone */
  --btn-bd: var(--clay);       /* the edge IS the fill: a steel hairline on a green button was tried and read as trim */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.65em;
  padding: 1.05em 1.9em;
  font-family: var(--display);
  font-size: 0.8rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--btn-fg);
  background: var(--btn-bg);
  border: 1.5px solid var(--btn-bd);
  border-radius: 2px;
  position: relative;
  overflow: hidden;
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease),
              background 0.25s, color 0.25s, border-color 0.25s;
  white-space: nowrap;
}
.btn::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 25%, rgba(255,255,255,0.18) 50%, transparent 75%);
  transform: translateX(-130%);
  transition: transform 0.75s var(--ease);
}
.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 14px 26px -16px rgba(31,58,44,0.7);
  background: var(--clay-hi);
  border-color: var(--clay-hi);
}
.btn:hover::after { transform: translateX(130%); }
.btn:active { transform: translateY(0); }

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  --btn-bd: var(--line-2);
}
/* This rule declared `color` TWICE — var(--paper) and then var(--bone), and the
   second one won, which is how a ghost button ended up painting cream on the
   brand green at 2.75:1. The dark ground is the correct label on a green fill
   and it is what the first declaration already said. */
.btn--ghost:hover {
  background: var(--volt);
  border-color: var(--volt);
  color: var(--bone);
  box-shadow: none;
}
.btn--clay { --btn-bg: var(--clay); --btn-bd: var(--clay); --btn-fg: var(--bone); }
.btn--clay:hover { background: var(--clay-hi); border-color: var(--clay-hi); color: var(--bone); box-shadow: 0 14px 26px -16px rgba(40,83,49,0.7); }
.btn--rust { --btn-bg: var(--clay); --btn-bd: var(--clay); --btn-fg: var(--bone); }
.btn--rust:hover { background: var(--clay-hi); border-color: var(--clay-hi); color: var(--bone); }
.btn--lg { padding: 1.25em 2.4em; font-size: 0.88rem; }

/* A BUTTON THAT CANNOT FIT ITS PHONE. `.btn` is `white-space: nowrap` so a
   label never breaks mid-phrase, and `.btn--lg` adds 2.4em of padding on each
   side. Together they make an UNSHRINKABLE box: "Book a free site walk →" on
   check-a-contractor.html measured 312px inside a 283px column at a 320px
   viewport, and the 29px it could not give up became a sideways scroll on the
   whole page.

   Below 420px the nowrap is lifted and the padding comes in, in that order of
   importance. Trimming the padding alone would have fixed exactly this label
   and left the next longer one to rediscover the bug — with wrapping allowed,
   a label that still does not fit becomes two centred lines instead of a
   horizontal scrollbar. The buttons that already fit are untouched: nothing
   wraps that was not overflowing, so this is a backstop rather than a
   restyle. */
@media (max-width: 419.98px) {
  .btn { max-width: 100%; white-space: normal; text-align: center; }
  .btn--lg { padding-inline: 1.5rem; }
}
/* An author `display` declaration outranks the UA [hidden] rule, so the multi-step
   form's Submit button stayed visible on every step. */
.btn[hidden] { display: none; }
.btn--block { width: 100%; }

/* On inverted (forest) backgrounds */
.on-dark .btn { --btn-bg: var(--bone); --btn-bd: var(--bone); --btn-fg: var(--bone); }
.on-dark .btn:hover { background: var(--clay-hi); border-color: var(--clay-hi); box-shadow: 0 14px 26px -16px rgba(0,0,0,0.55); color: var(--bone); }
.on-dark .btn--ghost { --btn-bg: transparent; --btn-fg: var(--bone); --btn-bd: var(--bone-line); }
.on-dark .btn--ghost:hover { background: var(--bone); color: var(--slate-deep); border-color: var(--bone); }

.btn-arrow { transition: transform 0.35s var(--ease); }
.btn:hover .btn-arrow { transform: translateX(4px); }

.link-x {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  font-family: var(--display);
  font-weight: 700;
  font-size: 0.8rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--mark);
  padding-bottom: 4px;
  background-image: linear-gradient(var(--mark), var(--mark));
  background-size: 0% 1.5px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size 0.4s var(--ease);
}
.link-x:hover { background-size: 100% 1.5px; }

/* ---------- 6. Header / nav ---------- */
.topbar {
  background: #1B232B;
  color: var(--bone-mute);
  font-size: 0.77rem;
}
.topbar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  min-height: 40px;
  flex-wrap: wrap;
}
.topbar strong { color: var(--bone); font-weight: 600; }
.topbar .acid { color: var(--mark); }
.topbar__list { display: flex; gap: 1.5rem; list-style: none; padding: 0; flex-wrap: wrap; }
.topbar__list li { display: flex; align-items: center; gap: 0.45em; }
/* Below 720 the top bar's content is hidden, but it still carries the dark band
   behind the header — so keep the element, drop only its contents. */
@media (max-width: 719.98px) {
  .topbar { display: block; height: 0; min-height: 0; padding: 0; border-bottom: 0; }
  .topbar__inner { display: none; }
}

.site-head {
  position: sticky;
  top: 0;
  z-index: 100;
  /* Deliberately bare: the glass is the .nav panel inside. Any background or
     backdrop-filter here paints a full-width band around the floating panel
     once the page scrolls under it. */
  background: transparent;
  border-bottom: 0;
}
.site-head.is-stuck {
  background: transparent;
  border-bottom: 0;
  box-shadow: none;
}
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  min-height: var(--nav-h);
}

.brand { display: flex; align-items: center; gap: 0.75rem; flex: none; }
.brand__mark { width: 58px; height: 58px; flex: none; }
.site-foot .brand__mark { width: 52px; height: 52px; }
.brand__text { display: grid; line-height: 1; }
.brand__name {
  font-family: var(--display);
  font-weight: 900;
  font-size: 1.12rem;
  letter-spacing: -0.03em;
}
.brand__name em { font-style: normal; color: var(--mark); }
.brand__sub {
  font-size: 0.55rem;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--faint);
  margin-top: 4px;
  font-weight: 600;
}

.nav__links {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  list-style: none;
  padding: 0;
  margin: 0 auto;
}
.nav__links a {
  display: block;
  padding: 0.6rem 0.85rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--stone);
  border-radius: 2px;
  transition: color 0.2s, background 0.2s;
  position: relative;
}
.nav__links a:hover { color: var(--ink); background: rgba(22,24,15,0.045); }
.nav__links a[aria-current="page"] { color: var(--mark); }
.nav__links a[aria-current="page"]::after {
  content: '';
  position: absolute;
  left: 0.85rem; right: 0.85rem; bottom: 0.3rem;
  height: 2px;
  background: var(--mark);
}
.nav__cta { display: flex; align-items: center; gap: 0.75rem; flex: none; }
.nav__phone {
  font-family: var(--display);
  font-weight: 800;
  font-size: 0.95rem;
  letter-spacing: -0.02em;
  display: flex;
  align-items: center;
  gap: 0.5em;
  transition: color 0.2s;
}
.nav__phone:hover { color: var(--mark); }

.nav__toggle {
  display: none;
  width: 46px; height: 46px;
  border: 1px solid var(--line-2);
  border-radius: 2px;
  place-items: center;
  flex: none;
}
.nav__toggle span {
  display: block;
  width: 20px; height: 2px;
  background: var(--ink);
  position: relative;
  transition: background 0.2s;
}
.nav__toggle span::before,
.nav__toggle span::after {
  content: '';
  position: absolute;
  left: 0;
  width: 20px; height: 2px;
  background: var(--ink);
  transition: transform 0.3s var(--ease);
}
.nav__toggle span::before { top: -6px; }
.nav__toggle span::after  { top: 6px; }
body.nav-open .nav__toggle span { background: transparent; }
body.nav-open .nav__toggle span::before { transform: translateY(6px) rotate(45deg); }
body.nav-open .nav__toggle span::after  { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 1080px) {
  .nav__links, .nav__cta .btn { display: none; }
  .nav__toggle { display: grid; }
}
@media (max-width: 559.98px) { .nav__phone span { display: none; } }

/* Below 380px the header simply does not fit, and the whole PAGE scrolls
   sideways as a result — measured at 320px on every page: the document is
   375px wide against a 320px viewport, because brand (213) + phone and toggle
   (104) needs 375 whatever the viewport does.
   The widest single element is the brand tagline, and it is the right thing to
   drop: the logo mark and "DZT" still identify the business, whereas the
   alternative is a horizontally scrolling site on a 320px phone. */
@media (max-width: 379.98px) {
  .site-head .brand__sub { display: none; }
  .site-head .brand__name { font-size: 1rem; }
  .site-head .nav { --pad: 0.6rem; }
}

.drawer {
  position: fixed;
  inset: var(--nav-h) 0 0;
  z-index: 99;
  background: var(--paper);
  padding: 2rem var(--gutter) 3rem;
  overflow-y: auto;
  transform: translateY(-8px);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, transform 0.3s var(--ease), visibility 0.3s;
}
body.nav-open .drawer { opacity: 1; visibility: visible; transform: none; }
.drawer__links { list-style: none; padding: 0; display: grid; }
.drawer__links a {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding: 1.1rem 0;
  border-bottom: 1px solid var(--line);
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.6rem;
  letter-spacing: -0.03em;
}
.drawer__links a span {
  font-family: var(--body);
  font-size: 0.65rem;
  letter-spacing: 0.2em;
  color: var(--faint);
  font-weight: 600;
}
.drawer__links a[aria-current="page"] { color: var(--mark); }
.drawer__foot { margin-top: 2rem; display: grid; gap: 1rem; }

/* ---------- 7. Hero ---------- */
.hero {
  position: relative;
  min-height: min(84vh, 800px);
  display: grid;
  align-items: end;
  padding-block: clamp(2.5rem, 7vh, 5rem) clamp(2rem, 5vh, 3.25rem);
  overflow: hidden;
  background: var(--paper);
}
.hero__bg { position: absolute; inset: 0; z-index: 0; }
.hero__bg img, .hero__bg svg { width: 100%; height: 100%; object-fit: cover; }
.hero__bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 85% 65% at 18% 88%, rgba(244,241,234,0.95), transparent 68%),
    linear-gradient(to top, var(--paper) 1%, rgba(244,241,234,0.55) 40%, rgba(244,241,234,0.25) 100%);
}
.hero__inner { position: relative; z-index: 1; width: 100%; }
.hero__copy { min-width: 0; max-width: 660px; }

/* ---- Photo-led hero: full-bleed image, copy on a scrim ---- */
.hero--photo {
  /* Derived from viewport WIDTH (56vw ~ 16:9) so the box matches the photo and
     nothing is cropped. aspect-ratio can't be used: paired with max-height the
     browser shrinks the width to hold the ratio and the hero stops being
     full-bleed.
     This is a MIN, not a fixed height. As a fixed height it clipped its own
     content on short viewports — at 500x600 the buttons fell 65px past the
     overflow:hidden edge and simply vanished. */
  min-height: min(56vw, 90vh);
  height: auto;
  align-items: end;
  padding-block: clamp(6rem, 18vh, 11rem) clamp(2.5rem, 7vh, 4.5rem);
  background: var(--slate-deep);
}
.hero__media { position: absolute; inset: 0; z-index: 0; }
.hero__media img { width: 100%; height: 100%; object-fit: cover; object-position: 62% 50%; }
.hero__media::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(100deg, rgba(20,26,32,0.88) 0%, rgba(20,26,32,0.64) 40%, rgba(20,26,32,0.14) 68%, transparent 84%),
    linear-gradient(to top, rgba(20,26,32,0.72) 0%, transparent 42%);
}
.hero--photo .hero__copy { max-width: min(780px, 68%); }
.hero--photo h1 { font-size: clamp(2.4rem, 5.1vw, 4.9rem); }
@media (max-width: 899.98px) { .hero--photo .hero__copy { max-width: 100%; } }
.hero--photo h1 { color: var(--bone); }
.hero--photo .hero__lede { color: rgba(241,238,228,0.84); max-width: 54ch; font-size: var(--step-0); }
.hero--photo .eyebrow { color: var(--mark); }
.hero--photo .eyebrow::before { background: var(--mark); }
.hero--photo .btn { --btn-bg: var(--bone); --btn-bd: var(--bone); --btn-fg: var(--bone); }
.hero--photo .btn:hover { background: var(--clay-hi); border-color: var(--clay-hi); box-shadow: 0 16px 30px -16px rgba(0,0,0,0.7); color: var(--bone); }
.hero--photo .btn--ghost { --btn-bg: transparent; --btn-fg: var(--bone); --btn-bd: rgba(241,238,228,0.5); }
.hero--photo .btn--ghost:hover { background: var(--bone); color: var(--slate-deep); border-color: var(--bone); }
@media (max-width: 639.98px) {
  .hero__media img { object-position: 68% 50%; }
  .hero--photo { min-height: min(80vh, 640px); }
}

/* Fact strip sitting under the photo, back on paper */
.hero-facts { background: var(--paper); border-bottom: 1px solid var(--line); }
.hero-facts .hero__meta { margin-top: 0; padding-block: 1.15rem; border-top: 0; }

.hero h1 { margin-block: 1.2rem 1.6rem; }
/* overflow:hidden masks the rise animation, but at line-height 1 it also sliced
   descenders (the "g" in Everything). Pad the mask, then pull the line back up. */
.hero h1 .line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.16em;
  margin-bottom: -0.16em;
}
.hero h1 { line-height: 1.04; }
.hero h1 .line > span { display: block; animation: rise 0.9s var(--ease) both; }
.hero h1 .line:nth-child(2) > span { animation-delay: 0.1s; }
.hero h1 .line:nth-child(3) > span { animation-delay: 0.2s; }
@keyframes rise { from { transform: translateY(105%); } to { transform: none; } }

.hero__lede { max-width: 58ch; font-size: var(--step-1); color: var(--stone); }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.9rem; margin-top: 2.2rem; }
.hero__meta {
  margin-top: clamp(2rem, 4vw, 3.25rem);
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  padding-top: 1.75rem;
  border-top: 1px solid var(--line-2);
}
.hero__meta div { display: grid; gap: 0.3rem; }
.hero__meta dt {
  font-size: 0.67rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--faint);
  font-weight: 600;
}
.hero__meta dd {
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.12rem;
  margin: 0;
  letter-spacing: -0.02em;
}

/* Interior page hero */
.pagehero {
  position: relative;
  padding-block: clamp(4rem, 12vw, 7.5rem) clamp(3rem, 7vw, 5rem);
  border-bottom: 1px solid var(--line);
  background: var(--paper-2);
  overflow: hidden;
}
.pagehero .hero__bg::after {
  background: linear-gradient(to right, var(--paper-2) 8%, rgba(235,231,220,0.7) 55%, rgba(235,231,220,0.35) 100%);
}
.pagehero__inner { position: relative; z-index: 1; max-width: 900px; }
.pagehero h1 { font-size: var(--step-4); margin-block: 1rem 1.15rem; }

.crumbs {
  display: flex; gap: 0.6rem; align-items: center;
  font-size: 0.71rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--faint); list-style: none; padding: 0; flex-wrap: wrap;
}
.crumbs a:hover { color: var(--mark); }
.crumbs li + li::before { content: '/'; margin-right: 0.6rem; color: var(--line-2); }

/* ---------- 8. Marquee ---------- */
.marquee {
  border-block: 1px solid var(--line);
  background: var(--paper-2);
  overflow: hidden;
  padding-block: 1.05rem;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent);
}
.marquee__track { display: flex; width: max-content; animation: slide 46s linear infinite; }
.marquee:hover .marquee__track { animation-play-state: paused; }
@keyframes slide { to { transform: translateX(-50%); } }
.marquee__group { display: flex; flex: none; }
.marquee__item {
  display: inline-flex;
  align-items: center;
  gap: 1.6rem;
  padding-inline: 1.6rem;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(0.92rem, 2vw, 1.28rem);
  letter-spacing: -0.02em;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}
.marquee__item::after {
  content: '';
  width: 6px; height: 6px;
  background: var(--clay);
  transform: rotate(45deg);
  flex: none;
}

/* ---------- 9. Statement (inverted band) ---------- */
.statement {
  background: var(--slate);
  color: var(--bone);
  position: relative;
  overflow: hidden;
}
.statement__inner {
  padding-block: clamp(2.75rem, 5vw, 4.5rem);
  display: grid;
  gap: 2.25rem;
}
@media (min-width: 900px) {
  .statement__inner { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 4rem; align-items: center; }
}
.statement h2 { color: var(--bone); }
.statement p { font-size: var(--step-1); line-height: 1.5; color: var(--bone-mute); }
.statement .eyebrow { color: var(--mark); }
.statement .eyebrow::before { background: var(--mark); }
.statement__note {
  border-left: 2px solid var(--bone-line);
  padding-left: 1.5rem;
  display: grid;
  gap: 1rem;
}
.statement__note p strong, .statement__note p em { color: var(--bone); font-style: normal; }

/* ---------- 10. Cards ---------- */
.card {
  position: relative;
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: clamp(1.35rem, 2.2vw, 1.65rem);
  display: grid;
  gap: 0.8rem;
  align-content: start;
  box-shadow: var(--sh-sm);
  transition: border-color 0.3s, transform 0.4s var(--ease), box-shadow 0.4s var(--ease);
  overflow: hidden;
}
.card::before {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 0; height: 2px;
  background: var(--mark);
  transition: width 0.45s var(--ease);
}
.card:hover { transform: translateY(-4px); box-shadow: var(--sh); border-color: var(--line-warm); }
.card:hover::before { width: 100%; }

.card__icon {
  width: 54px; height: 54px;
  display: grid; place-items: center;
  border: 1px solid var(--line-warm);
  border-radius: 3px;
  color: var(--mark);
  background: rgba(31, 58, 44, 0.05);
  flex: none;
}
.card__icon svg { width: 26px; height: 26px; }
.card h3 { font-size: var(--step-1); }
.card p { color: var(--stone); font-size: 0.94rem; line-height: 1.6; }
.card__list {
  list-style: none;
  padding: 0;
  display: grid;
  gap: 0.4rem;
  font-size: 0.86rem;
  color: var(--faint);
  border-top: 1px solid var(--line);
  padding-top: 1rem;
  margin-top: 0.25rem;
}
.card__list li { display: flex; gap: 0.6em; align-items: baseline; }
.card__list li::before { content: '—'; color: var(--mark); flex: none; }

.card--num { padding-top: 2.6rem; }
.card__num {
  position: absolute;
  top: 1.1rem; right: 1.3rem;
  font-family: var(--display);
  font-weight: 900;
  font-size: 2.6rem;
  line-height: 1;
  color: transparent;
  -webkit-text-stroke: 1px var(--line-2);
  letter-spacing: -0.05em;
}

/* Wide feature card */
.feature {
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--white);
  display: grid;
  box-shadow: var(--sh);
}
@media (min-width: 880px) {
  .feature--split { grid-template-columns: 1.05fr 1fr; }
  .feature--split.is-reverse .feature__media { order: 2; }
}
.feature__media { position: relative; min-height: 250px; background: var(--paper-2); }
.feature__media img { width: 100%; height: 100%; object-fit: cover; }
.feature__body {
  padding: clamp(1.8rem, 4vw, 3.2rem);
  display: grid;
  gap: 1.25rem;
  align-content: center;
}
.feature__body h3 { font-size: var(--step-3); }
.tagpill {
  position: absolute;
  top: 1rem; left: 1rem;
  z-index: 1;
  background: var(--volt);
  color: var(--bone);
  font-family: var(--display);
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 0.5em 0.9em;
  border-radius: 2px;
}

/* ---------- 11. Service index ---------- */
.srv-list { border-top: 1px solid var(--line-2); }
.srv-row {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1rem 1.5rem;
  align-items: center;
  padding: clamp(1.4rem, 3vw, 2.2rem) 0;
  border-bottom: 1px solid var(--line);
  position: relative;
  transition: padding-left 0.4s var(--ease);
}
@media (min-width: 860px) {
  .srv-row { grid-template-columns: 5rem minmax(0, 1fr) minmax(0, 1.15fr) auto; gap: 2.5rem; }
}
.srv-row::before {
  content: '';
  position: absolute;
  /* The hover wash deliberately bleeds into the gutter so it reads as a full
     row rather than a boxed one. A fixed -1.5rem bleed is wider than the gutter
     itself below a 600px viewport — `--gutter` is clamp(1.15rem, 4vw, 3.5rem),
     so it only reaches 1.5rem at 600px — and the excess pushed the DOCUMENT
     6px wider than the viewport on every page using .srv-row. Measured at
     320px: services.html and careers.html scrolled sideways. Never bleed
     further than there is gutter to bleed into. */
  inset: 0 calc(-1 * min(1.5rem, var(--gutter)));
  background: linear-gradient(90deg, rgba(31,58,44,0.055), transparent 62%);
  opacity: 0;
  transition: opacity 0.35s;
  pointer-events: none;
}
.srv-row:hover::before { opacity: 1; }
@media (min-width: 860px) { .srv-row:hover { padding-left: 1.2rem; } }
.srv-row__idx {
  font-family: var(--display);
  font-weight: 800;
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  color: var(--faint);
}
.srv-row h3 { font-size: clamp(1.3rem, 3vw, 1.85rem); transition: color 0.3s; }
.srv-row:hover h3 { color: var(--mark); }
.srv-row p { color: var(--stone); font-size: 0.92rem; grid-column: 1 / -1; }
@media (min-width: 860px) { .srv-row p { grid-column: auto; } }
.srv-row__tags { display: flex; flex-wrap: wrap; gap: 0.4rem; grid-column: 1 / -1; }
@media (min-width: 860px) { .srv-row__tags { grid-column: auto; justify-content: flex-end; max-width: 290px; } }

/* Careers openings reuse .srv-row, but add rows the services list never has.
   Without this they fall into the narrow index column and set one word per
   line. Scoped to descendants, so the services rows are untouched. */
.srv-row .card__list,
.srv-row > .link-arrow { grid-column: 1 / -1; }
.srv-row .card__list { margin: 0.2rem 0 0; }
.srv-row > .link-arrow { justify-self: start; margin-top: 0.3rem; }
.tag {
  font-size: 0.67rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--stone);
  border: 1px solid var(--line);
  background: var(--white);
  border-radius: 100px;
  padding: 0.34em 0.78em;
  white-space: nowrap;
}

/* ---------- 12. Process ---------- */
.process { display: grid; gap: 0; }
.process__step {
  display: grid;
  gap: 1rem 2.5rem;
  padding-block: clamp(1.75rem, 4vw, 2.75rem);
  border-top: 1px solid var(--line);
  align-items: start;
}
@media (min-width: 800px) {
  .process__step { grid-template-columns: 8rem minmax(0, 1fr) minmax(0, 1.2fr); }
}
.process__step:last-child { border-bottom: 1px solid var(--line); }
.process__num {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(2.2rem, 5vw, 3.4rem);
  line-height: 0.85;
  color: transparent;
  -webkit-text-stroke: 1.5px var(--mark);
  letter-spacing: -0.05em;
}
.process__step h3 { font-size: var(--step-2); }
.process__step p { color: var(--stone); }
.process__step ul {
  list-style: none; padding: 0; margin-top: 0.85rem;
  display: grid; gap: 0.35rem;
  font-size: 0.86rem; color: var(--faint);
}
.process__step ul li::before { content: '↳ '; color: var(--mark); }

/* ---------- 13. Projects — image over caption (architectural grid) ---------- */
.filters { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 2rem; }
.filter {
  font-family: var(--display);
  font-size: 0.73rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 0.7em 1.15em;
  border: 1px solid var(--line);
  background: var(--white);
  border-radius: 2px;
  color: var(--stone);
  transition: all 0.25s;
}
.filter:hover { color: var(--ink); border-color: var(--line-2); }
.filter.is-active { background: var(--volt); border-color: var(--mark); color: var(--bone); }
/* The active filter is also hoverable, and without this the generic
   `.filter:hover` painted --ink onto the green fill at 2.75:1 — the one chip
   you are most likely to have the mouse on. */
.filter.is-active:hover { color: var(--bone); border-color: var(--clay-hi); background: var(--volt-hi); }

.proj-grid { display: grid; gap: clamp(1.25rem, 2.2vw, 2rem); }
@media (min-width: 700px)  { .proj-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1100px) { .proj-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1100px) {
  .proj-grid--feature > *:first-child { grid-column: span 2; grid-row: span 2; }
}
.proj-grid > * { align-self: start; }

.proj {
  position: relative;
  display: grid;
  grid-template-rows: auto 1fr;
  gap: 1.1rem;
  min-height: 0;
  background: none;
  border: 0;
  transition: transform 0.4s var(--ease);
}
.proj__img {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--r);
  border: 1px solid var(--line);
  background: var(--paper-2);
  aspect-ratio: 4 / 3;
  box-shadow: var(--sh-sm);
  transition: box-shadow 0.4s var(--ease), border-color 0.3s;
}
@media (min-width: 1100px) {
  .proj-grid--feature > .proj:first-child .proj__img { aspect-ratio: 3 / 2; }
}
.proj__img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.9s var(--ease); }
.proj:hover .proj__img { box-shadow: var(--sh); border-color: var(--line-warm); }
.proj:hover .proj__img img { transform: scale(1.045); }
.proj__body {
  position: static;
  display: grid;
  gap: 0.5rem;
  align-content: start;
  padding: 0;
  background: none;
}
.proj__cat {
  font-family: var(--display);
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--mark);
}
.proj h3 { font-size: var(--step-1); transition: color 0.3s; }
.proj:hover h3 { color: var(--mark); }
.proj-grid--feature > .proj:first-child h3 { font-size: var(--step-2); }
.proj__specs {
  display: flex; flex-wrap: wrap; gap: 0.3rem 0.9rem;
  font-size: 0.8rem; color: var(--stone);
  list-style: none; padding: 0;
}
.proj__specs li { display: flex; align-items: center; gap: 0.45em; }
.proj__specs li + li::before {
  content: ''; width: 3px; height: 3px; background: var(--faint);
  border-radius: 50%; margin-right: 0.45em;
}
.proj.is-hidden { display: none; }

/* ---------- 14. Stats ---------- */
.stats {
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  box-shadow: var(--sh-sm);
}
@media (min-width: 560px) { .stats { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 980px) { .stats { grid-template-columns: repeat(4, 1fr); } }
.stat {
  background: var(--white);
  padding: clamp(1.6rem, 3.5vw, 2.6rem);
  display: grid;
  gap: 0.5rem;
  transition: background 0.3s;
}
.stat:hover { background: var(--paper-3); }
.stat__num {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(2.4rem, 5vw, 3.6rem);
  line-height: 1;
  letter-spacing: -0.05em;
  color: var(--mark);
}
.stat__label {
  font-size: 0.71rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--stone);
  font-weight: 600;
}
.stat__note { font-size: 0.81rem; color: var(--faint); }

/* ---------- 15. Split / why-us ---------- */
.split { display: grid; gap: clamp(2rem, 5vw, 4.5rem); }
@media (min-width: 900px) {
  .split { grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); align-items: start; }
  .split--sticky > *:first-child { position: sticky; top: calc(var(--nav-h) + 3rem); }

  /* A CONTENTS RAIL IS NOT HALF THE PAGE. `.split` is a near-even 1fr / 1.1fr,
     which is right when both columns carry content — and wrong on the four
     reference pages, where the first column is a `.toc` jump-list of six to
     ten short links. It was taking 541px of a 1320px wrap and leaving 595px
     for the body.

     That is what broke the spec tables. Nine three-column tables on
     standards.html were being squeezed into 595px — columns at 182/170/242 —
     so "Thickened edge or edge reinforcement" wrapped to SIX lines of about
     six characters. Same on safety.html and warranty.html. The tables were
     never at fault; they were being handed a third of the page.

     260px fits the longest rail item without wrapping and hands the body
     ~990px, which a three-column table fits into comfortably. Verified on all
     four pages, and every one of them uses `.toc` as its first child. */
  .split--sticky { grid-template-columns: minmax(0, 260px) minmax(0, 1fr); }
}

.checks { list-style: none; padding: 0; display: grid; gap: 1.15rem; }
.checks li { display: grid; grid-template-columns: 30px 1fr; gap: 1rem; align-items: start; }
/* This drew an X, not a tick. Two linear-gradients at 45deg and -45deg, both
   centred at the same size, cross — which is the construction for a multiply
   sign. It sat on the estimate list on contact.html and the benefits list on
   careers.html, putting a "no" marker against every line that means "yes".

   A tick is two strokes of DIFFERENT lengths meeting at a point, so gradients
   are the wrong tool. Drawn as an SVG path instead and applied as a MASK rather
   than a background image, which keeps the colour coming from var(--volt)
   instead of being hardcoded inside a data URI where a token change would not
   reach it. */
.checks li::before {
  content: '';
  width: 26px; height: 26px;
  /* The ring lives in the SVG, not in a CSS border, because a mask clips the
     element's border along with everything else — the first version of this fix
     produced a correct tick and silently lost the circle around it. One mask,
     both shapes, one colour token. */
  background-color: var(--mark);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 26' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='13' cy='13' r='12.1' stroke-width='1.5'/%3E%3Cpath d='M7.6 13.3l3.6 3.6 7.2-7.4' stroke-width='2.2'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 26' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='13' cy='13' r='12.1' stroke-width='1.5'/%3E%3Cpath d='M7.6 13.3l3.6 3.6 7.2-7.4' stroke-width='2.2'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
  flex: none;
  margin-top: 3px;
}
.checks strong { display: block; font-family: var(--display); font-size: var(--step-1); font-weight: 800; letter-spacing: -0.025em; }
.checks p { color: var(--stone); font-size: 0.93rem; margin-top: 0.35rem; }

/* Compare */
.compare {
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  box-shadow: var(--sh-sm);
}
@media (min-width: 720px) { .compare { grid-template-columns: 1fr 1fr; } }
.compare__col { background: var(--white); padding: clamp(1.5rem, 3vw, 2.25rem); display: grid; gap: 1.1rem; align-content: start; }
.compare__col--us { background: var(--slate); box-shadow: none; }
.compare__col h3 { font-size: var(--step-1); }
.compare__col--us h3 { color: var(--bone); }
.compare__col ul { list-style: none; padding: 0; display: grid; gap: 0.7rem; font-size: 0.92rem; }
.compare__col li { display: grid; grid-template-columns: 20px 1fr; gap: 0.75rem; align-items: start; color: var(--stone); }
.compare__col li::before { content: '×'; color: var(--faint); font-size: 1.1rem; line-height: 1.3; }
.compare__col--us li { color: var(--bone-mute); }
.compare__col--us li::before { content: '✓'; color: var(--mark); }
.compare__col--soon li::before { content: '→'; color: var(--mark); font-size: 0.95rem; }
.compare__col--us .form-note { color: var(--bone-mute); }

/* ---------- 16. Testimonials ---------- */
.quote {
  background: var(--white);
  border: 1px solid var(--line);
  border-left: 3px solid var(--mark);
  border-radius: var(--r);
  padding: clamp(1.6rem, 3vw, 2.4rem);
  display: grid;
  gap: 1.25rem;
  align-content: start;
  box-shadow: var(--sh-sm);
}
.quote__stars { display: flex; gap: 3px; color: var(--mark); }
.quote__stars svg { width: 16px; height: 16px; }
.quote blockquote { font-size: var(--step-1); line-height: 1.5; letter-spacing: -0.015em; }
.quote figcaption { display: grid; gap: 0.2rem; font-size: 0.84rem; }
/* The reviewer name renders as <b> (config.js), so style that too, and give it
   a crisp readable colour — it was inheriting the tan --stone and reading badly
   on the dark review band. The "Verified Google review" line stays muted. */
.quote figcaption strong,
.quote__who b { font-family: var(--display); font-weight: 800; letter-spacing: -0.02em; font-size: 0.97rem; color: var(--ink); }
.quote__src { color: var(--faint); }
.quote figcaption span:not(.quote__mono) { color: var(--faint); }

/* ---------- 17. Accordion ---------- */
.acc { border-top: 1px solid var(--line-2); }
.acc__item { border-bottom: 1px solid var(--line); }
.acc__btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.15rem 0;
  text-align: left;
  font-family: var(--display);
  font-weight: 700;
  font-size: var(--step-1);
  letter-spacing: -0.025em;
  color: var(--ink);
  transition: color 0.25s;
}
.acc__btn:hover { color: var(--mark-hi); }
.acc__sign {
  flex: none;
  width: 30px; height: 30px;
  border: 1px solid var(--line-2);
  border-radius: 50%;
  display: grid;
  place-items: center;
  position: relative;
  transition: border-color 0.3s, background 0.3s, color 0.3s;
}
.acc__sign::before, .acc__sign::after {
  content: '';
  position: absolute;
  background: currentColor;
  transition: transform 0.35s var(--ease), opacity 0.25s;
}
.acc__sign::before { width: 12px; height: 1.5px; }
.acc__sign::after  { width: 1.5px; height: 12px; }
.acc__btn[aria-expanded="true"] .acc__sign { border-color: var(--mark); background: var(--volt); color: var(--bone); }
.acc__btn[aria-expanded="true"] .acc__sign::after { transform: rotate(90deg); opacity: 0; }
.acc__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.4s var(--ease); }
.acc__panel[data-open="true"] { grid-template-rows: 1fr; }
.acc__panel > div { overflow: hidden; }
.acc__panel p { color: var(--stone); padding-bottom: 1.25rem; max-width: 78ch; }
.acc__panel p + p { padding-top: 0.9rem; }

/* ---------- 18. Areas ---------- */
.areas { display: flex; flex-wrap: wrap; gap: 0.5rem; list-style: none; padding: 0; }
.areas li {
  border: 1px solid var(--line);
  background: var(--white);
  border-radius: 2px;
  padding: 0.48em 0.85em;
  font-size: 0.82rem;
  color: var(--stone);
  transition: all 0.25s;
}
.areas li:hover { border-color: var(--mark); color: var(--mark); }

/* ---------- 19. Quote form ---------- */
.quotebox {
  background: var(--white);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--sh-lg);
}
.quotebox__head {
  padding: clamp(1.4rem, 3vw, 2rem) clamp(1.4rem, 3vw, 2.25rem);
  border-bottom: 1px solid var(--line);
  background: var(--paper-2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.quotebox__head h3 { font-size: var(--step-1); }
.steps { display: flex; gap: 0.4rem; }
.steps span { width: 30px; height: 4px; background: var(--line-2); border-radius: 2px; transition: background 0.35s; }
.steps span.is-on { background: var(--volt); }
.quotebox__body { padding: clamp(1.4rem, 3vw, 2.25rem); }

.fstep { display: none; gap: 1.25rem; }
.fstep.is-active { display: grid; animation: fade 0.35s var(--ease) both; }
@keyframes fade { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.fstep__q { font-family: var(--display); font-weight: 800; font-size: var(--step-1); letter-spacing: -0.025em; }

.opts { display: grid; gap: 0.6rem; }
@media (min-width: 620px) { .opts--2 { grid-template-columns: 1fr 1fr; } }
.opt {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 1rem 1.15rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: all 0.25s;
  font-size: 0.93rem;
  background: var(--paper);
}
.opt:hover { border-color: var(--line-2); background: var(--paper-2); }
.opt input { position: absolute; opacity: 0; pointer-events: none; }
.opt__box {
  width: 20px; height: 20px;
  border: 1.5px solid var(--line-2);
  border-radius: 50%;
  flex: none;
  display: grid;
  place-items: center;
  transition: border-color 0.25s;
}
.opt__box::after {
  content: '';
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--volt);
  transform: scale(0);
  transition: transform 0.25s var(--ease);
}
.opt:has(input:checked) { border-color: var(--mark); background: rgba(31,58,44,0.06); }
.opt:has(input:checked) .opt__box { border-color: var(--mark); }
.opt:has(input:checked) .opt__box::after { transform: scale(1); }

.field { display: grid; gap: 0.45rem; }
.field label {
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--stone);
}
.field input, .field textarea, .field select {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 0.9rem 1rem;
  color: var(--ink);
  width: 100%;
  transition: border-color 0.25s, background 0.25s, box-shadow 0.25s;
  font-size: 0.95rem;
}
.field input::placeholder, .field textarea::placeholder { color: var(--faint); }
.field input:focus, .field textarea:focus, .field select:focus {
  outline: none;
  border-color: var(--mark);
  background: var(--white);
  box-shadow: 0 0 0 3px rgba(31,58,44,0.1);
}
.field textarea { resize: vertical; min-height: 120px; }

/* File input. The native control cannot be styled, so style the field around
   it and give its button the same shape as a ghost button. Two vendor
   selectors are needed because no browser agrees on the pseudo-element. */
.field input[type="file"] { padding: 0.55rem 0.7rem; cursor: pointer; }
.field input[type="file"]::file-selector-button,
.field input[type="file"]::-webkit-file-upload-button {
  font: inherit;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--mark);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: calc(var(--r) / 2);
  padding: 0.45rem 0.85rem;
  margin-right: 0.8rem;
  cursor: pointer;
  transition: background 0.25s, border-color 0.25s;
}
.field input[type="file"]::file-selector-button:hover,
.field input[type="file"]::-webkit-file-upload-button:hover {
  background: rgba(31, 58, 44, 0.06);
  border-color: var(--mark);
}
@media (min-width: 620px) { .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } }

.fnav { display: flex; justify-content: space-between; gap: 1rem; margin-top: 0.5rem; flex-wrap: wrap; }
.form-note { font-size: 0.78rem; color: var(--faint); }
.form-done { display: none; text-align: center; padding: 2rem 0; }
.form-done.is-on { display: grid; gap: 1rem; justify-items: center; animation: fade 0.4s var(--ease) both; }
.form-done__tick {
  width: 64px; height: 64px;
  border: 2px solid var(--mark);
  border-radius: 50%;
  display: grid; place-items: center;
  color: var(--mark);
}

/* ---------- 20. CTA band (inverted) ---------- */
.cta-band {
  position: relative;
  overflow: hidden;
  background: var(--slate-deep);
  color: var(--bone);
}
.cta-band .hazard {
  background: repeating-linear-gradient(-45deg, var(--sage) 0 12px, var(--slate-deep) 12px 24px);
  opacity: 1;
}
.cta-band__inner {
  padding-block: clamp(2.75rem, 5vw, 4.25rem);
  display: grid;
  gap: 2rem;
  position: relative;
  z-index: 1;
}
@media (min-width: 900px) {
  .cta-band__inner { grid-template-columns: minmax(0, 1.3fr) auto; align-items: center; gap: 3.5rem; }
}
.cta-band h2 { font-size: var(--step-3); color: var(--bone); }
.cta-band .lede { color: var(--bone-mute); }
.cta-band__actions { display: flex; flex-wrap: wrap; gap: 0.9rem; }
.cta-band .btn { --btn-bg: var(--bone); --btn-bd: var(--bone); --btn-fg: var(--bone); }
.cta-band .btn:hover { background: var(--clay-hi); border-color: var(--clay-hi); box-shadow: 0 14px 26px -16px rgba(0,0,0,0.6); color: var(--bone); }
.cta-band .btn--ghost { --btn-bg: transparent; --btn-fg: var(--bone); --btn-bd: var(--bone-line); }
.cta-band .btn--ghost:hover { background: var(--bone); color: var(--slate-deep); border-color: var(--bone); }

/* ---------- 21. Footer (inverted) ---------- */
.site-foot { background: var(--slate-deep); color: var(--bone-mute); }
.site-foot h4, .site-foot strong, .site-foot .brand__name { color: var(--bone); }
.site-foot .brand__name em { color: var(--mark); }
.site-foot .brand__sub { color: rgba(241,238,228,0.45); }
.foot-grid { display: grid; gap: 2.5rem; padding-block: clamp(3rem, 6vw, 4.5rem); }
@media (min-width: 760px)  { .foot-grid { grid-template-columns: repeat(2, 1fr); } }
/* One column per child — keep these in sync. This was four against five
   children, so "Get In Touch" wrapped onto a second row on its own and left a
   band of dead space beside it. It is now SIX: the reference pages (standards,
   warranty, safety) earned their own "Standards & Trust" column rather than
   being buried at the bottom of "Company", because they are the pages a
   homeowner comparing bids is actually hunting for.
   If you add a seventh child, this line changes too. */
@media (min-width: 1040px) {
  .foot-grid {
    grid-template-columns: 1.45fr 0.85fr 0.85fr 0.9fr 0.85fr 1.05fr;
    gap: 2.25rem 2rem;
  }
}
/* Between 760px and 1040px the grid is two columns, which leaves six children
   as three tidy rows — no change needed there. */
.foot-col h3 {
  font-size: 0.71rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(241,238,228,0.5);
  font-weight: 700;
  margin-bottom: 1.1rem;
  font-family: var(--body);
  line-height: 1.4;
  letter-spacing: 0.18em;
}
.foot-col ul { list-style: none; padding: 0; display: grid; gap: 0.6rem; font-size: 0.89rem; }
.foot-col a { color: var(--bone-mute); transition: color 0.2s; }
.foot-col a:hover { color: var(--mark); }
.foot-col .muted { color: var(--bone-mute); }
.foot-about p { color: var(--bone-mute); font-size: 0.91rem; margin-top: 1.1rem; max-width: 40ch; }
.foot-badges { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1.4rem; }
.badge {
  border: 1px solid var(--bone-line);
  border-radius: 2px;
  padding: 0.45em 0.8em;
  font-size: 0.67rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bone-mute);
  font-weight: 600;
}
.badge--acid { border-color: rgba(201,139,84,0.5); color: var(--mark); }
/* .foot-col a (0,2,0) outranks .btn (0,1,0) on `color`, which made the footer
   button bone-on-bone — invisible until the hover rule kicked in. Match specificity. */
.site-foot .btn { --btn-bg: var(--bone); --btn-bd: var(--bone); --btn-fg: var(--bone); }
.site-foot .foot-col a.btn,
.site-foot .foot-col a.btn:hover { color: var(--bone); }
.site-foot .btn:hover { background: var(--clay-hi); border-color: var(--clay-hi); color: var(--bone); }

.foot-bottom {
  border-top: 1px solid var(--bone-line);
  padding-block: 1.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  justify-content: space-between;
  align-items: center;
  font-size: 0.79rem;
  color: rgba(241,238,228,0.5);
}
.foot-bottom a { color: rgba(241,238,228,0.5); }
.foot-bottom a:hover { color: var(--mark); }
.foot-bottom ul { display: flex; gap: 1.25rem; list-style: none; padding: 0; flex-wrap: wrap; }

.social { display: flex; gap: 0.5rem; }
.social a {
  width: 38px; height: 38px;
  border: 1px solid var(--bone-line);
  border-radius: 2px;
  display: grid;
  place-items: center;
  color: var(--bone-mute);
  transition: all 0.25s;
}
.social a:hover { border-color: var(--mark); color: var(--mark); }
.social svg { width: 17px; height: 17px; }

/* ---------- 22. Mobile call bar ---------- */
.callbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 90;
  display: none;
  gap: 1px;
  background: var(--line-2);
  border-top: 1px solid var(--line-2);
  box-shadow: 0 -10px 30px -18px rgba(22,24,15,0.6);
}
.callbar a {
  flex: 1;
  background: var(--white);
  color: var(--ink);
  padding: 1rem;
  text-align: center;
  font-family: var(--display);
  font-weight: 800;
  font-size: 0.77rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
}
.callbar a.is-primary { background: var(--clay); color: var(--bone); }
@media (max-width: 780px) {
  .callbar { display: flex; }
  /* Reserving the space on <body> exposed the pale page background as a strip
     under the dark footer. Let the footer own that space instead. */
  .site-foot { padding-bottom: 58px; }
}

/* ---------- 23. Scroll reveal ---------- */
/* SCOPED TO `.js`, WHICH IS THE WHOLE POINT. Unscoped, this hid 38 elements on
   the home page behind an opacity the stylesheet sets and only JavaScript can
   undo — so a script error, a blocked file, or any client that applies CSS
   without executing JS renders most of the page invisible. The class is added
   by config.js, the one script loaded synchronously in the head. No script, no
   class, no hiding. See the note there. */
/* `.js .js [data-reveal]` — a doubled class, and it can never match: `.js` is
   on <html>, so this asked for a .js element nested inside a .js element. The
   effect was that the fade-in below was never armed. Nothing looked broken,
   which is why it survived: elements simply appeared at full opacity. What did
   show was the OTHER half of the pair — `.js [data-reveal]` in §6 sets a 9px
   offset that only `.is-in` removes — so every revealed block on the site
   arrived by jumping 9px rather than by fading, which reads as jitter on scroll
   and as nothing at all if you are not looking for it.

   RESTORED, HAVING BEEN FIXED ONCE ALREADY. This exact repair was made in
   a9c72a7 and lost in fc72899a, the revert that took the site back from Extra
   Fresh Bins to DZT. That revert was right about the branding and blind about
   everything else in the same commit: a business rename and a real bug fix
   travelled together, and reverting by commit could only take both. The two
   sibling rules below kept the single `.js` throughout, which is what made the
   mismatch findable — a selector that disagrees with the two rules it was
   written to pair with is either the bug or the fix, and never neither. */
/* KEYED ON `reveal-armed`, WHICH main.js SETS, NOT ON `.js`, WHICH config.js
   SETS. The two are different files and only one of them can undo this. See
   the note above reveal() in main.js: with `.js` as the gate, a main.js that
   never runs left 26 nodes at opacity 0 permanently. Now the hiding and the
   showing are armed and disarmed by the same script, so the failure mode is
   "no animation" rather than "no page". */
.reveal-armed [data-reveal] {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
  transition-delay: var(--d, 0ms);
}
.reveal-armed [data-reveal].is-in { opacity: 1; transform: none; }

/* ---------- 24. Utilities ---------- */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
/* Parked off-canvas with clip-path rather than left:-9999px. The old trick
   still counts toward the document's scroll width, which is what pushed
   services.html 4px wider than the viewport on a phone. */
.skip {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 999;
  background: var(--volt);
  color: var(--bone);
  padding: 0.9rem 1.4rem;
  font-weight: 700;
  clip-path: inset(50%);
}
.skip:focus { clip-path: none; }

/* Band backgrounds — class names kept stable across the palette change */
.bg-ink2, .bg-surface { background: var(--paper-2); }
.bordered-top { border-top: 1px solid var(--line); }
.text-center { text-align: center; }
.mt-lg { margin-top: clamp(2rem, 4vw, 3rem); }
.mt-md { margin-top: 1.5rem; }
.max-60 { max-width: 60ch; }
.flex-row { display: flex; flex-wrap: wrap; gap: 0.9rem; align-items: center; }

/* ---- .stack — WAS NEVER WRITTEN ------------------------------------------
 *
 * Eighteen uses across about, index, privacy and terms, and not one line of
 * CSS behind it. The reset at the top of this file is `* { margin: 0 }`, so
 * every one of those blocks rendered with EXACTLY 0px between its paragraphs
 * — measured on about.html: three paragraphs at tops 51, 157, 245 and bottoms
 * 157, 245, 332, butted edge to edge. It reads as one grey slab of text, which
 * is what "the text is glitched" looks like when the cause is a missing rule
 * rather than a broken one. Same failure as `.srv-detail`: markup shipped
 * against a class nobody wrote.
 *
 * MARGINS, NOT GRID. `display: grid` would make every direct child a grid
 * item, and several of these stacks have a bare <br> or an inline <span> at
 * the top level — each would claim its own row and open a gap where the author
 * meant a line break. The owl selector leaves inline boxes alone, because
 * vertical margins do nothing on them. That is the property being relied on,
 * so it is worth saying out loud.
 *
 * Headings get asymmetric space: more above than below, so an <h2> belongs to
 * the paragraph under it rather than floating between two. privacy.html and
 * terms.html are entirely h2/p pairs and this is the whole reason they scan.
 */
/* ONE RHYTHM, THREE CONTAINERS. `.stack`, `.area-deep` and the reference-page
   `.refbody > section` are the same object — a heading and the prose under it
   — and all three had the same bug for the same reason, so they get one rule
   set rather than three that will drift apart.

   The pattern is the owl selector: space goes BETWEEN siblings, never on the
   first or last child, so a container never contributes stray margin to
   whatever it sits in. Headings take more above than below, which is what
   binds a heading to its own paragraph instead of leaving it floating between
   two. An `.eyebrow` is the exception in the other direction: it is a label
   ON the heading below it, so it hugs. */
.stack > * + *,
.area-deep > * + *,
.refbody > section > * + *          { margin-top: 1.05rem; }

.stack > * + h2,
.stack > * + h3,
.area-deep > * + h3,
.refbody > section > * + h2,
.refbody > section > * + h3         { margin-top: clamp(1.75rem, 3vw, 2.5rem); }

.stack > h2 + *,
.stack > h3 + *,
.area-deep > h3 + *,
.refbody > section > h2 + *,
.refbody > section > h3 + *         { margin-top: 0.55rem; }

.stack > .eyebrow + *,
.area-deep > .eyebrow + *,
.refbody > section > .eyebrow + *   { margin-top: 0.4rem; }

/* Tables and callouts are blocks, not sentences — they need the air a heading
   gets, in both directions. */
.refbody > section > * + .spec-wrap,
.refbody > section > * + .callout,
.refbody > section > .spec-wrap + *,
.refbody > section > .callout + *   { margin-top: clamp(1.6rem, 3vw, 2.4rem); }

.stack--lg > * + * { margin-top: clamp(1.2rem, 2.2vw, 1.7rem); }

/* ---- .area-deep — ALSO NEVER WRITTEN ------------------------------------
 *
 * Fourteen sections across the eight city pages, no rule behind any of them.
 * Reported from granite-bay.html and visible on every one: `<h3>Heritage
 * oaks…</h3>` sat ON the last line of the paragraph above it, because the
 * reset zeroes margins and nothing put them back. Three separate undefined
 * classes now — .srv-detail, .stack and this — which is the same mistake
 * three times: markup shipped against CSS that was never written. The class
 * diff that found it is worth keeping; see the commit message.
 *
 * These blocks are the long-form part of a city page, sitting after the grid
 * of short facts, so they need three things the grid does not:
 *
 *   A MEASURE. They were running the full 1208px container — about 110
 *   characters a line, where prose is comfortable at 60–75. That alone reads
 *   as "sloppy" before a single word is read, because the eye loses the line
 *   it is returning to. 68ch caps it without hard-coding a pixel width, so it
 *   tracks the font size.
 *
 *   A SEPARATION. A rule above rather than a box around, which is the same
 *   decision §45 made everywhere else on the site.
 *
 *   THE RHYTHM ABOVE, shared with .stack rather than restated, because they
 *   are the same object: a heading and its paragraphs.
 */
.area-deep {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: clamp(1.75rem, 3.5vw, 2.75rem);
  border-top: 1px solid var(--line);
  max-width: 68ch;
}

/* ---------- 25. Condensed process strip (home page) ---------- */
.steps-strip {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  box-shadow: var(--sh-sm);
  counter-reset: step;
}
@media (min-width: 640px)  { .steps-strip { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .steps-strip { grid-template-columns: repeat(5, 1fr); } }
.steps-strip li {
  background: var(--white);
  padding: clamp(1.3rem, 2.4vw, 1.85rem);
  display: grid;
  gap: 0.55rem;
  align-content: start;
  transition: background 0.3s;
}
.steps-strip li:hover { background: var(--paper-3); }
.steps-strip__num {
  font-family: var(--display);
  font-weight: 900;
  font-size: 1.5rem;
  line-height: 1;
  letter-spacing: -0.04em;
  color: transparent;
  -webkit-text-stroke: 1.4px var(--mark);
}
.steps-strip h3 { font-size: var(--step-1); }
.steps-strip p { color: var(--stone); font-size: 0.89rem; line-height: 1.55; }

.inline-link {
  color: var(--mark);
  font-weight: 600;
  box-shadow: inset 0 -1px 0 var(--line-warm);
  transition: box-shadow 0.25s;
}
.inline-link:hover { box-shadow: inset 0 -2px 0 var(--mark); }

/* ==========================================================================
   26. GLASS LAYER
   Frosted surfaces over an ambient field. Everything degrades to the flat
   design if backdrop-filter is unsupported, so nothing depends on it.
   ========================================================================== */

:root {
  --glass:        rgba(255, 255, 255, 0.58);
  --glass-strong: rgba(255, 255, 255, 0.74);
  --glass-edge:   rgba(255, 255, 255, 0.92);
  --glass-ring:   rgba(27, 34, 41, 0.07);
  --glass-lift:   0 1px 0 rgba(255,255,255,0.85) inset,
                  0 0 0 1px var(--glass-ring),
                  0 16px 40px -26px rgba(27, 34, 41, 0.42);
  --glass-lift-h: 0 1px 0 rgba(255,255,255,0.95) inset,
                  0 0 0 1px rgba(9, 94, 65, 0.16),
                  0 26px 60px -30px rgba(27, 34, 41, 0.5);
  --blur: blur(18px) saturate(150%);
}

/* Ambient field — gives the glass something to refract, and stops the page
   reading as flat paper. Fixed, so it drifts under content as you scroll. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(58rem 40rem at 10% -8%,  rgba(9, 94, 65, 0.13), transparent 62%),
    radial-gradient(52rem 38rem at 94% 6%,   rgba(55, 69, 80, 0.15), transparent 64%),
    radial-gradient(44rem 34rem at 74% 98%,  rgba(9, 94, 65, 0.09), transparent 60%);
}
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(rgba(27,34,41,0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(27,34,41,0.05) 1px, transparent 1px);
  background-size: 68px 68px;
  -webkit-mask-image: radial-gradient(ellipse 130% 80% at 50% 0%, #000 5%, transparent 68%);
  mask-image: radial-gradient(ellipse 130% 80% at 50% 0%, #000 5%, transparent 68%);
}

/* Light bands go translucent so the ambient field shows through */
.bg-ink2, .bg-surface { background: rgba(233, 237, 234, 0.62); }
.pagehero { background: rgba(233, 237, 234, 0.52); }
.hero-facts { background: rgba(243, 245, 243, 0.55); }

/* ---- Frosted surfaces ---- */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .card,
  .quote,
  .quotebox,
  .feature,
  .stat,
  .compare__col,
  .steps-strip li,
  .areas li,
  .tag,
  .filter,
  .proj__img {
    background: var(--glass);
    -webkit-backdrop-filter: var(--blur);
    backdrop-filter: var(--blur);
    border-color: var(--glass-edge);
    box-shadow: var(--glass-lift);
  }
  .card:hover, .feature:hover, .quote:hover { box-shadow: var(--glass-lift-h); }
  .proj:hover .proj__img { box-shadow: var(--glass-lift-h); }

  .stats, .compare { background: var(--glass-ring); border-color: var(--glass-edge); }
  /* WHITE GLASS IS STILL GLASS, AND THAT IS THE TRAP. The tokens above
     (--glass, --glass-edge) were re-pointed for the dark theme; these four
     literals were written in the same block and were not, so they went on
     frosting the page white underneath cream text. A dark theme's glass is a
     LIFT off the ground, not a wash toward white. */
  .quotebox__head { background: rgba(255,255,255,0.05); }
  .opt, .field input, .field textarea, .field select { background: rgba(255,255,255,0.05); }
  .opt:hover { background: rgba(255,255,255,0.10); }

  /* header glass lives in §29 — nothing paints the bar itself */
  .callbar a { background: rgba(20, 24, 20, 0.86); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); }
  .drawer { background: rgba(16, 20, 16, 0.92); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); }
}

/* Specular top edge — the detail that sells glass */
.card, .quotebox, .feature, .quote, .steps-strip li, .stat {
  position: relative;
  isolation: isolate;
}
.card::after, .quotebox::after, .feature::after, .quote::after, .steps-strip li::after {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.95) 22%, rgba(255,255,255,0.95) 78%, transparent);
  pointer-events: none;
  z-index: 2;
}

/* Slightly softer geometry reads more "product", less "brochure" */
:root { --r-sm: 6px; --r: 12px; --r-lg: 18px; }
.card, .quote, .stat, .compare__col, .steps-strip li { border-radius: var(--r); }
.stats, .compare, .steps-strip, .quotebox, .feature, .proj__img { border-radius: var(--r-lg); }
.btn { border-radius: 8px; }
.filter, .areas li { border-radius: 100px; }
.tag { border-radius: 100px; }

/* ---- Buttons: subtle depth + a controlled glow ---- */
.btn {
  background-image: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0) 62%);
  box-shadow: 0 1px 0 rgba(255,255,255,0.22) inset, 0 8px 20px -14px rgba(9,94,65,0.7);
}
.btn:hover {
  box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,
              0 14px 30px -14px rgba(9,94,65,0.62),
              0 0 0 4px rgba(9,94,65,0.10);
}
.btn--ghost { background-image: none; box-shadow: none; }
.btn--ghost:hover { box-shadow: 0 0 0 4px rgba(9,94,65,0.10); }

/* ---- Inverted bands: depth instead of flat fill ---- */
.statement, .cta-band, .site-foot { position: relative; isolation: isolate; }
.statement::before, .cta-band::before, .site-foot::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(46rem 30rem at 88% 0%,  rgba(9, 94, 65, 0.40), transparent 62%),
    radial-gradient(38rem 26rem at 4% 100%, rgba(127, 183, 154, 0.14), transparent 60%);
}
.statement::after, .cta-band::after {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(243,245,244,0.35), transparent);
}

/* Hero: lift the headline off the photo without a heavier scrim */
.hero--photo h1 { text-shadow: 0 2px 30px rgba(12, 18, 22, 0.45); }
.hero--photo .hero__lede { text-shadow: 0 1px 16px rgba(12, 18, 22, 0.5); }

/* Fact strip reads as an instrument panel */
.hero-facts { border-top: 1px solid rgba(255,255,255,0.6); }
.hero-facts .hero__meta dd { letter-spacing: -0.01em; }

@media (prefers-reduced-transparency: reduce) {
  .card, .quote, .quotebox, .feature, .stat, .compare__col, .steps-strip li, .proj__img {
    background: var(--white);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* Green accent is unreadable on the slate bands — swap to sage there */
.statement .clay,
.statement .acid,
.cta-band .clay,
.cta-band .acid,
.site-foot .clay,
.site-foot .acid,
.compare__col--us .clay { color: var(--mark); }

/* Survey tape: keep the construction cue, dial down the noise */
.hazard { height: 7px; opacity: 0.85; }
.hazard--thin { height: 3px; }

/* ==========================================================================
   27. Hero slideshow + glass header over the photo
   ========================================================================== */

/* ---- Buttons: flat and confident. The gradient + glow read like an ad. ---- */
.btn { background-image: none; box-shadow: none; }
.btn:hover { box-shadow: 0 10px 22px -14px rgba(9, 94, 65, 0.55); }
.btn::after { display: none; }          /* kill the sweep-shine */
.btn--ghost, .btn--ghost:hover { box-shadow: none; }

/* ---- Slides ---- */
.hero__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 42%;
  /* NO `opacity: 0` HERE ANY MORE, AND THAT PAIR HAD TO GO TOGETHER. This rule
     used to hide every slide and let `.is-active` bring one back, which is how
     a crossfade works. There is one slide now. Deleting the crossfade without
     deleting the `opacity: 0` under it would leave the only photograph on the
     page invisible and the page would look like it had failed to load. */
}

/* .hero__dots / .hero__dot deleted with the carousel they steered. */

/* Header sits above the hero, not over it — the photo starts below the bar. */
.hero--photo { margin-top: 0; padding-top: clamp(4rem, 12vh, 7rem); }

/* ==========================================================================
   28. Dark ADU band — breaks up the run of pale sections mid-page
   ========================================================================== */
.dark-band {
  background: var(--slate);
  color: var(--bone);
  position: relative;
  isolation: isolate;
  border-block: 1px solid rgba(243, 245, 244, 0.10);
}
.dark-band::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(48rem 32rem at 88% 0%,   rgba(9, 94, 65, 0.42), transparent 62%),
    radial-gradient(38rem 26rem at 2% 100%,  rgba(127, 183, 154, 0.13), transparent 60%);
}
.dark-band h2 { color: var(--bone); }
.dark-band .lede,
.dark-band p { color: var(--bone-mute); }
.dark-band .eyebrow { color: var(--sage); }
.dark-band .eyebrow::before { background: var(--sage); }
.dark-band .link-x {
  color: var(--sage);
  background-image: linear-gradient(var(--mark), var(--mark));
}
.dark-band .btn { --btn-bg: var(--bone); --btn-bd: var(--bone); --btn-fg: var(--bone); }
.dark-band .btn:hover { background: var(--clay-hi); border-color: var(--clay-hi); color: var(--bone); }


/* ==========================================================================
   29. Header — light frosted glass
   A dark bar with a green glow read as gaming/tech. Light frost over the
   paper background is the premium register and keeps the brand green for
   the one thing that should shout: the CTA.
   ========================================================================== */
/* The topbar rides the dark band token rather than a hex sampled from a logo
   that no longer exists. */
.topbar {
  background: var(--slate);
  color: rgba(243, 245, 244, 0.72);
  border-bottom: 1px solid rgba(243, 245, 244, 0.10);
}
.topbar strong { color: var(--bone); }
.topbar .acid { color: var(--sage); }

/* The bar is invisible; the glass is the panel inside it.
   Dark glass on a dark band, because a light panel needs so much opacity to
   carry dark text that it stops being transparent at all. Dark glass holds
   real transparency AND high contrast — it just needs light text.
   Fill firms up on scroll so the panel stays dark over pale page content. */
.site-head {
  background: transparent;
  border-bottom: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  padding: 0.85rem var(--gutter);
  transition: padding 0.35s var(--ease);
}
.site-head .nav {
  position: relative;
  max-width: var(--max);
  margin-inline: auto;
  --pad: clamp(1.1rem, 2vw, 2rem);
  /* `--mark: 58px` WAS HERE AND IT WAS A TRAP. --mark is the steel colour on
     :root (#9EA9B3) — the colour of every "green" word, link, eyebrow, rule
     and icon on the site. Redefining it here as a LENGTH shadowed the colour
     for every descendant of the masthead, so any `var(--mark)` written inside
     the header silently resolved to `58px`. The 2px rule under the nav is
     built from `linear-gradient(90deg, var(--mark) 0 ..., ...)`, which makes
     the whole gradient invalid, which makes `background` fall back to none —
     so that rule has never painted, on any page, since the token was added.
     It sized `.brand__mark`, a class that appears in none of the 25 HTML
     files; the mark is `.brand__logo`. A token that only breaks things and
     styles nothing. Gone, and --mark means one thing everywhere again. */
  padding-inline: var(--pad);
  min-height: 84px;
  border-radius: 22px;
  background: none;
  border: 0;
  box-shadow: none;
}
/* The glass sheet. It lives on a pseudo-element so the circular mask can cut a
   hole in it without clipping .brand, which is a real child of .nav. */
.site-head .nav::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  background: linear-gradient(180deg, rgba(20, 27, 33, 0.95), rgba(12, 18, 23, 0.97));
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.10),
    inset 0 1px 0 rgba(255, 255, 255, 0.20),
    inset 0 -14px 26px -20px rgba(0, 0, 0, 0.9),
    0 2px 4px rgba(0, 0, 0, 0.18),
    0 22px 44px -24px rgba(27, 34, 41, 0.45);
  transition: background 0.4s, box-shadow 0.4s;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .site-head .nav::after {
    background: linear-gradient(180deg, rgba(20, 27, 33, 0.90), rgba(12, 18, 23, 0.94));
    -webkit-backdrop-filter: blur(26px) saturate(150%) contrast(1.05);
    backdrop-filter: blur(26px) saturate(150%) contrast(1.05);
  }
  /* Over pale content the sheet has to hold its own darkness for the text */
  .site-head.is-stuck .nav::after {
    background: linear-gradient(180deg, rgba(18, 25, 31, 0.86), rgba(12, 18, 23, 0.90));
  }
}
.site-head.is-stuck { padding-block: 0.5rem; }
.site-head.is-stuck .nav::after {
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.14),
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    inset 0 -14px 26px -20px rgba(0, 0, 0, 0.9),
    0 26px 54px -28px rgba(0, 0, 0, 0.8);
}
@media (max-width: 699.98px) {
  .site-head { padding: 0.5rem 0.7rem; }
  .site-head .nav { border-radius: 16px; min-height: 68px; --pad: 0.9rem; }
}

.site-head .brand__name { color: var(--bone); }
.site-head .brand__name em { color: var(--mark); }
.site-head .brand__sub  { color: rgba(243, 245, 244, 0.55); }

.site-head .nav__links a { color: rgba(243, 245, 244, 0.78); }
.site-head .nav__links a:hover { color: var(--bone); background: rgba(243, 245, 244, 0.10); }
.site-head .nav__links a[aria-current="page"] { color: var(--bone); }
.site-head .nav__links a[aria-current="page"]::after { background: var(--sage); }

.site-head .nav__phone { color: var(--bone); }
.site-head .nav__phone:hover { color: var(--sage); }

.site-head .nav__toggle { border-color: rgba(243, 245, 244, 0.3); }
.site-head .nav__toggle span,
.site-head .nav__toggle span::before,
.site-head .nav__toggle span::after { background: var(--bone); }

/* The CTA is the only saturated element up here.
   THE LABEL IS THE PAGE GROUND, NOT THE CREAM. Cream on this green is 2.65:1 —
   it looked fine to write and is unreadable to use. The dark ground on the same
   green is 5.53:1, and it is the same relationship a physical sign has: the
   colour is the field, the text is the hole punched in it.
   The hover was worse: `background: var(--ink)` with `color: var(--bone)` was
   written when --ink was near-black and --bone was cream. The dark inversion
   made both #EFE9DD, so the label disappeared into its own button at 1.00:1. */
.site-head .btn {
  --btn-bg: var(--clay);
  --btn-bd: var(--clay);
  --btn-fg: var(--bone);
}
.site-head .btn:hover {
  background: var(--clay-hi);
  border-color: var(--clay-hi);
  color: var(--bone);
  box-shadow: 0 10px 24px -14px rgba(138, 154, 160, 0.55);
}

/* Drawer matches the bar */
/* THE DRAWER WAS STILL CREAM. This is the same inversion bug as the buttons and
   it is the worst instance of it, because the drawer IS the navigation on a
   phone: a near-white panel with --ink links on it, which is now also cream —
   1.06:1, an empty white sheet with a close button. It never showed up in a
   screenshot because the drawer only exists once it is opened. */
.drawer { background: rgba(20, 24, 20, 0.97); }
@supports (backdrop-filter: blur(1px)) {
  .drawer { background: rgba(16, 20, 16, 0.88); -webkit-backdrop-filter: blur(26px) saturate(140%); backdrop-filter: blur(26px) saturate(140%); }
}
.drawer__links a { color: var(--ink); border-bottom-color: var(--line); }
.drawer__links a span { color: var(--faint); }
.drawer__links a[aria-current="page"] { color: var(--mark); }
.drawer .btn { --btn-bg: var(--clay); --btn-bd: var(--clay); --btn-fg: var(--bone); }
.drawer .btn--ghost { --btn-bg: transparent; --btn-fg: var(--ink); --btn-bd: var(--line-2); }
.drawer .btn--ghost:hover { background: var(--volt); color: var(--bone); border-color: var(--mark); }

/* ==========================================================================
   30. Mobile ergonomics
   Measured at 500px: the phone link collapsed to a bare 15x15 icon, footer
   links were 17px tall, filter chips 38px, and the brand sub-label 8.8px.
   ========================================================================== */
@media (max-width: 780px) {
  /* Primary call action must be thumb-sized, not an icon */
  .nav__phone {
    min-width: 46px;
    min-height: 46px;
    justify-content: center;
    border: 1px solid rgba(243, 245, 244, 0.28);
    border-radius: 8px;
    gap: 0;
  }
  .nav__phone svg { width: 19px; height: 19px; }

  .foot-col ul { gap: 0.15rem; }
  .foot-col ul a {
    display: block;
    padding-block: 0.62rem;      /* 17px -> ~40px touch height */
  }
  .foot-bottom ul { gap: 0.25rem 1.5rem; }
  .foot-bottom ul a { display: inline-block; padding-block: 0.72rem; }
  .social a { width: 44px; height: 44px; }

  .link-x { padding-block: 0.55rem; }
  .crumbs a { display: inline-block; padding-block: 0.35rem; }
}

.filter { min-height: 44px; }                 /* was 38px */
.brand__sub { font-size: 0.66rem; }           /* was 0.55rem / 8.8px */
@media (max-width: 559.98px) {
  .brand__mark { width: 46px; height: 46px; }
  .brand__sub { font-size: 0.63rem; letter-spacing: 0.16em; }
}

/* ==========================================================================
   31. Estimate form: grouping, step counter, focus
   ========================================================================== */
.fstep__set { border: 0; margin: 0; padding: 0; min-width: 0; display: grid; gap: 1.25rem; }
.fstep__set > .fstep__q { padding: 0; }
legend.fstep__q { display: block; width: 100%; }

.steps__label {
  font-family: var(--display);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--stone);
  margin: 0;
}
.quotebox__head { align-items: baseline; }
.quotebox__head > h3 { margin-right: auto; }

/* Focus lands on the step wrapper after Next/Back — make that legible
   without a jarring outline on the whole panel. */
/* The step wrapper is focused programmatically so screen readers follow the
   change; it isn't an interactive control, so a ring around the whole panel
   is just noise. The live region carries the announcement instead. */
.fstep[tabindex="-1"]:focus,
.fstep[tabindex="-1"]:focus-visible { outline: none; }
.form-done:focus { outline: none; }

/* ==========================================================================
   32. Compact layout for phones and tablets
   Measured at 500px the page ran 13,142px (~15 screens); the capability grid
   alone was 2,525px, projects 1,684px. Applied up to 900px because 768px
   measured TALLER than 500px once the columns had collapsed but the
   compaction hadn't kicked in.
   ========================================================================== */
@media (max-width: 899.98px) {
  .section { padding-block: clamp(3rem, 9vw, 4.5rem); }
  .section--tight { padding-block: 2.5rem; }
  .section-head { margin-bottom: 1.4rem; gap: 1rem; }
  .statement__inner { padding-block: 2.5rem; gap: 1.5rem; }
  .cta-band__inner { padding-block: 2.5rem; }

  /* Capabilities: nine stacked cards was the single tallest block.
     Two-up with compact type keeps all nine and halves the rows. */
  #build .grid-3 { grid-template-columns: 1fr 1fr; gap: 0.7rem; }
  #build .card { padding: 0.95rem 0.85rem; gap: 0.5rem; border-radius: 10px; }
  #build .card__icon { width: 34px; height: 34px; border-radius: 8px; }
  #build .card__icon svg { width: 17px; height: 17px; }
  #build .card h3 { font-size: 0.92rem; line-height: 1.15; }
  #build .card p { font-size: 0.76rem; line-height: 1.45; }
  #build .grid-3 > :last-child:nth-child(odd) { grid-column: 1 / -1; }

  /* Projects: 4:3 frames stacked were 375px of image each */
  .proj__img { aspect-ratio: 16 / 9; }
  .proj-grid { gap: 1.15rem; }
  .proj__body { gap: 0.35rem; }

  /* THE `.quotes` SNAP-SCROLLER WAS DELETED FROM HERE, and it is worth saying
     why in the place it used to be. No element on this site has ever had
     class="quotes" — the reviews container is `.quote-grid`, filled by
     config.js. This block was a near-exact twin of the LIVE scroller ~5,400
     lines below, comment and all, so anyone tuning the mobile review swipe
     found this one first, changed it, saw nothing happen, and changed it
     harder. A decoy is worse than dead code. The real one is under
     `.quote-grid` in the mobile section near the end of the file. */

  /* Process + ADU + areas: tighten without losing anything */
  .steps-strip li { padding: 1rem 1.1rem; gap: 0.35rem; }
  .steps-strip p { font-size: 0.84rem; }
  .areas { gap: 0.35rem; }
  .areas li { padding: 0.42em 0.75em; font-size: 0.78rem; }
  .acc__btn { padding: 1rem 0; }
  .split { gap: 1.75rem; }
  .hero-facts .hero__meta { gap: 0.9rem 1.75rem; padding-block: 1rem; }
}

@media (max-width: 899.98px) {
  /* Home projects: swipe row. The full gallery on projects.html stays
     stacked — that page is a browse experience, this is a teaser. */
  #work .proj-grid {
    display: flex;
    gap: 0.85rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 0.5rem;
  }
  #work .proj-grid::-webkit-scrollbar { display: none; }
  #work .proj { flex: 0 0 78%; scroll-snap-align: start; }

  /* Process: five short-titled steps read fine two-up */
  .steps-strip { grid-template-columns: 1fr 1fr; gap: 0.6rem; background: none; border: 0; box-shadow: none; }
  .steps-strip li { border: 1px solid var(--line); border-radius: 10px; }
  .steps-strip li:last-child { grid-column: 1 / -1; }
  .steps-strip__num { font-size: 1.25rem; }
  .steps-strip h3 { font-size: 0.98rem; }
  .steps-strip p { font-size: 0.8rem; }

  /* Estimate section: the intro column can be tighter above the form */
  #estimate .checks { gap: 0.75rem; }
  #estimate .checks p { font-size: 0.85rem; }
}

/* ==========================================================================
   33. Compact footer
   Four stacked columns ran 1,505px — almost two screens of footer.
   ========================================================================== */
@media (max-width: 899.98px) {
  .foot-grid {
    grid-template-columns: 1fr 1fr;
    gap: 1.6rem 1.25rem;
    padding-block: 2.25rem 1.5rem;
  }
  .foot-about { grid-column: 1 / -1; }
  .foot-about p { font-size: 0.85rem; max-width: none; margin-top: 0.8rem; }
  .foot-badges { margin-top: 1rem; gap: 0.35rem; }
  .badge { padding: 0.38em 0.65em; font-size: 0.63rem; }
  .social { margin-top: 1rem; }

  .foot-col h3 { margin-bottom: 0.6rem; }
  .foot-col ul { gap: 0; font-size: 0.86rem; }
  .foot-col ul a { padding-block: 0.46rem; }        /* ~36px, still tappable */

  /* Service Areas is the longest list by far and used to sit in one narrow
     column with nothing beside it. Full width in two sub-columns halves its
     height and fills the gap it was leaving. */
  .foot-areas { grid-column: 1 / -1; }
  .foot-areas ul { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1rem; }

  .foot-col:last-child { grid-column: 1 / -1; }     /* contact details full width */
  .foot-col:last-child ul { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1rem; }
  .foot-col:last-child ul li:nth-child(3) { grid-column: 1 / -1; }
  .foot-col:last-child .btn { margin-top: 1rem; width: 100%; }

  .foot-bottom { padding-block: 1.1rem; gap: 0.5rem; font-size: 0.75rem; }
  .foot-bottom ul { gap: 0 1.1rem; }
}

/* ==========================================================================
   34. Tablet density
   768px measured 10,205px — taller than the 500px layout, because the
   compaction in §32 stopped at 700px while the columns had already
   collapsed. Apply the spacing half of it up to 900px.
   ========================================================================== */
@media (max-width: 899.98px) {
  .section { padding-block: clamp(3.25rem, 8vw, 5rem); }
  .section--tight { padding-block: 2.75rem; }
  .section-head { margin-bottom: 1.6rem; }
  .statement__inner { padding-block: 2.75rem; }
  .cta-band__inner { padding-block: 2.75rem; }
  .feature__body { padding: 1.6rem; }
  .steps-strip li { padding: 1.1rem 1.2rem; }
  .acc__btn { padding: 1.05rem 0; }
  .foot-grid { padding-block: 2.5rem 1.75rem; gap: 1.75rem; }
  .foot-col ul a { padding-block: 0.52rem; }
}

/* ==========================================================================
   35. Material warmth
   Flat digital surfaces read clinical. A fine grain, warm light and a
   little imperfection make them feel like paper and stone.
   ========================================================================== */

/* Grain over the whole page — invisible individually, felt collectively.
   SVG turbulence, so it costs nothing to download. */
body::after {
  background-image:
    linear-gradient(rgba(27,34,41,0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(27,34,41,0.05) 1px, transparent 1px),
    url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  background-size: 68px 68px, 68px 68px, 140px 140px;
  opacity: 0.55;
}

/* Warmer ambient light, so the page feels lit rather than printed */
body::before {
  background:
    radial-gradient(46rem 22rem at 14% -4%, rgba(9, 94, 65, 0.20), transparent 66%),
    radial-gradient(40rem 20rem at 88% -3%, rgba(176, 140, 90, 0.22), transparent 68%),
    radial-gradient(58rem 40rem at 10% 6%,  rgba(9, 94, 65, 0.08), transparent 62%),
    radial-gradient(44rem 34rem at 74% 98%,  rgba(9, 94, 65, 0.08), transparent 60%);
}

/* Cards were uniformly flat. A faint warm top-light gives them a surface. */
.card, .quote, .stat, .steps-strip li {
  background-image: linear-gradient(180deg, rgba(255,255,255,0.7), rgba(255,255,255,0) 42%);
}

/* ==========================================================================
   36. Owner's note
   A signed, first-person moment. For a contractor this does more for trust
   than any amount of copy written in the third person.
   ========================================================================== */
.ownernote {
  margin: 0;
  max-width: 60rem;
  display: grid;
  gap: 1.5rem;
  padding: clamp(1.75rem, 4vw, 3rem) clamp(1.5rem, 4vw, 3.25rem);
  background: var(--white);
  border: 1px solid var(--line);
  border-left: 3px solid var(--mark);
  border-radius: var(--r-lg);
  box-shadow: var(--sh);
  position: relative;
}
.ownernote::before {
  content: '';
  position: absolute;
  top: clamp(1.4rem, 3vw, 2.2rem);
  right: clamp(1.4rem, 3vw, 2.4rem);
  width: 46px; height: 46px;
  border-radius: 50%;
  background: var(--mark);
  opacity: 0.07;
}
.ownernote blockquote { margin: 0; display: grid; gap: 1rem; }
.ownernote blockquote p {
  font-size: var(--step-1);
  line-height: 1.55;
  letter-spacing: -0.01em;
  color: var(--ink);
  max-width: 52ch;
}
.ownernote figcaption {
  display: grid;
  gap: 0.2rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--line);
}
.ownernote__sig {
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.15rem;
  letter-spacing: -0.02em;
  color: var(--ink);
}
.ownernote__role { font-size: 0.82rem; color: var(--faint); }
@media (max-width: 899.98px) {
  .ownernote { border-radius: var(--r); }
  .ownernote blockquote p { font-size: 1.02rem; }
}

/* One-of field group: phone or email, either will do */
.field__opt { color: var(--faint); font-weight: 500; text-transform: none; letter-spacing: 0; }
.form-error {
  font-size: 0.82rem;
  color: var(--mark);
  font-weight: 600;
  margin-top: 0.25rem;
}
.field input[aria-invalid="true"] {
  border-color: var(--mark);
  box-shadow: 0 0 0 3px rgba(176, 112, 58, 0.12);
}

/* Specular streak across the sheet — the cue that says "glass", not "panel" */
.site-head .nav::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(102deg,
    transparent 14%,
    rgba(255, 255, 255, 0.13) 31%,
    rgba(255, 255, 255, 0.03) 44%,
    transparent 60%);
  opacity: 1;
}
.site-head .nav { position: relative; }
.site-head .nav > * { position: relative; z-index: 1; }

/* The dark band belongs to the TOP OF THE PAGE, not to the header. Painting it
   on .site-head made it sticky too, so it slid down over the content on scroll.
   It hangs off the (static) top bar instead, and the panel floats free. */
.topbar { position: relative; }
/* The band reads as a survey drawing rather than as a grey filler bar.
   Contour lines are the trade's own visual language — this is a contractor
   whose work starts with grading and site survey — and at 15% sage on slate
   they register as texture rather than as a picture competing with the nav.
   Drawn as an inline SVG so there is no extra request and nothing to cache-bust;
   `data:` is permitted by the img-src directive in netlify.toml.
   preserveAspectRatio='none' lets the 1200×120 drawing stretch to whatever the
   band actually measures at each breakpoint instead of tiling a seam. */
.topbar::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 100%;
  height: 116px;                    /* header measures 111.19; overlap absorbs sub-pixel rounding */
  pointer-events: none;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1200' height='120' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cg fill='none' stroke='%237E9483' stroke-opacity='0.26' stroke-width='1.1' stroke-linecap='round'%3E%3Cpath d='M-20 104 C 180 78, 300 112, 470 96 S 760 62, 940 82 S 1140 104, 1220 90'/%3E%3Cpath d='M-20 88 C 180 62, 300 96, 470 80 S 760 46, 940 66 S 1140 88, 1220 74'/%3E%3Cpath d='M-20 72 C 180 46, 300 80, 470 64 S 760 30, 940 50 S 1140 72, 1220 58'/%3E%3Cpath d='M-20 56 C 180 30, 300 64, 470 48 S 760 14, 940 34 S 1140 56, 1220 42'/%3E%3Cpath d='M-20 40 C 180 14, 300 48, 470 32 S 760 -2, 940 18 S 1140 40, 1220 26'/%3E%3Cpath d='M-20 24 C 180 -2, 300 32, 470 16 S 760 -18, 940 2 S 1140 24, 1220 10'/%3E%3C/g%3E%3C/svg%3E")
      center / 100% 100% no-repeat,
    radial-gradient(38rem 13rem at 6%  140%, rgba(14, 122, 84, 0.55), transparent 66%),
    radial-gradient(34rem 12rem at 94% 145%, rgba(9, 94, 65, 0.48), transparent 68%),
    radial-gradient(28rem 10rem at 52% -50%, rgba(126, 148, 131, 0.12), transparent 70%),
    linear-gradient(168deg, #33454C 0%, #2B4340 52%, #22383A 100%);
}

/* Band height tracks the measured header height per breakpoint */
@media (max-width: 699.98px) { .topbar::after { height: 89px; } }

/* Slate badge on a dark sheet needs a rim to separate */
.site-head .brand__mark {
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(243, 245, 244, 0.22), 0 0 0 5px rgba(243, 245, 244, 0.05);
}

/* ==========================================================================
   37. Editorial pass
   The page had become a field of rounded rectangles — every idea sat in its
   own bordered, shadowed card. That plus star ratings is the directory-ad
   signature. Boxes are kept only where they earn it: the estimate form
   (containment), photographs (frames) and the ADU list (on a dark band).
   Everything else is separated by hairline rules and whitespace.
   ========================================================================== */

/* ---- Capability grid: columns under rules, not cards ---- */
#build .grid-3 { gap: clamp(1.75rem, 3vw, 2.75rem) clamp(2rem, 3.5vw, 3.25rem); }
#build .card {
  background: none;
  background-image: none;
  border: 0;
  border-top: 1px solid var(--line-2);
  border-radius: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  padding: 1.15rem 0 0;
  gap: 0.55rem;
  transform: none;
}
#build .card::before, #build .card::after { display: none; }
#build .card:hover { transform: none; box-shadow: none; border-top-color: var(--mark); }
#build .card__icon {
  width: auto; height: auto;
  border: 0;
  background: none;
  border-radius: 0;
  color: var(--mark);
  place-items: start;
  margin-bottom: 0.15rem;
}
#build .card__icon svg { width: 21px; height: 21px; }
#build .card h3 { font-size: 1.12rem; letter-spacing: -0.022em; }
#build .card p { color: var(--stone); font-size: 0.92rem; }

/* ---- Reviews: quotes, not rated cards ---- */
.quote {
  background: none;
  background-image: none;
  border: 0;
  border-top: 1px solid var(--line-2);
  border-radius: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  padding: 1.35rem 0 0;
  gap: 1rem;
}
.quote::after { display: none; }
/* WAS `display: none` with the note "star rows read as a review widget". The
   note is arguable and the rule never actually held: a `display: inline-block`
   buried inside the scroll-animation block un-hid them for everybody except
   readers with reduced motion switched on, which is the one group it should
   never have excluded. Made deliberate and unconditional instead — five stars
   beside a named review is the single most legible form the trust this
   business actually has can take, and it costs nothing to show it to
   everybody. See the note in the animation block. */
.quote__stars { display: inline-block; }
.quote blockquote {
  font-size: 1.16rem;
  line-height: 1.5;
  letter-spacing: -0.015em;
  color: var(--ink);
}
.quote blockquote::before { content: none; }
.quote figcaption strong { font-size: 0.92rem; }

/* ---- Process: numbered columns ---- */
.steps-strip {
  background: none;
  border: 0;
  box-shadow: none;
  border-radius: 0;
  gap: clamp(1.5rem, 2.5vw, 2.25rem) clamp(1.5rem, 2.5vw, 2.5rem);
}
.steps-strip li {
  background: none;
  background-image: none;
  border: 0;
  border-top: 1px solid var(--line-2);
  border-radius: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  padding: 1.1rem 0 0;
}
.steps-strip li::after { display: none; }
.steps-strip li:hover { background: none; }
.steps-strip__num { font-size: 0.82rem; -webkit-text-stroke: 0; color: var(--mark); letter-spacing: 0.14em; font-weight: 800; }

/* ---- Service areas: a list, not a keyword cloud ---- */
.areas { gap: 0.35rem 0; display: block; columns: 4; column-gap: 2.5rem; }
.areas li {
  border: 0;
  background: none;
  /* §26 gives chips a glass ring via box-shadow; without this each city
     kept a 1px outline and the list read as a table. */
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-radius: 0;
  padding: 0.28rem 0;
  font-size: 0.9rem;
  color: var(--stone);
  break-inside: avoid;
}
.areas li:hover { color: var(--mark); background: none; }
@media (max-width: 899.98px) { .areas { columns: 2; } }

/* ---- Owner's note: a signed statement, not a panel ---- */
.ownernote {
  background: none;
  border: 0;
  border-left: 2px solid var(--mark);
  border-radius: 0;
  box-shadow: none;
  padding: 0.35rem 0 0.35rem clamp(1.25rem, 2.5vw, 2rem);
}
.ownernote::before { display: none; }

/* ==========================================================================
   38. Footer density
   Measured 502px desktop / 987px mobile for 100 words. Tighter without
   dropping a single link.
   ========================================================================== */
/* Scoped above the mobile breakpoint: an unscoped rule here sat AFTER §33 and
   silently undid the tighter mobile gap. */
@media (min-width: 901px) {
  .foot-grid { padding-block: clamp(2.25rem, 4vw, 3rem) 1.25rem; gap: 2.5rem; }
}
@media (max-width: 899.98px) {
  .foot-grid { padding-block: 1.9rem 1.1rem; gap: 1.4rem 1.25rem; }
  .foot-about p { max-width: none; }
  .foot-col:last-child .btn { margin-top: 0.8rem; }
}
.foot-col h3 { margin-bottom: 0.75rem; }
.foot-col ul { gap: 0.42rem; font-size: 0.87rem; }
.foot-about p { margin-top: 0.7rem; font-size: 0.87rem; max-width: 34ch; }
.foot-badges { margin-top: 0.9rem; gap: 0.35rem; }
.badge { padding: 0.36em 0.62em; font-size: 0.63rem; }
.social { margin-top: 0.9rem; }
.social a { width: 34px; height: 34px; }
.foot-col .btn { margin-top: 0.9rem; }
.foot-bottom { padding-block: 1rem; font-size: 0.76rem; }

/* Social links are removed at runtime when no URL is configured; don't leave
   their margin behind. */
.social:empty { display: none; }
.foot-badges:empty { display: none; }

/* ==========================================================================
   39. Mobile re-assertion
   §37 is unscoped and lands after §32, so it silently undid the mobile
   compaction for the capability grid. Same trap as the footer.
   ========================================================================== */
@media (max-width: 899.98px) {
  #build .grid-3 { gap: 1.15rem 1.15rem; }
  #build .card { padding: 0.85rem 0 0; gap: 0.35rem; }
  #build .card__icon { margin-bottom: 0; }
  #build .card__icon svg { width: 18px; height: 18px; }
  #build .card h3 { font-size: 0.94rem; }
  #build .card p { font-size: 0.79rem; line-height: 1.45; }

  /* 25 cities as a column list is taller than the chips ever were.
     As running text it's shorter and reads less like keyword stuffing. */
  .areas { display: block; columns: auto; font-size: 0.88rem; line-height: 1.75; color: var(--stone); }
  .areas li { display: inline; padding: 0; }
  .areas li:not(:last-child)::after { content: ',\00a0'; }

  .quote { padding-top: 1.1rem; }
  .quote blockquote { font-size: 1.02rem; }
  .steps-strip li { padding-top: 0.9rem; }
}

/* ==========================================================================
   40. Logo token
   No ring, and no hole. The mark is already a circle — putting a second
   circle around it made a bullseye, and the aperture behind it only showed
   as a grey halo. Depth instead of outline: a hairline rim to lift it off
   the sheet, a contact shadow, and a soft cast below.
   ========================================================================== */
.site-head .brand { position: relative; }
.site-head .brand::before,
.site-head .brand::after { content: none; }

/* `.site-head .brand__mark` and its 559px size override lived here. The class
   is in none of the 25 HTML files — the masthead mark is `.brand__logo` — and
   the only thing the rules achieved was to consume the `--mark: 58px`/`46px`
   token whose real effect was breaking the steel colour inside the header.
   Deleted together; see the note on `.site-head .nav`. */

/* ==========================================================================
   41. Mobile scroll length
   Desktop untouched — everything below is inside a max-width query.
   Measured at 500px: 9,337px (~10.4 screens).
   ========================================================================== */
@media (max-width: 899.98px) {
  /* Display type doesn't need desktop proportions on a phone */
  h2 { font-size: clamp(1.72rem, 6.4vw, 2.2rem); }
  .section-head { margin-bottom: 1.15rem; gap: 0.85rem; }
  .section-head h2 { margin-top: 0.6rem; }
  .section { padding-block: clamp(2rem, 6vw, 2.6rem); }
  .section--tight { padding-block: 1.7rem; }
  .lede { font-size: 1rem; line-height: 1.5; }

  /* Hero: the fact strip repeats what the lede just said */
  .hero--photo { min-height: min(56vw, 74vh); }
  .hero-facts .hero__meta { gap: 0.7rem 1.5rem; padding-block: 0.85rem; }
  .hero-facts dt { font-size: 0.62rem; }
  .hero-facts dd { font-size: 0.95rem; }

  /* Capability grid — the tallest block on the page */
  #build .grid-3 { gap: 0.9rem 1rem; }
  #build .card { padding-top: 0.7rem; }
  #build .card p { font-size: 0.76rem; line-height: 1.4; }
  #build .card__icon svg { width: 16px; height: 16px; }

  .statement__inner { padding-block: 2rem; gap: 1.1rem; }
  .dark-band .lede { font-size: 1rem; }

  .steps-strip { gap: 1rem 1.1rem; }
  .steps-strip p { font-size: 0.78rem; line-height: 1.42; }

  /* Estimate: the intro column duplicates the form's own labels */
  #estimate .checks { gap: 0.6rem; }
  #estimate .checks strong { font-size: 1rem; }
  #estimate .checks p { font-size: 0.8rem; }
  #estimate .checks li::before { width: 22px; height: 22px; }
  .quotebox__body { padding: 1.1rem; }
  .quotebox__head { padding: 1rem 1.1rem; }
  .opt { padding: 0.8rem 1rem; font-size: 0.88rem; }
  .opts { gap: 0.45rem; }

  .acc__btn { padding: 0.9rem 0; font-size: 1.02rem; }
  .acc__panel p { padding-bottom: 1rem; font-size: 0.92rem; }
  .ownernote blockquote p { font-size: 0.96rem; line-height: 1.5; }
  .cta-band__inner { padding-block: 2rem; }
}

/* ==========================================================================
   42. Desktop hero framing
   The photo runs up behind the bar so there's no visible top edge. Left to
   start below the header it showed a hard horizontal seam — the image
   looked cut off at the top, which mobile never does because its header is
   compact and the photo begins immediately under it.
   Only sky sits in the strip behind the bar, so nothing is lost to it.
   ========================================================================== */
@media (min-width: 901px) {
  :root { --head-h: 111px; }          /* measured: .nav 84 + .site-head padding */

  /* Full image height, always. The 90vh cap was what forced the crop: once
     the box is shorter than width/1.778 the image has to lose height. Sized
     off width alone, the box always matches or exceeds the image's ratio, so
     cover scales by height and trims the sides instead. */
  .hero--photo {
    margin-top: calc(-1 * var(--head-h));
    padding-top: calc(var(--head-h) + clamp(3rem, 8vh, 5rem));
    min-height: calc(56.25vw + var(--head-h));
  }

  /* No vertical overflow left to pan, so centre it */
  .hero__slide { object-position: center center; }

  /* Lift the hero copy off the bottom edge — the taller full-height frame
     left it sitting low in the composition. */
  /* Anchor the copy to the TOP of the frame instead of the bottom. Padding
     alone couldn't lift it far — the block was pinned to the bottom edge and
     every increase just fought the hero's own height. */
  .hero--photo {
    align-items: start;
    padding-top: calc(var(--head-h) + clamp(3rem, 9vh, 6.5rem));
    padding-bottom: clamp(3rem, 8vh, 6rem);
  }

  /* Scrim under the bar so nav text never fights a bright sky */
  .hero__media::after {
    background:
      linear-gradient(180deg, rgba(12,18,23,0.52) 0%, rgba(12,18,23,0.10) 17%, transparent 32%),
      linear-gradient(100deg, rgba(11,20,14,0.88) 0%, rgba(11,20,14,0.64) 40%, rgba(11,20,14,0.14) 68%, transparent 84%),
      linear-gradient(to top, rgba(11,20,14,0.72) 0%, transparent 42%);
  }
}

/* ==========================================================================
   §44  ADDITIONS — hero picture, slideshow pause, form assurance, print
   ▸ Anything unscoped in here would override the mobile blocks above, which
     has bitten this stylesheet before. Every rule that differs by width is
     wrapped in its own query; nothing relies on being last in the file.
   ========================================================================== */

/* <picture> would otherwise create a box between .hero__media and the
   absolutely positioned slide inside it. display:contents removes the box
   and leaves the img positioning exactly as it was. */
.hero__pic { display: contents; }

/* .hero__pause deleted with the carousel it paused. */

/* --- Honeypot -----------------------------------------------------------
   Not display:none. Some bots skip hidden fields; this one is off-canvas
   but still "visible" to anything parsing the DOM naively. */
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- Reassurance under the form ----------------------------------------- */
.form-note {
  margin: 0.9rem 0 0;
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--faint);
}
.form-note a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.form-note a:hover { color: var(--mark); }

/* --- Photography disclosure --------------------------------------------- */
.stock-note {
  max-width: 62ch;
  margin: 0 0 2rem;
  padding: 0.9rem 1.1rem;
  border-left: 3px solid var(--sage);
  background: rgba(126, 148, 131, 0.10);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--stone);
}
.stock-note strong { color: var(--ink); }

/* --- Tap targets --------------------------------------------------------
   WCAG 2.2 and Apple both want 44px. Applied through padding so the visual
   rhythm of the text is untouched — only the hit area grows. */
@media (pointer: coarse) {
  .foot-col a,
  .foot-legal a,
  .drawer a,
  .filter,
  .acc__btn {
    min-height: 44px;
    display: flex;
    align-items: center;
  }
  .foot-col a, .foot-legal a { padding-block: 0.35rem; }
  .hero__dot { padding: 12px 4px; background-clip: content-box; }
  .hero__pause { width: 34px; height: 34px; }
}

/* --- Focus, consistently ------------------------------------------------
   :focus-visible only, so the ring appears for keyboard and assistive use
   and not on every mouse click. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--mark);
  outline-offset: 2px;
  border-radius: 2px;
}
.bg-ink a:focus-visible,
.bg-ink2 a:focus-visible,
.bg-ink button:focus-visible,
.bg-ink2 button:focus-visible,
.site-head a:focus-visible,
.site-head button:focus-visible,
.cta-band a:focus-visible,
.site-foot a:focus-visible {
  outline-color: var(--sage);
}

/* --- Glass fallback -----------------------------------------------------
   Where backdrop-filter is unsupported the panel is otherwise a thin wash
   over whatever is behind it, and the text contrast is anyone's guess.
   Opaque backgrounds keep it legible. */
@supports not (backdrop-filter: blur(2px)) {
  .site-head,
  .site-head__slab,
  .glass,
  .quotebox,
  .topbar { background-color: rgba(24, 32, 38, 0.94); }
  .card.glass,
  .quotebox { background-color: rgba(247, 247, 244, 0.98); }
}

/* --- Print --------------------------------------------------------------
   Homeowners print the process page and the estimate to sit beside
   competing bids. Give them something worth putting on the pile. */
@media print {
  *, *::before, *::after {
    background: transparent !important;
    color: #000 !important;
    box-shadow: none !important;
    text-shadow: none !important;
  }
  body { font-size: 11pt; line-height: 1.45; }
  .site-head, .topbar, .drawer, .nav__toggle, .mobile-bar,
  .hero__media, .hero__dots, .cta-band, .filters, .skip,
  .quotebox, .form-note, [data-reveal] .btn { display: none !important; }
  main { padding-top: 0 !important; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
  a[href^="tel:"]::after, a[href^="mailto:"]::after { content: ""; }
  h1, h2, h3 { break-after: avoid; page-break-after: avoid; }
  .section, section { break-inside: auto; padding: 0.6rem 0 !important; }
  .acc__panel { display: block !important; height: auto !important; }
  .site-foot { border-top: 1px solid #999; padding-top: 0.5rem; }
}

/* --- Reduced motion, catching the additions above ------------------------ */
@media (prefers-reduced-motion: reduce) {
  .hero__pause { transition: none; }
}

/* ==========================================================================
   §45  SERVICE-AREA PAGES
   ========================================================================== */
.section-head__note {
  margin: 0;
  color: var(--stone);
  font-size: 1.0625rem;
  line-height: 1.62;
  max-width: 46ch;
}
@media (min-width: 900px) {
  .section-head--split .section-head__note { padding-bottom: 0.35rem; }
}

/* Local-conditions blocks. Two up on desktop, stacked on a phone — these are
   read, not scanned, so they get a comfortable measure rather than a tight grid. */
.area-facts {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.25rem) clamp(2rem, 4vw, 3.5rem);
}
@media (min-width: 860px) {
  .area-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.area-fact { border-top: 1px solid var(--line); padding-top: 1.05rem; }
.area-fact h3 {
  margin: 0 0 0.5rem;
  font-size: 1.0625rem;
  letter-spacing: -0.01em;
}
.area-fact p {
  margin: 0;
  color: var(--stone);
  font-size: 0.9375rem;
  line-height: 1.68;
  max-width: 54ch;
}

.areas--wide li { font-size: 0.86rem; }
.area-more { margin: 1.6rem 0 0; }

.area-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem 1.35rem;
  margin: 0.7rem 0 0;
  font-size: 0.9375rem;
}
.area-links a { color: var(--stone); text-decoration: none; }
.area-links a:hover { color: var(--mark); text-decoration: underline; text-underline-offset: 3px; }

.pagehero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  margin-top: clamp(1.4rem, 3vw, 2rem);
}

.link-arrow {
  color: var(--mark);
  font-weight: 700;
  font-size: 0.82rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  text-decoration: none;
}
.link-arrow:hover { text-decoration: underline; text-underline-offset: 4px; }

/* The dark band on these pages carries body copy, which the base .split
   does not colour for a dark ground. */
.dark-band .split p { color: rgba(233, 238, 235, 0.82); }
.dark-band .split .muted { color: rgba(233, 238, 235, 0.60); font-size: 0.875rem; }

/* ==========================================================================
   §46  UTILITIES
   These replace inline style attributes. Removing those is what lets the
   Content-Security-Policy set style-src 'self' instead of 'unsafe-inline'.
   ========================================================================== */
.u-fs-2     { font-size: var(--step-2); }
.u-fs-3     { font-size: var(--step-3); }
.u-fs-86    { font-size: 0.86rem; }
.u-fs-92    { font-size: 0.92rem; }
.u-fs-4-mt  { font-size: var(--step-4); margin-top: 1rem; }
.u-o-75     { opacity: 0.75; }
.u-note     { font-size: 0.85rem; margin-top: 0.5rem; }
.u-mt-6     { margin-top: 0.6rem; }
.u-mt-8     { margin-top: 0.8rem; }
.u-flush    { border-top: 0; padding-top: 0; }
.u-bt-0     { border-top: 0; }
.u-bold     { font-weight: 700; }
.u-mw-13    { max-width: 13ch; }

/* ==========================================================================
   §47  SITE ASSISTANT
   The widget for assets/js/assistant.js. Reuses the existing tokens rather
   than introducing a second visual language — same forest, same rules, same
   radius, same shadow scale as the rest of the site.
   ========================================================================== */
/* ABOVE THE HEADER. This was 90 and .site-head is 100, so scrolling with the
   panel open slid the masthead over the top of it — the one element on the page
   that is explicitly pinned above the content was outranked by the one element
   that is pinned above everything else.

   150 clears the header (100), the drawer (99) and the consent bar (95), and
   stays under the grain overlay (500) and the skip link (999), which both have
   to stay on top of everything.

   AND IT HIDES WITH THE MOBILE MENU. Raising it over the header also raises it
   over the drawer, which would newly float the assistant on top of an open
   navigation — a regression this fix would otherwise introduce rather than a
   pre-existing one. The drawer already owns the screen when it is open. */
.ai { position: fixed; right: 1.25rem; bottom: 1.25rem; z-index: 150; }
body.nav-open .ai { display: none; }

.ai__fab {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.85rem 1.15rem;
  background: var(--volt); color: var(--bone);
  border: 0; border-radius: 999px;
  font-family: var(--display); font-weight: 800;
  font-size: 0.82rem; letter-spacing: 0.04em;
  box-shadow: var(--sh-lg);
  cursor: pointer;
  transition: background 0.25s var(--ease), transform 0.25s var(--ease);
}
.ai__fab:hover { background: var(--clay-hi); transform: translateY(-1px); color: var(--bone); }
.ai__fab svg { width: 20px; height: 20px; flex: none; }
.ai.is-open .ai__fab { display: none; }

.ai__panel {
  display: flex; flex-direction: column;
  width: min(24rem, calc(100vw - 2.5rem));
  /* SIZES TO ITS CONTENT, capped. A fixed height was leaving a block of empty
     panel under the openers once the copy was cut back — the panel was
     asserting a size the conversation had not earned yet. `height: auto` with
     a max lets the opening state be small and the panel grow into the cap as
     the transcript fills, at which point the log takes the overflow.
     THE CAP CLEARS THE HEADER. 6rem was a guess and it was too small on a
     short window: the header is sticky, the panel is bottom-anchored, and a
     long conversation grew it until its own title and close button sat behind
     the site header. `--head-h` is the measured height of that bar, so this
     subtracts what is actually up there. Same reasoning as the mobile rule. */
  height: auto;
  max-height: min(38rem, calc(100svh - var(--head-h, 4.5rem) - 3rem));
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--sh-lg);
  overflow: hidden;
}
.ai__panel[hidden] { display: none; }

.ai__head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem;
  padding: 1rem 1.1rem;
  background: var(--slate-deep); color: var(--bone);
}
.ai__title { font-family: var(--display); font-weight: 800; font-size: 0.95rem; }
.ai__sub { font-size: 0.76rem; color: var(--bone-mute); margin-top: 0.15rem; }
.ai__close {
  background: transparent; border: 0; color: var(--bone-mute);
  cursor: pointer; padding: 0.25rem; flex: none; border-radius: 3px;
}
.ai__close:hover { color: var(--bone); }
.ai__close svg { width: 18px; height: 18px; display: block; }

/* `min-height: 0` is load-bearing, not tidying. A flex child defaults to
   min-height:auto, so the log refuses to shrink below its own content and
   pushes the FORM out of the bottom of the panel — the input ends up outside
   the box it is supposed to be inside. It shows up whenever the panel is
   shorter than the transcript: a long conversation, or a phone with the
   keyboard open. The log is the part that should scroll; the form is not. */
.ai__log { flex: 1; min-height: 0; overflow-y: auto; padding: 1rem; display: grid; gap: 0.85rem; align-content: start; }

.ai__msg { font-size: 0.88rem; line-height: 1.55; border-radius: var(--r); padding: 0.7rem 0.85rem; max-width: 92%; }
.ai__msg p + p { margin-top: 0.6rem; }
.ai__msg--you { background: var(--volt); color: var(--bone); justify-self: end; }
.ai__msg--bot { background: rgba(27,34,41,0.045); color: var(--ink); justify-self: start; }
.ai__msg--wait { color: var(--stone); letter-spacing: 0.2em; }

.ai__src { display: flex; flex-wrap: wrap; gap: 0.4rem 0.8rem; margin-top: 0.7rem; }
.ai__src a {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--mark);
  text-decoration: underline; text-underline-offset: 3px;
}

.ai__chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.8rem; }
.ai__chip {
  font: inherit; font-size: 0.76rem; line-height: 1.3;
  padding: 0.4rem 0.65rem; text-align: left;
  background: #fff; color: var(--mark);
  border: 1px solid var(--line-warm); border-radius: 999px; cursor: pointer;
  transition: background 0.2s;
}
.ai__chip:hover { background: rgba(9,94,65,0.06); }

.ai__form { display: flex; gap: 0.5rem; padding: 0.8rem; border-top: 1px solid var(--line); }
.ai__input {
  flex: 1; font: inherit; font-size: 0.88rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--line-2); border-radius: var(--r);
  background: #fff; color: var(--ink);
}
.ai__input:focus { outline: 2px solid var(--mark); outline-offset: 1px; border-color: transparent; }
.ai__send {
  flex: none; width: 42px;
  background: var(--volt); color: var(--bone);
  border: 0; border-radius: var(--r); cursor: pointer;
  display: grid; place-items: center;
}
.ai__send:hover { background: var(--clay-hi); color: var(--bone); }
.ai__send:disabled { opacity: 0.5; cursor: default; }
.ai__send svg { width: 18px; height: 18px; }

@media (max-width: 520px) {
  .ai { right: 0.75rem; bottom: 0.75rem; left: 0.75rem; }
  .ai__fab { width: 100%; justify-content: center; }
  /* SIZES TO CONTENT, like the desktop panel. This was a fixed 28rem, and at
     375x667 that is a 448px box holding 448px of content — the log overflowed
     by 130px and the opening line, the one that says we build all of it, was
     scrolled off before anyone read it. A first impression behind a scrollbar
     is not a first impression.
     `svh` rather than `vh` because on a phone `vh` measures the viewport
     WITHOUT the browser chrome, which is the one number that is wrong exactly
     when the address bar is showing. */
  .ai__panel {
    width: 100%;
    height: auto;
    /* CLEAR OF BOTH BARS. 4.5rem of clearance was not enough: on a subpage the
       site header is a sticky ~62px bar and the bottom carries the call bar,
       so a long conversation grew the panel until its OWN head — the title,
       CLEAR and the close button — sat behind the site header and could not be
       reached. A panel you cannot close is worse than a short one.
       Both bars are already measured into custom properties, so this subtracts
       what is actually there rather than a guess, plus a gap so the panel reads
       as floating rather than wedged. */
    max-height: calc(100svh - var(--head-h, 4rem) - var(--callbar-h, 3.5rem) - 2.5rem);
  }
}

/* The sticky mobile call bar already owns the bottom edge on small screens. */
@media (max-width: 860px) {
  .ai { bottom: calc(0.75rem + var(--callbar-h, 3.5rem)); }
}

/* Keyboard-aware panel. `--ai-kb` is published by assistant.js from
   visualViewport and is the height the on-screen keyboard is covering; it is
   0 on desktop and whenever no keyboard is up. 100vh cannot see a keyboard, so
   without this the input — the bottom element of a bottom-anchored panel —
   sits behind it and the visitor cannot see what they are typing. */
/* The lift itself is a margin set from JS — see assistant.js. */
/* A FLOOR on the height, because subtracting the keyboard without one lets
   the panel shrink to less than the form is tall — at which point the input is
   pushed outside the panel and ends up under the keyboard anyway, which is the
   exact failure this whole mechanism exists to prevent. Below the floor the
   panel's TOP goes off-screen instead, which costs scrollback the visitor is
   not reading while typing and keeps the box they ARE typing in visible. */
/* THE CLEARANCE HAS TO BE IN HERE TOO, and it was not. This rule is
   `.ai.is-open .ai__panel` — two classes, later in the file — so it beat both
   the base cap and the mobile one and the panel grew to `100vh - 5rem`
   regardless. On a subpage that put the panel's own head, with its close
   button, behind the sticky site header on a long conversation: measured at
   390x760, panel top 12px against a header bottom of 62px.
   The keyboard subtraction and the chrome clearance are not alternatives —
   both have to hold at once — so they are one expression. `--head-h` and
   `--callbar-h` are the measured heights of the two bars; `--ai-kb` is what
   the on-screen keyboard is covering. The 13rem floor is unchanged and still
   wins over all of it, for the reason above. */
.ai.is-open .ai__panel {
  max-height: max(
    13rem,
    calc(100svh - var(--head-h, 4rem) - var(--callbar-h, 3.5rem) - 2.5rem - var(--ai-kb, 0px))
  );
}
@media (prefers-reduced-motion: no-preference) {
  .ai.is-open { transition: margin-bottom 0.18s var(--ease); }
}

@media (prefers-reduced-motion: reduce) {
  .ai__fab { transition: none; }
  .ai.is-open { transition: none; }
  .ai__fab:hover { transform: none; }
}

/* ==========================================================================
   §48  REFERENCE PAGES — standards, safety, warranty, commercial

   These four pages exist to be READ CLOSELY and, in the case of the standards
   page, argued with. A homeowner comparing three bids is looking for a number
   they can hold one contractor to; a property manager is looking for evidence
   the company has thought about this before. That is a different reading mode
   from the rest of the site, which is scanned, and it needs different
   furniture: tables that survive a phone, a contents rail that does not lose
   your place in a 3,000-word page, and a way to cite the standard a number
   comes from without turning every paragraph into a footnote.

   No new colours. Everything below is built from the existing tokens.
   ========================================================================== */

/* ---- 48.1  Spec tables -------------------------------------------------
   A spec table is the whole point of the standards page, and the default
   <table> is unusable on a 375px screen: it either overflows the viewport
   and takes the page's horizontal scroll with it, or it wraps every cell to
   one word per line. The fix is a wrapper that owns the overflow, so the
   TABLE scrolls inside its box and the PAGE never does.

   `contain: paint` stops the sticky first column from painting outside the
   wrapper when it scrolls. Without it the frozen column smears over the
   section padding on Safari. */
.spec-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  contain: paint;
  border: 1px solid var(--line);
  border-radius: var(--r);
  background: var(--white);
  /* A hairline shadow on the right edge that only appears when there is more
     table to the right. Pure CSS scroll-affordance: two background-attachment
     layers, one pinned to the scroll container and one to the content, so the
     gradient fades out exactly when you reach the end. */
  background-image:
    linear-gradient(to right, var(--white) 30%, rgba(255,255,255,0)),
    linear-gradient(to right, rgba(255,255,255,0), var(--white) 70%),
    linear-gradient(to right, rgba(27,34,41,0.10), rgba(27,34,41,0)),
    linear-gradient(to left,  rgba(27,34,41,0.10), rgba(27,34,41,0));
  background-position: left center, right center, left center, right center;
  background-repeat: no-repeat;
  background-size: 40px 100%, 40px 100%, 14px 100%, 14px 100%;
  background-attachment: local, local, scroll, scroll;
}
.spec {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1);
  line-height: 1.5;
  /* Numerals in a spec table are compared down the column, so they must line
     up. `tabular-nums` is the entire reason this rule exists. */
  font-variant-numeric: tabular-nums;
  min-width: 34rem;
}
.spec caption {
  caption-side: top;
  text-align: left;
  padding: 1rem 1.15rem 0.35rem;
  font-family: var(--display);
  font-weight: 800;
  letter-spacing: -0.015em;
  font-size: var(--step-0);
  color: var(--ink);
}
.spec th,
.spec td {
  text-align: left;
  padding: 0.72rem 1.15rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.spec thead th {
  font-family: var(--display);
  font-weight: 700;
  font-size: 0.7rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--stone);
  background: var(--paper-2);
  border-bottom-color: var(--line-2);
  white-space: nowrap;
}
/* The row label is the thing you scan for, so it is frozen. 11rem is wide
   enough for "Segmental paving" without wrapping and narrow enough to leave
   the value column readable on a phone. */
.spec tbody th {
  font-weight: 600;
  color: var(--ink);
  position: sticky;
  left: 0;
  background: var(--white);
  min-width: 11rem;
}
.spec tbody tr:last-child th,
.spec tbody tr:last-child td { border-bottom: 0; }
.spec tbody tr:hover th,
.spec tbody tr:hover td { background: var(--paper); }
.spec td strong { font-weight: 650; color: var(--ink); }

/* ---- 48.2  Standard reference chip -------------------------------------
   `.badge` already exists but is coloured for the inverted bands — bone text
   on a dark surface. On paper it is invisible. This is the light-background
   sibling, used inline after a spec value to name the governing document. */
.stdref {
  display: inline-block;
  border: 1px solid var(--line-warm);
  border-radius: 2px;
  padding: 0.2em 0.5em;
  margin-left: 0.35em;
  font-size: 0.66rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-weight: 650;
  color: var(--mark);
  white-space: nowrap;
  vertical-align: 0.08em;
}

/* ---- 48.3  Contents rail ------------------------------------------------
   Pairs with `.split--sticky`, which already pins the first column. This only
   styles what sits inside it. Below 900px the rail un-sticks (that is
   `.split`'s own breakpoint) and reads as a plain jump-list at the top of the
   page, which is the right behaviour on a phone. */
.toc { position: relative; }
.toc ol {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: 0.1rem;
  counter-reset: toc;
}
.toc li { counter-increment: toc; }
.toc a {
  display: grid;
  grid-template-columns: 2.1rem 1fr;
  gap: 0.5rem;
  align-items: baseline;
  padding: 0.5rem 0.6rem 0.5rem 0;
  border-bottom: 1px solid var(--line);
  color: var(--stone);
  text-decoration: none;
  font-size: var(--step--1);
  line-height: 1.35;
  transition: color 0.18s var(--ease), padding-left 0.18s var(--ease);
}
.toc a::before {
  content: counter(toc, decimal-leading-zero);
  font-family: var(--display);
  font-weight: 800;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: var(--faint);
}
.toc a:hover,
.toc a:focus-visible { color: var(--mark); padding-left: 0.3rem; }
.toc a:hover::before,
.toc a:focus-visible::before { color: var(--mark); }

/* ---- 48.4  Callout ------------------------------------------------------
   One box, three tones. Used for the thing on a page that must not be missed:
   a code minimum, a limit of scope, a claim deadline. The left rule carries
   the tone so the box itself stays quiet. */
.callout {
  border: 1px solid var(--line);
  border-left: 3px solid var(--mark);
  border-radius: var(--r-sm);
  background: var(--white);
  padding: clamp(1.05rem, 2.2vw, 1.5rem) clamp(1.15rem, 2.4vw, 1.7rem);
  display: grid;
  gap: 0.5rem;
}
.callout > h3 {
  font-size: var(--step-0);
  letter-spacing: -0.01em;
  margin: 0;
}
.callout > p { margin: 0; color: var(--stone); font-size: var(--step--1); line-height: 1.6; }
.callout--warn  { border-left-color: #A8541F; }
.callout--quiet { border-left-color: var(--line-2); background: var(--paper-2); }

/* ---- 48.5  Definition rows ---------------------------------------------
   Term on the left, meaning on the right, hairline between. Used for the
   warranty exclusions and the glossary blocks where a table would be
   over-engineered — there are only two columns and one is always short. */
.deflist { display: grid; gap: 0; margin: 0; }
.deflist > div {
  display: grid;
  gap: 0.25rem 1.75rem;
  padding: 1rem 0;
  border-bottom: 1px solid var(--line);
}
.deflist > div:last-child { border-bottom: 0; }
@media (min-width: 760px) {
  .deflist > div { grid-template-columns: minmax(0, 0.62fr) minmax(0, 1fr); align-items: start; }
}
.deflist dt {
  font-family: var(--display);
  font-weight: 750;
  letter-spacing: -0.012em;
  color: var(--ink);
  font-size: var(--step-0);
}
.deflist dd { margin: 0; color: var(--stone); font-size: var(--step--1); line-height: 1.62; }

/* ---- 48.6  Anchored section headings -----------------------------------
   Every standards section is linkable, because the practical use of this page
   is a contractor or an inspector sending someone a link to ONE clause.
   scroll-margin-top keeps the heading clear of the fixed header when you
   arrive from a #hash — html's scroll-padding already does this for smooth
   in-page scrolls, but not for a cold load on a deep link. */
.anchored { scroll-margin-top: calc(var(--nav-h) + 28px); }
.anchored > h2,
.anchored > h3 { position: relative; }
.anchored .anchor-link {
  position: absolute;
  left: -1.1em;
  top: 0.06em;
  opacity: 0;
  color: var(--mark);
  text-decoration: none;
  font-weight: 400;
  transition: opacity 0.15s var(--ease);
}
.anchored:hover .anchor-link,
.anchor-link:focus-visible { opacity: 0.55; }
@media (max-width: 900px) { .anchored .anchor-link { display: none; } }

/* ---- 48.7  Reference page rhythm ---------------------------------------
   Long-form reading wants a tighter vertical rhythm than the marketing
   sections, which are spaced to be scrolled past. */
.refbody { display: grid; gap: clamp(2.6rem, 5vw, 4rem); }

/* THE SCROLLING TABLE ABOVE DID NOT SCROLL, AND THIS IS WHY.
   `.spec-wrap` owns the overflow correctly and `.spec` carries `min-width:
   34rem` so the columns stay readable — but a GRID ITEM's automatic minimum
   size is its min-content size, so every grid ancestor of that table refused
   to shrink below 544px and the PAGE took the scroll instead. Measured at
   320px: html 320 · .split 320 · .refbody 546 · section 546 · .spec-wrap 546.
   The wrapper was never given the chance to scroll, because nothing above it
   was ever narrower than the thing it was supposed to be clipping.

   `.split` already spells its desktop columns `minmax(0, 1fr)` for exactly
   this reason, so the trap was known — it just was not spelled at the single
   implicit track below 900px, where the track is `auto` and takes the item's
   min-content as its floor. Both levels are needed: `.refbody` is an item of
   `.split`, and each `section` is an item of `.refbody`. Fixing only the outer
   one moves the 546px overflow inwards instead of removing it.

   Cost of not having it: warranty, standards, safety and commercial all
   scrolled sideways on every phone — 12 of the 13 narrow-viewport failures. */
.refbody,
.refbody > section { min-width: 0; }
.refbody p { max-width: 62ch; }
.refbody > section > h2 { margin-bottom: 0.9rem; }
.refbody ul:not([class]) { padding-left: 1.15rem; display: grid; gap: 0.5rem; }
.refbody ul:not([class]) li { color: var(--stone); }
.refbody ul:not([class]) li::marker { color: var(--mark); }

/* Print: a homeowner comparing bids prints this page and writes on it. Drop
   the furniture, keep the substance, and let tables break across pages. */
@media print {
  .toc, .cta-band, .callbar, .ai, .topbar, .drawer { display: none !important; }
  .spec-wrap { overflow: visible; border-color: #999; background-image: none; }
  .spec { min-width: 0; font-size: 9.5pt; }
  .spec tbody th { position: static; }
  .refbody { gap: 1.5rem; }
}

/* ==========================================================================
   §49  HEADLESS MOBILE HEADER

   On a phone the header was a 68px dark glass panel sitting on an 89px dark
   contour band — 157px of chrome, over a third of a small viewport, spent
   restating a logo the visitor has already seen and a nav that is behind a
   hamburger anyway. Below 720px that whole apparatus is removed and replaced
   with three free-floating controls: the badge top-left, call and menu
   top-right.

   Three consequences that all have to be handled together, because fixing one
   and not the others is what makes this pattern go wrong:

   1. FIXED, NOT STICKY. Sticky occupies layout space; the point here is that
      it does not. That reclaims the space but drops the ~89px the document
      used to reserve, so .pagehero has to grow its top padding to keep the
      breadcrumbs out from under the buttons. The home hero is the deliberate
      exception — controls floating over the photograph is the intent.

   2. THE BAR MUST NOT EAT TAPS. .site-head still spans the full width, so it
      is pointer-events:none and only the three controls take pointer-events
      back. Without this an invisible full-width strip swallows every tap
      across the top of the page.

   3. CONTRAST ON BOTH SURFACES. These sit over a dark photograph on the home
      page and over pale paper everywhere else. Rather than script a state
      change, each control is a dark frosted disc with a light rim — legible
      on the photo because of the rim, legible on paper because it is dark.
      One treatment, no JS, no flash on load.
   ========================================================================== */

@media (max-width: 719.98px) {

  /* The dark contour band belongs to the header that no longer exists. */
  .topbar::after { display: none; }

  .site-head {
    position: fixed;
    left: 0; right: 0; top: 0;
    padding: 0;
    /* See note 2 — the strip itself must be transparent to taps. */
    pointer-events: none;
  }
  .site-head.is-stuck { padding-block: 0; }

  /* The panel IS the header. Both the glass sheet (::after) and the specular
     streak (::before) go; what is left is a transparent flex row that only
     positions the three controls. */
  .site-head .nav::after,
  .site-head .nav::before { display: none; }

  .site-head .nav {
    min-height: 0;
    border-radius: 0;
    align-items: flex-start;
    gap: 0.5rem;
    /* max() rather than a bare value so a notched phone's safe area wins when
       it is larger, and the 0.7rem baseline applies when there is none. */
    padding-top: calc(env(safe-area-inset-top, 0px) + 0.7rem);
    padding-bottom: 0;
    padding-left: max(0.7rem, env(safe-area-inset-left, 0px));
    padding-right: max(0.7rem, env(safe-area-inset-right, 0px));
  }

  .site-head .brand,
  .site-head .nav__cta { pointer-events: auto; }

  /* ---- Badge, top-left. The mark alone: it already contains the wordmark. */
  .site-head .brand__text { display: none; }
  .site-head .brand { gap: 0; }
  .site-head .brand__mark {
    width: 50px; height: 50px;
    border-radius: 50%;
    /* Light rim to lift it off a dark photograph, plus a real drop shadow so
       it reads as hovering rather than as pasted on. */
    box-shadow:
      0 0 0 1px rgba(243, 245, 244, 0.30),
      0 0 0 5px rgba(243, 245, 244, 0.07),
      0 10px 24px -12px rgba(0, 0, 0, 0.65);
  }

  /* ---- Call + menu, top-right. */
  .site-head .nav__cta { gap: 0.55rem; }

  .site-head .nav__phone,
  .site-head .nav__toggle {
    width: 50px; height: 50px;
    min-width: 50px; min-height: 50px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    gap: 0;
    background: linear-gradient(180deg, rgba(20, 27, 33, 0.92), rgba(12, 18, 23, 0.95));
    border: 1px solid rgba(243, 245, 244, 0.26);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.16),
      0 10px 24px -12px rgba(0, 0, 0, 0.65);
    color: var(--bone);
  }
  @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .site-head .nav__phone,
    .site-head .nav__toggle {
      background: linear-gradient(180deg, rgba(20, 27, 33, 0.80), rgba(12, 18, 23, 0.86));
      -webkit-backdrop-filter: blur(20px) saturate(150%);
      backdrop-filter: blur(20px) saturate(150%);
    }
  }
  /* Site convention: transparency is an enhancement, never a dependency. */
  @media (prefers-reduced-transparency: reduce) {
    .site-head .nav__phone,
    .site-head .nav__toggle {
      background: #141b21;
      -webkit-backdrop-filter: none;
      backdrop-filter: none;
    }
  }

  /* The number is already on the call bar and in the footer; up here the icon
     carries it, and the accessible name comes from the link's own href. */
  .site-head .nav__phone span { display: none; }
  .site-head .nav__phone svg { width: 20px; height: 20px; }

  /* ---- Drawer runs full height so the floating controls sit ON it rather
     than beside a seam. z-index 100 (.site-head) over 99 (.drawer) already
     keeps them clickable; the padding stops the first link sliding under.

     OPAQUE, deliberately. The drawer ships at 0.9 alpha and leans on
     backdrop-filter to make whatever is behind it illegible. That is a fine
     bet for a panel hanging below the header — but this one now covers the
     whole viewport, and on the home page what sits behind it is 4rem white
     display type. Where backdrop-filter does not paint (older engines, GPU
     fallback, a compositor that skips it) the two texts overlap and the menu
     becomes unreadable. A full-screen navigation has nothing worth showing
     through it, so legibility wins and the frost is dropped here. */
  .drawer {
    inset: 0;
    /* 5.2rem was the old 60px bar plus clearance. The bar is `--nav-h` now and
       the mark in it is 78–96px, so the first link has to clear the token
       rather than a number that was measured against a header that is gone. */
    padding-top: calc(env(safe-area-inset-top, 0px) + var(--nav-h) + 1.2rem);
    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 2.5rem);
    background: var(--head);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    overscroll-behavior: contain;   /* stop the page behind scrolling with it */
  }

  /* ---- See note 1: replace the layout space the header no longer reserves. */
  .pagehero { padding-top: calc(env(safe-area-inset-top, 0px) + 7.25rem); }

  /* ---- Assistant: an icon, to match. It shipped stretched edge-to-edge
     below 520px, which put a full-width green slab across the bottom of every
     phone page — louder than the call bar it sits above, and permanent. As a
     disc it reads as the same family as the header controls, and the screen
     stops having two competing full-width bars stacked on each other. */
  .ai { left: auto; right: 0.9rem; }
  .ai__fab {
    width: 54px; height: 54px;
    padding: 0;
    gap: 0;
    border-radius: 50%;
    justify-content: center;
  }
  .ai__fab svg { width: 23px; height: 23px; }
  /* Clipped, NOT display:none. The label is the button's only accessible
     name — remove it from the tree and the control announces as "button". */
  .ai__fab-label {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }
  /* .ai shrink-wraps to the disc now that it is no longer stretched, so the
     panel has to state its own width or it inherits the 54px. */
  .ai__panel { width: min(24rem, calc(100vw - 1.8rem)); }
}

/* The 380px rule below §6 drops the brand tagline to stop the header
   overflowing. Headless has no tagline and no row to overflow, so the mark
   keeps its size and only the gutter tightens. */
@media (max-width: 379.98px) {
  .site-head .nav { padding-left: 0.55rem; padding-right: 0.55rem; }
  .site-head .nav__cta { gap: 0.45rem; }
  .site-head .brand__mark,
  .site-head .nav__phone,
  .site-head .nav__toggle { width: 46px; height: 46px; min-width: 46px; min-height: 46px; }
}

/* ==========================================================================
   §50  MOBILE HERO — height and breathing room

   Two rules were fighting and the wrong one was winning. §42 set
   `.hero--photo { min-height: min(80vh, 640px) }` at <=640px; the later
   mobile-scroll-length pass set `min(56vw, 74vh)` at <=900px. Same
   specificity, later wins — so on a 390px phone the effective floor was
   56vw = 218px. The hero only looked full-height because its own content
   propped it up, which is why it read as packed: every element was doing
   structural work and nothing had slack.

   Set a real floor and give the stack room. `svh` is the small-viewport unit —
   it resolves against the viewport WITH the browser chrome showing, so the
   hero does not grow and re-flow when the URL bar retracts on scroll. The
   plain `vh` line above it is the fallback and must stay first.
   ========================================================================== */
@media (max-width: 719.98px) {
  .hero--photo {
    min-height: 88vh;
    min-height: 88svh;
    /* Copy is bottom-aligned, so the top figure is what actually lets the
       photograph breathe above the headline. */
    padding-block: clamp(7.5rem, 24vh, 11rem) clamp(3rem, 8vh, 4.5rem);
  }

  /* The stack: eyebrow -> h1 -> lede -> buttons. Each gap was tuned for a
     desktop hero that had horizontal room to spare. */
  .hero h1 { margin-block: 1.4rem 1.55rem; }
  .hero--photo .hero__lede { font-size: 1.03rem; line-height: 1.62; }
  .hero__actions { margin-top: 2.35rem; gap: 0.7rem; }

  /* Two buttons at their natural widths leave a ragged right edge that reads
     as clutter more than the buttons themselves do. Matching them squares the
     block off and makes both a full-width tap target. */
  .hero__actions .btn { width: 100%; justify-content: center; }

  /* The dots/pause control sits under the copy; it was touching the buttons. */
  .hero__dots { margin-top: 1.5rem; }
}

/* ==========================================================================
   §51  MOBILE FOOTER — 1,284px is not a footer, it is a second page

   Measured at 390x800: 1,284px, or 1.6 full screens, below the CTA band.
   Three separate causes, and only fixing all three moves the number.

   (a) THE §38 LEAK. §33 compacts the footer below 900px. §38 then re-states
       `.foot-col h3` and `.foot-col ul` UNSCOPED, lands later in the
       cascade, and silently restores the loose spacing on mobile — the exact
       trap §38's own comment warns about two rules above where it happens.
       Measured proof: §33 sets ul gap 0, computed value was 6.72px.

   (b) AN ORPHANED COLUMN. "Standards & Trust" was added as a sixth block. In
       the 2-up grid it landed on a row of its own beside dead space, because
       both blocks after it are forced full-width. 206px for one short list.

   (c) SERVICE AREAS AS A STACK. Eight city names, one per row, 206px. They
       are short labels; a stack is the wrong shape for them.
   ========================================================================== */
@media (max-width: 899.98px) {

  /* (a) Re-assert over §38. Scoped, so it cannot leak upward to desktop. */
  .site-foot .foot-col h3 { margin-bottom: 0.5rem; font-size: 0.66rem; }
  .site-foot .foot-col ul { gap: 0; font-size: 0.84rem; }
  .site-foot .foot-col ul a { padding-block: 0.4rem; }   /* ~34px, still tappable */

  /* (b) Let the last two blocks share a row instead of claiming one each.
     `order` moves Service Areas to the end so the pairing lands correctly
     without touching the HTML. */
  .site-foot .foot-areas { order: 1; }
  .site-foot .foot-col:last-child { grid-column: auto; }
  .site-foot .foot-col:last-child ul { display: block; }
  .site-foot .foot-col:last-child .btn { margin-top: 0.7rem; }

  /* (c) Cities run inline and wrap, with a hairline separator carrying the
     rhythm. Four rows becomes two. */
  .site-foot .foot-areas { grid-column: 1 / -1; }
  .site-foot .foot-areas ul {
    display: flex;
    flex-wrap: wrap;
    gap: 0 0.55rem;
    align-items: baseline;
  }
  .site-foot .foot-areas li { display: flex; align-items: baseline; gap: 0.55rem; }
  .site-foot .foot-areas li:not(:last-child)::after {
    content: '';
    width: 1px; height: 0.72em;
    background: var(--bone-line);
  }
  .site-foot .foot-areas ul a { padding-block: 0.34rem; }

  /* Brand block: the strapline restates the tagline that is three lines up. */
  .site-foot .foot-about p { font-size: 0.82rem; margin-top: 0.55rem; }
  .site-foot .foot-badges { margin-top: 0.7rem; }
  .site-foot .social { margin-top: 0.7rem; }
  .site-foot .social a { width: 38px; height: 38px; }

  .foot-grid { gap: 1.15rem 1.25rem; padding-block: 1.75rem 1rem; }

  /* The legal strip was 104px for two lines of 12px type. */
  .foot-bottom { padding-block: 0.8rem; font-size: 0.72rem; gap: 0.4rem; }
  .foot-bottom ul { gap: 0.2rem 1.1rem; }
  .foot-bottom ul a { padding-block: 0.4rem; }
}

/* ==========================================================================
   §52  MOBILE HERO — the second pass

   §50 gave it height. Height alone did not fix it: the copy block still
   reads as a dense slab because every line is near the full measure and the
   two calls to action carry equal visual weight.

   - The eyebrow wrapped to two lines. A kicker that wraps stops reading as a
     kicker and starts reading as a sentence.
   - h1 at line-height 1 with three wrapped lines needs its own tracking; the
     desktop value is set for a two-line lockup.
   - Two solid full-width buttons is two primary actions. "See the work" is
     secondary and should look it, which also returns ~20px.
   ========================================================================== */
@media (max-width: 719.98px) {
  /* Kicker holds one line: smaller, tighter tracking, and the rule shrinks. */
  .hero--photo .eyebrow {
    font-size: 0.58rem;
    letter-spacing: 0.13em;
    white-space: nowrap;
  }
  .hero--photo .eyebrow::before { width: 18px; margin-right: 0.5rem; }

  /* Three wrapped lines want slightly more leading than a two-line lockup. */
  .hero--photo h1 {
    font-size: clamp(2.5rem, 11.5vw, 3.4rem);
    line-height: 1.02;
    letter-spacing: -0.035em;
  }
  .hero h1 { margin-block: 1.15rem 1.3rem; }

  .hero--photo .hero__lede {
    font-size: 0.98rem;
    line-height: 1.58;
    max-width: 34ch;      /* four lines instead of a full-width block */
  }

  /* One primary, one secondary. */
  .hero__actions { margin-top: 1.9rem; gap: 0.55rem; }
  .hero--photo .hero__actions .btn--ghost {
    --btn-bg: transparent;
    --btn-bd: transparent;
    border-color: transparent;
    text-decoration: underline;
    text-underline-offset: 0.35em;
    text-decoration-thickness: 1px;
    text-decoration-color: rgba(241, 238, 228, 0.45);
    padding-block: 0.55rem;
  }
  .hero__dots { margin-top: 1.15rem; }
}

/* ==========================================================================
   §53  THE SCRIM IS TUNED FOR A LAYOUT THAT ONLY EXISTS ON DESKTOP

   .hero__media::after lays two gradients over the photograph. The first runs
   at 100deg — left to right — and holds 0.88 alpha across the left edge,
   because on desktop the copy sits in the left 68% and needs a dark bed under
   it. The second lifts the bottom.

   On a phone the copy is full-width (`.hero--photo .hero__copy { max-width:
   100% }` at 900px), so the horizontal gradient is no longer covering a
   column — it is covering the entire frame. The result is the photograph is
   ~80% blacked out and the house in it is barely legible. We are paying for a
   hero image and then hiding it.

   Below 900px the horizontal pass is dropped and only a vertical one remains:
   the copy sits at the bottom, so that is the only place that needs a bed.
   The top two-thirds of the photograph is allowed to be a photograph.
   ========================================================================== */
@media (max-width: 899.98px) {
  .hero__media::after {
    background:
      linear-gradient(to top,
        rgba(18, 24, 30, 0.92) 0%,
        rgba(18, 24, 30, 0.80) 26%,
        rgba(18, 24, 30, 0.42) 48%,
        rgba(18, 24, 30, 0.12) 68%,
        rgba(18, 24, 30, 0.06) 100%);
  }
  /* The floating header controls sit on the brightest part of the frame now.
     They already carry their own light rim and drop shadow, which is what
     separates them — so this wash only has to take the edge off, not build a
     bed. The first version was 130px at 0.38 and read as the top of the
     photograph having been cut off, which is precisely the thing §53 exists
     to stop doing. Shorter, weaker, and eased so it has no visible end. */
  .hero__media::before {
    content: '';
    position: absolute;
    inset: 0 0 auto;
    height: 96px;
    z-index: 1;
    background: linear-gradient(to bottom,
      rgba(18, 24, 30, 0.20) 0%,
      rgba(18, 24, 30, 0.09) 45%,
      rgba(18, 24, 30, 0) 100%);
    pointer-events: none;
  }
}

/* ==========================================================================
   §54  TABLET — the size nobody looks at

   Measured at 820x770 (iPad portrait):

   (a) The top bar wrapped to two rows. It is a 4-item utility strip; at 820px
       the four do not fit on one line, so it doubled its own height to say
       nothing new. Below 900px it keeps the licence and the location — the
       two facts that do work — and drops the hours/CTA pair, which are both
       repeated in the header and the footer.

   (b) The header panel is 84px tall with a logo at one end, a phone number
       and a hamburger at the other, and roughly 400px of nothing between.
       Nav links are hidden below 1080px, which is right for a phone and far
       too early for a tablet: at 820px there is room for all six. The
       breakpoint moves to 860px and the spacing tightens to fit.

   (c) The hero was 478px on a 770px-tall viewport, because `min(56vw, 74vh)`
       resolves on WIDTH at this size. A letterboxed hero on a tablet held
       three lines of copy in a 478px band with the fact strip crowding it.
   ========================================================================== */

/* (a) */
@media (max-width: 899.98px) and (min-width: 720px) {
  .topbar__list:last-child { display: none; }
  .topbar__inner { justify-content: flex-start; }
  .topbar__list { gap: 1.75rem; }
}

/* (b1) iPad PORTRAIT, 720–860px. Seven links do not fit here beside a 200px
   brand and a phone number — "What We Build" alone is 110px — so the
   hamburger stays. What was actually wrong is that the panel was 84px tall
   and four-fifths empty. The estimate CTA is hidden below 1080px for phone
   reasons that do not apply to a tablet; putting it back fills the gap with
   the one control the page most wants pressed, and the panel loses the
   height it was not using. */
@media (min-width: 720px) and (max-width: 859.98px) {
  .nav__cta .btn { display: inline-flex; }
  .site-head .nav { min-height: 70px; --pad: 1.2rem; }
  .site-head .nav__cta { gap: 0.6rem; }
  .site-head .nav__phone { font-size: 0.9rem; }
}

/* (b2) Above 860px the six links themselves fit, so the hamburger goes and
   the real nav returns. Measured, not guessed: below this the last link
   collides with the phone number. */
@media (min-width: 860px) and (max-width: 1080px) {
  .nav__links { display: flex; }
  .nav__cta .btn { display: none; }        /* CTA stays out; the phone is enough */
  .nav__toggle { display: none; }
  .site-head .nav { --pad: 1.1rem; gap: 1rem; }
  .site-head .nav__links { gap: 0.15rem; }
  .site-head .nav__links a { padding-inline: 0.5rem; font-size: 0.83rem; }
  .site-head .nav__phone { font-size: 0.86rem; }
}

/* (c) */
@media (min-width: 720px) and (max-width: 1023.98px) {
  .hero--photo {
    min-height: min(72svh, 640px);
    padding-block: clamp(6rem, 16vh, 9rem) clamp(2.5rem, 6vh, 3.75rem);
  }
  .hero--photo .hero__copy { max-width: 82%; }
  .hero--photo .hero__lede { max-width: 46ch; }
}

/* ==========================================================================
   §55  MATERIAL REFINEMENT

   §26 already built the glass: tokens, an ambient field, frosted surfaces, a
   specular top edge. This does not replace any of it and adds no new
   backdrop-filter — that is the expensive property and it is already applied
   to every surface that should have it.

   What separates cheap glass from expensive glass is not transparency, it is
   EDGES AND LIGHT. Cheap glass is a flat white fill at 50% with a blur behind
   it. Expensive glass has a fill that is brighter at the top than the bottom
   (light falls from above), a hairline that catches that light on its top
   edge and loses it on its bottom edge, a near shadow that grounds it, a wide
   diffuse shadow that lifts it, and grain so none of those gradients band.

   Four of the five changes below are TOKEN changes. They propagate to every
   component §26 already styled, which is why this is ~60 lines rather than a
   rewrite, and why reverting it is deleting one block.

   Deliberately NOT done: more transparency (it costs contrast and the site's
   tokens are AA-tuned), glass on the dark bands (they are the solid ground
   the glass reads against — making everything glass makes nothing glass), and
   blur on any new element.
   ========================================================================== */

:root {
  /* 1. FILL — was a flat rgba(255,255,255,0.58). Same average opacity, but
     graded, so a card looks lit rather than painted. Slightly denser at the
     top also means text at the top of a card sits on MORE opacity, not less,
     so this reads better and measures better at the same time. */
  --glass-grad: linear-gradient(
      168deg,
      rgba(255, 255, 255, 0.80) 0%,
      rgba(255, 255, 255, 0.62) 42%,
      rgba(255, 255, 255, 0.52) 100%);

  /* 2. ELEVATION — was 3 layers. Six, with the near pair tight and dark and
     the far pair wide and weak, is what makes an object sit ON a surface
     instead of floating over a grey smudge. */
  --glass-lift:
      inset 0  1px 0 rgba(255, 255, 255, 0.95),
      inset 0 -1px 0 rgba(27, 34, 41, 0.045),
      0 0 0 1px rgba(27, 34, 41, 0.055),
      0 1px 2px rgba(27, 34, 41, 0.035),
      0 8px 18px -10px rgba(27, 34, 41, 0.16),
      0 30px 60px -34px rgba(27, 34, 41, 0.34);
  --glass-lift-h:
      inset 0  1px 0 rgba(255, 255, 255, 1),
      inset 0 -1px 0 rgba(27, 34, 41, 0.05),
      0 0 0 1px rgba(9, 94, 65, 0.14),
      0 2px 4px rgba(27, 34, 41, 0.05),
      0 14px 28px -14px rgba(9, 94, 65, 0.16),
      0 42px 74px -38px rgba(27, 34, 41, 0.42);
}

/* Apply the graded fill wherever §26 put the flat one. `background-image`
   layers OVER the existing background-color, so the non-backdrop-filter
   fallback still has its solid colour underneath and nothing goes see-through
   on an engine that cannot blur. */
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .card, .quote, .quotebox, .feature, .stat,
  .compare__col, .steps-strip li, .proj__img {
    background-image: var(--glass-grad);
  }
}

/* 3. GRAIN — the reason expensive interfaces do not band.
   Every gradient on this page is a wide, low-contrast ramp, which is exactly
   the case 8-bit colour renders as visible stripes. A little noise dithers
   them away, and adds a tooth that reads as material rather than as screen.
   html::before, because body's two pseudo-elements are already the ambient
   field and the grid. soft-light so it works over the pale sections AND the
   slate bands without lightening either. */
html::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 500;              /* over header (100) and drawer (99); under skip (999) */
  pointer-events: none;
  opacity: 0.055;
  mix-blend-mode: soft-light;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 140px 140px;
}

/* 4. THE FIELD BREATHES. A static gradient behind glass is wallpaper; one
   that moves imperceptibly is depth. Oversized by 12% so the transform never
   drags an edge into frame, and it animates TRANSFORM only — the compositor
   moves an already-rasterised layer, so this costs nothing per frame. */
body::before { inset: -12%; }
@media (prefers-reduced-motion: no-preference) {
  body::before {
    animation: field-drift 48s ease-in-out infinite alternate;
    will-change: transform;
  }
}
@keyframes field-drift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(-2.2%, 1.6%, 0) scale(1.06); }
}

/* 5. HOVER — the lift was a shadow swap with no movement, which reads as the
   shadow changing rather than the card rising. 2px and a spring. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .card, .feature, .quote, .proj__img {
    transition:
      box-shadow 0.4s var(--ease),
      transform  0.4s cubic-bezier(0.34, 1.4, 0.64, 1);
  }
  .card:hover, .feature:hover, .quote:hover { transform: translateY(-2px); }
}

/* Fallbacks. Both of these already exist for §26; they have to cover the new
   layers too or the refinement becomes a regression for the people who asked
   the OS not to do this. */
@media (prefers-reduced-transparency: reduce) {
  html::before { display: none; }
  .card, .quote, .quotebox, .feature, .stat,
  .compare__col, .steps-strip li, .proj__img { background-image: none; }
}
@media (prefers-reduced-motion: reduce) {
  body::before { animation: none; }
}
/* Grain is decoration; it must never end up in a printout or on a saved PDF. */
@media print { html::before { display: none; } }

/* ==========================================================================
   §56  THE ASSISTANT AS A GLASS POPUP

   The panel was a solid white box with a hairline — correct, safe, and
   visually unrelated to the header floating twenty pixels above it. It is the
   one surface on the site that ALWAYS sits over arbitrary page content, which
   makes it the best possible candidate for real glass: blur is doing genuine
   work here (separating the panel from whatever is behind it), not decoration.

   It is also cheap to do properly: one element, only in the DOM's paint path
   while open. Everywhere else in §55 I avoided adding backdrop-filter. Here it
   earns its cost.

   The header bar reuses §29's exact dark-glass recipe rather than a new one,
   so the assistant reads as the same product as the site header instead of a
   widget someone bolted on.
   ========================================================================== */

.ai__panel {
  border-radius: var(--r-lg);
  border-color: rgba(255, 255, 255, 0.55);
  box-shadow: var(--glass-lift-h);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .ai__panel {
    /* 0.88 at the top where the header sits, easing back as it descends.
       Below ~0.82 the log text starts picking up whatever is behind it. */
    background:
      linear-gradient(168deg,
        rgba(252, 253, 252, 0.90) 0%,
        rgba(248, 250, 249, 0.84) 45%,
        rgba(245, 248, 246, 0.82) 100%);
    -webkit-backdrop-filter: blur(30px) saturate(165%);
    backdrop-filter: blur(30px) saturate(165%);
  }

  /* Same sheet as the site header (§29), so the two read as one system. */
  .ai__head {
    background: linear-gradient(180deg, rgba(20, 27, 33, 0.90), rgba(12, 18, 23, 0.94));
    -webkit-backdrop-filter: blur(26px) saturate(150%);
    backdrop-filter: blur(26px) saturate(150%);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
  }

  /* A bot message at 4.5% ink is invisible once the surface under it is
     already translucent. Give it its own small pane instead. */
  .ai__msg--bot {
    background: linear-gradient(168deg, rgba(255,255,255,0.82), rgba(255,255,255,0.62));
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.9),
      0 0 0 1px rgba(27, 34, 41, 0.05),
      0 6px 14px -10px rgba(27, 34, 41, 0.22);
  }
  /* The visitor's own message stays solid forest. If everything is glass,
     nothing is — the accent is what the glass is read against. */

  .ai__form {
    background: linear-gradient(to top, rgba(255,255,255,0.55), rgba(255,255,255,0.18));
    border-top-color: rgba(255, 255, 255, 0.6);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
  }
  .ai__input {
    background: rgba(255, 255, 255, 0.68);
    border-color: rgba(27, 34, 41, 0.14);
    box-shadow: inset 0 1px 2px rgba(27, 34, 41, 0.05);
  }
  .ai__input:hover { background: rgba(255, 255, 255, 0.82); }
  .ai__chip {
    background: rgba(255, 255, 255, 0.62);
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), 0 1px 2px rgba(27,34,41,0.04);
  }
  .ai__chip:hover { background: rgba(255, 255, 255, 0.92); }
}

/* The launcher gets the same light treatment as the rest of the material
   system: graded fill, specular top edge, layered lift. Still solid forest —
   it is a call to action sitting on the page, not a pane floating over it. */
.ai__fab {
  background-image: linear-gradient(168deg, rgba(255,255,255,0.16), rgba(255,255,255,0) 58%);
  box-shadow:
    inset 0  1px 0 rgba(255, 255, 255, 0.28),
    inset 0 -1px 0 rgba(0, 0, 0, 0.14),
    0 1px 2px rgba(27, 34, 41, 0.10),
    0 10px 22px -12px rgba(9, 94, 65, 0.55),
    0 28px 52px -30px rgba(27, 34, 41, 0.45);
}
.ai__fab:hover {
  box-shadow:
    inset 0  1px 0 rgba(255, 255, 255, 0.34),
    inset 0 -1px 0 rgba(0, 0, 0, 0.16),
    0 2px 4px rgba(27, 34, 41, 0.12),
    0 16px 30px -14px rgba(9, 94, 65, 0.62),
    0 40px 70px -34px rgba(27, 34, 41, 0.5);
}

@media (prefers-reduced-transparency: reduce) {
  .ai__panel { background: #fff; -webkit-backdrop-filter: none; backdrop-filter: none; }
  .ai__head  { background: var(--slate-deep); -webkit-backdrop-filter: none; backdrop-filter: none; }
  .ai__msg--bot { background: rgba(27,34,41,0.06); box-shadow: none; }
  .ai__form  { background: transparent; }
  .ai__input { background: #fff; }
  .ai__chip  { background: #fff; }
  .ai__fab   { background-image: none; }
}

/* ==========================================================================
   §57  THE UTILITY STRIP AS A GLASS CAP

   It was a 40px full-bleed slab of #1B232B running edge to edge — the widest,
   heaviest element on the page, spent on four short facts. Everything below it
   floats: the header is a rounded glass panel, the cards are rounded glass.
   The strip was the one thing still built like a 2014 website.

   It becomes a cap: a thinner, narrower pane in grey glass with rounded top
   corners, sitting ON the header slab rather than above it. Narrower on
   purpose — matching the header's width would read as two stacked bars, and
   inset reads as one assembly, the way a lid is smaller than the box.

   Two mechanical details this needs:

   1. THE BAND HAS TO COME UP. `.topbar::after` is the dark contour backdrop
      and it starts at `top: 100%` — below the strip, because the strip used to
      paint its own background. A transparent strip over nothing would show
      paper above the glass. It now starts at the top of the page and covers
      the strip too.

   2. PAINT ORDER. `::after` is generated as the last child, so a positioned
      pseudo-element paints OVER a non-positioned sibling. That never mattered
      while the band sat entirely below the strip; now that they overlap, the
      band would cover the text. Both get an explicit layer.
   ========================================================================== */

@media (min-width: 720px) {

  .topbar {
    background: transparent;
    padding-top: 0.5rem;
    font-size: 0.7rem;                 /* was 0.77 */
  }

  /* (1) + (2): backdrop up, and behind. */
  .topbar::after {
    top: 0;
    height: calc(100% + 116px);
    z-index: 0;
  }

  .topbar__inner {
    position: relative;
    z-index: 1;                        /* over the band */
    max-width: calc(var(--max) - 7rem);
    min-height: 30px;                  /* was 40 */
    /* Bottom padding is deliberately heavier: the last 10px sit behind the
       header slab, so the text needs to clear them. */
    padding: 0.1rem 1.35rem 0.6rem;
    margin-bottom: -10px;              /* tucks under the header panel */
    border-radius: 13px 13px 0 0;
    color: rgba(243, 245, 244, 0.82);  /* lifted: grey glass is lighter than #1B232B */

    background: linear-gradient(180deg,
      rgba(126, 139, 150, 0.34) 0%,
      rgba(82,  96, 108, 0.40) 55%,
      rgba(62,  75,  86, 0.46) 100%);
    box-shadow:
      inset 0  1px 0 rgba(255, 255, 255, 0.22),
      inset 1px 0 0 rgba(255, 255, 255, 0.07),
      inset -1px 0 0 rgba(255, 255, 255, 0.07),
      0 -1px 0 rgba(0, 0, 0, 0.10);
  }
  @supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
    .topbar__inner {
      background: linear-gradient(180deg,
        rgba(126, 139, 150, 0.26) 0%,
        rgba(82,  96, 108, 0.32) 55%,
        rgba(62,  75,  86, 0.38) 100%);
      -webkit-backdrop-filter: blur(16px) saturate(140%);
      backdrop-filter: blur(16px) saturate(140%);
    }
  }

  .topbar__list { gap: 1.15rem; }
  .topbar strong { font-weight: 650; }
}

/* The cap is narrower than the header slab, and on a small tablet that pushes
   the four items into a wrap. §54 already drops the second list below 900px;
   this just keeps the remaining pair centred in the narrower pane. */
@media (min-width: 720px) and (max-width: 899.98px) {
  .topbar__inner { max-width: calc(var(--max) - 3rem); }
}

@media (prefers-reduced-transparency: reduce) {
  .topbar__inner {
    background: #2B3742;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* ==========================================================================
   §58  THE BAND MUST NOT CUT A LINE ACROSS THE PHOTOGRAPH

   `.topbar::after` is the dark contour backdrop the floating header reads
   against. It is 116px tall with a HARD bottom edge, and the hero begins
   directly under the utility strip — so on the home page that edge lands
   ~115px down the hero image and draws a visible horizontal seam straight
   across the roofline. Measured, not guessed: hero.top = 29, band bottom =
   144, overlap = 115px.

   The image is not cropped. It is covered, which looks identical to the
   visitor and is worse, because we are paying to serve a photograph and then
   painting over the top eighth of it.

   The band still has to exist — it is what makes the dark glass header legible
   before you scroll. It just has to END without an edge. A mask fades the last
   third to nothing, so the backdrop is at full strength behind the header and
   has dissolved by the time it reaches the picture.
   ========================================================================== */
.topbar::after {
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,0.55) 78%, transparent 100%);
  mask-image:         linear-gradient(to bottom, #000 0%, #000 58%, rgba(0,0,0,0.55) 78%, transparent 100%);
}

/* ==========================================================================
   §59  LEGIBILITY WITHOUT DARKENING THE PICTURE

   Lightening the scrim in §53 was right for the photograph and cost the
   kicker: "OUTDOOR GENERAL CONTRACTOR · ROSEVILLE, CA" is 0.58rem sage type
   and it now lands on open sky at the point where the gradient has fallen to
   ~0.42. The old fix for this is to darken the scrim again, which just undoes
   §53 and hides the image a second time.

   Type shadow instead. It costs the photograph nothing — it is bound to the
   glyphs, not the frame — and it is what the scrim was being over-used to do.
   Two stacked shadows: a tight dark one for edge contrast against sky, and a
   wider soft one so the letterforms hold together over busy foliage.
   ========================================================================== */
@media (max-width: 899.98px) {
  .hero--photo .eyebrow {
    text-shadow:
      0 1px 2px rgba(10, 14, 18, 0.55),
      0 2px 12px rgba(10, 14, 18, 0.40);
  }
  .hero--photo h1 {
    text-shadow:
      0 1px 2px rgba(10, 14, 18, 0.30),
      0 4px 26px rgba(10, 14, 18, 0.32);
  }
  .hero--photo .hero__lede {
    text-shadow: 0 1px 10px rgba(10, 14, 18, 0.45);
  }
}

/* ---- Employee login, in the top bar ------------------------------------
   Quiet on purpose. It sits in the same row as the estimate link, which is
   what a customer came for, so it is styled DOWN rather than up: no accent,
   no arrow, and it reads as furniture until somebody is looking for it. */
.topbar__staff {
  color: inherit;
  opacity: 0.75;
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
}
.topbar__staff:hover,
.topbar__staff:focus-visible { opacity: 1; }



/* ==========================================================================
   45. ONE THEME, HEADER TO FOOTER
   ==========================================================================

   Owner, 2026-08-11: the home page reads like "10 themes mixed together".
   True, and self-inflicted. Five separate passes were appended here in one
   afternoon — glass on, glass off, cards frosted, cards stripped, header
   solid, header floating — each one overriding the last from further down the
   cascade. Six answers to the same question all shipped at once is not a
   design, it is an argument in public.

   This section REPLACES all of them. It is written as a system rather than a
   list of corrections, in the order the page is read: ground, chrome, hero,
   surface, type, motion. Where an earlier section of this file disagrees, the
   disagreement is named in the comment rather than silently overridden, so the
   next person can see which rule won and why.

   THE ONE IDEA, and everything below follows from it:

       DARK SURFACES ARE GLASS. PALE SURFACES ARE PAPER.

   Glass needs something behind it — a photograph, a page scrolling underneath.
   Where that exists (the header, the hero, the dark bands) the material is
   translucent, blurred and saturated, and you can see the site moving through
   it. Where it does not exist (the body, on #F5F4F0) there is nothing to
   refract, so a translucent panel composites to a flat white box. On paper the
   answer is not a better box: it is no box. Space and one hairline.

   That is why the page can carry both and still be one thing. The two
   materials never appear on the same ground.
   ========================================================================== */

/* ---- 1. ground ---------------------------------------------------------- */

/* The ambient gradient behind everything existed to give §26's frost something
   to refract. Nothing on the pale half is frosted now, so it is a tint on a
   page that wants to be paper. */
body::before { display: none !important; }

/* Sections alternate ground and separate with a hairline. No washes: 62% of a
   colour two shades off the page is not a section break, it is a smudge, and
   it was why the page read as one undifferentiated scroll. */
.bg-ink2, .bg-surface {
  background: var(--paper-2);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.pagehero {
  background: var(--paper-2) !important;
  border-bottom: 1px solid var(--line);
  padding-block: clamp(3.5rem, 8vh, 6.5rem) clamp(2.5rem, 6vh, 4.5rem);
}
.hero-facts { background: var(--paper); border: 0 !important; box-shadow: none !important; }

/* Display type runs to 4.6rem; sections were padded 2.75rem. The type had
   outgrown the spacing, so a heading sat closer to the section above it than to
   its own body copy. */
.section { padding-block: clamp(3.75rem, 7vw, 7rem); }
.section--tight { padding-block: clamp(2.75rem, 4.5vw, 4.25rem); }
.section-head { margin-bottom: clamp(2rem, 3.6vw, 3.25rem); }

/* Decoration that stood in for content, all three removed together: a 78px
   surveyor grid behind body copy, a 45-degree hazard stripe, and the contour
   lines under the page heroes. The photographs are the texture on this site. */
.bp-grid::before { display: none !important; }
.hazard, .hazard--thin { height: 1px !important; background: var(--line-warm) !important; }
.pagehero .hero__bg, .pagehero .hero__bg::after { display: none !important; }

/* ---- 2. chrome: the only glass on the page ------------------------------ */

/* The topbar is gone from every page: five facts in a 34px strip — licence,
   area, hours, "free site walk", and an EMPLOYEE LOGIN that does not belong on
   a customer's first impression. All of it is in the footer and the proof bar.
   Removing it is also what makes every page's header identical. */
.topbar { display: none !important; }

@media (min-width: 1081px) {
  .site-head {
    padding: 0 !important;
    background: transparent !important;
    border-bottom: 0 !important;
  }
  /* Full-bleed. A rounded panel inset from the edges with a drop shadow is the
     most template-looking element a page can have, and it cost 150px of the
     first screen. */
  .site-head .nav {
    position: relative;
    max-width: none !important;
    margin-inline: 0 !important;
    padding-inline: var(--gutter) !important;
    min-height: 72px !important;
    border-radius: 0 !important;
    border: 0 !important;
    box-shadow: none !important;
    /* AT THE TOP OF A PHOTOGRAPH THERE IS NO BAR AT ALL.
       A 22% veil with a brightness filter was still a band across the sky —
       and a band across the sky is the single thing that makes a first screen
       look like a template, because it announces "header" before the picture
       has said anything. The photograph now runs edge to edge from pixel zero;
       the logo, links and number float on it the way the mobile pills do.
       Nothing is lost, because the glass has not gone away — it arrives on
       scroll, where it has an actual job: carrying white type over pale page
       content that would otherwise swallow it. */
    background: transparent !important;
    border-bottom: 1px solid transparent !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    transition:
      background 0.45s var(--ease),
      backdrop-filter 0.45s var(--ease),
      border-color 0.45s var(--ease);
  }
  .site-head .nav::after, .site-head .nav::before { display: none !important; }

  /* WHAT HOLDS THE TYPE UP WHEN NOTHING IS PAINTED. The hero's scrim runs
     left-to-right, so the brand and links sit on 0.86–0.20 of shade but the
     phone number at 79% across sits on almost none. Shadow rather than a
     plate: it costs no rectangle and it travels with the glyph, so it works
     over sky, foliage or roof without any of them being covered up. */
  body:has(.hero--photo) .site-head:not(.is-stuck) .brand {
    filter: drop-shadow(0 2px 12px rgba(6, 10, 14, 0.62));
  }
  /* A HALO, NOT A GLOW. Measured worst case: the links sit on 7.2:1 but the
     phone number can land on blown sky at 202,203,203 — 1.6:1, unreadable —
     because the left-to-right scrim is spent by 88% of the frame and the sky
     shows through the gaps between glyphs. No veil weak enough to stay
     invisible fixes a 255 backdrop, so the contrast has to live at the glyph
     edge instead: four 1px offsets draw a dark outline that follows the
     letterform. It is the same reason map labels survive over satellite
     imagery, and it costs no rectangle. */
  body:has(.hero--photo) .site-head:not(.is-stuck) .nav__links a,
  body:has(.hero--photo) .site-head:not(.is-stuck) .nav__phone {
    color: #fff !important;
    text-shadow:
      1px 0 1.5px rgba(6, 10, 14, 0.9), -1px 0 1.5px rgba(6, 10, 14, 0.9),
      0 1px 1.5px rgba(6, 10, 14, 0.9), 0 -1px 1.5px rgba(6, 10, 14, 0.9),
      0 2px 18px rgba(6, 10, 14, 0.85);
  }
  body:has(.hero--photo) .site-head:not(.is-stuck) .nav__phone svg {
    filter: drop-shadow(0 0 1.5px rgba(6, 10, 14, 0.95));
  }

  /* Scrolled off the photograph it firms up — a change of degree, not of
     material, and now the only state in which the header paints anything.
     Interior pages are paper top to bottom, so they hold the glass from the
     first pixel; there is no photograph for them to keep out of. */
  .site-head.is-stuck .nav,
  body:not(:has(.hero--photo)) .site-head .nav {
    background: rgba(16, 22, 27, 0.30) !important;
    -webkit-backdrop-filter: blur(30px) saturate(210%) brightness(0.34) !important;
    backdrop-filter: blur(30px) saturate(210%) brightness(0.34) !important;
    border-bottom-color: rgba(255, 255, 255, 0.16) !important;
  }
  /* One hairline of light along the leading edge — what separates a pane of
     glass from a translucent rectangle, and the only place this file uses that
     trick. */
  .site-head.is-stuck .nav::after {
    content: '';
    display: block !important;
    position: absolute;
    inset: 0 0 auto 0;
    height: 1px;
    pointer-events: none;
    background: linear-gradient(90deg, transparent,
      rgba(255,255,255,0.30) 18%, rgba(255,255,255,0.36) 50%,
      rgba(255,255,255,0.30) 82%, transparent);
  }
  .brand__mark { width: 44px !important; height: 44px !important; }
}

/* MOBILE HAS NO BAR. On a handset a band across the top of a photograph makes
   the first screen look like a template whatever fills it, so nothing paints:
   the logo floats left, the phone and the menu float right as two pills
   carrying their own contrast. The estimate CTA is not lost — it is the hero's
   own primary button below, at four times the size.
   CONDITIONAL ON THERE BEING A PHOTOGRAPH. Interior pages are paper, and
   near-white type on #ECEAE3 measures 1.03:1 — invisible. They get the bar. */
@media (max-width: 1080px) {
  .site-head {
    position: sticky !important;
    background: var(--slate-deep) !important;
    padding: 0 var(--gutter) !important;
  }
  .site-head .nav { min-height: 60px !important; }
  .nav__links { display: none !important; }
  .nav__cta { gap: 0.45rem; align-items: center; }
  .nav__cta .btn { display: none !important; }
  .brand__mark { width: 42px !important; height: 42px !important; }

  .nav__toggle, .nav__phone {
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    height: 44px;
    min-width: 44px;
    padding: 0 0.85rem;
    border-radius: 999px;
    color: var(--bone) !important;
    background: rgba(255, 255, 255, 0.10);
    border: 1px solid rgba(255, 255, 255, 0.22);
    -webkit-backdrop-filter: blur(14px) saturate(160%);
    backdrop-filter: blur(14px) saturate(160%);
    text-decoration: none;
    cursor: pointer;
  }
  .nav__toggle { display: grid !important; place-items: center; padding: 0; }
  .nav__toggle span, .nav__toggle span::before, .nav__toggle span::after {
    background: var(--bone); height: 2px; width: 18px; border-radius: 2px;
  }

  body:has(.hero--photo) .site-head {
    position: absolute !important;
    top: 0; left: 0; right: 0;
    z-index: 100;
    background: transparent !important;
    padding: clamp(0.85rem, 3vw, 1.4rem) var(--gutter) !important;
  }
  body:has(.hero--photo) .site-head .nav { min-height: 0 !important; background: none !important; }
  body:has(.hero--photo) .brand { filter: drop-shadow(0 2px 10px rgba(0,0,0,0.6)); }
  body:has(.hero--photo) .brand__name,
  body:has(.hero--photo) .brand__tag { color: var(--bone); }
  body:has(.hero--photo) .nav__toggle,
  body:has(.hero--photo) .nav__phone { background: rgba(18, 24, 29, 0.5); }

  /* ---- and the interior pages, which are the other half of that sentence ----
     The pills above carry their own dark plate BECAUSE they float on a
     photograph. On an interior page they were being dropped onto a flat
     `--slate-deep` slab — a dark disc on a dark bar, two values apart — and
     the whole header went soft and dated: no edge, no material, three vague
     shapes on a grey band.
     The bar is the same tinted glass the desktop interior header uses, so a
     page looks like one design at every width, and the page scrolling under
     it is what makes it read as glass rather than as a painted strip. Once
     the bar carries the contrast the buttons must stop trying to: no plate,
     no ring, just the icon. */
  body:not(:has(.hero--photo)) .site-head {
    background: transparent !important;
    padding: 0 !important;
  }
  body:not(:has(.hero--photo)) .site-head .nav {
    padding-inline: var(--gutter) !important;
    background: rgba(16, 22, 27, 0.30) !important;
    -webkit-backdrop-filter: blur(30px) saturate(210%) brightness(0.34) !important;
    backdrop-filter: blur(30px) saturate(210%) brightness(0.34) !important;
    border-bottom: 1px solid rgba(255, 255, 255, 0.16) !important;
    border-radius: 0 !important;
  }
  body:not(:has(.hero--photo)) .nav__toggle,
  body:not(:has(.hero--photo)) .nav__phone {
    background: transparent !important;
    border-color: transparent !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    /* THE BEVEL IS THE DATED PART. §33 gives these buttons an inset white
       top-highlight plus `0 10px 24px -12px` of drop shadow — a raised-button
       treatment that reads correctly over a photograph and reads like 2011
       over a dark bar, where it draws a grey disc around an icon that did not
       need one. Clearing the background without clearing this left the disc
       behind and the header still looked soft. */
    box-shadow: none !important;
  }
  /* The mark carries a 1px ring, a 5px halo AND a drop shadow. All three earn
     their place over a photograph, where the logo has to survive whatever is
     behind it. On a bar of one flat value they stack into a grey disc with a
     glow — the second thing making this header look a decade old. One hairline
     is all a dark monogram needs against a dark bar. */
  body:not(:has(.hero--photo)) .brand__mark {
    box-shadow: 0 0 0 1px rgba(243, 245, 244, 0.22) !important;
  }
  /* The tap target stays 44px — losing the plate must not lose the target. */
  body:not(:has(.hero--photo)) .nav__toggle:active,
  body:not(:has(.hero--photo)) .nav__phone:active {
    background: rgba(255, 255, 255, 0.12) !important;
  }
}
@media (max-width: 420px) {
  .brand__name { font-size: 1rem; }
  .nav__phone { padding: 0 0.7rem; }
}

/* ---- 3. the hero -------------------------------------------------------- */

/* THE PIN IS GONE BECAUSE THE CAROUSEL IS GONE. This block used to force
   slides 2-5 to `display:none !important` and hide the dots, holding a dead
   slideshow shut from the stylesheet so that editing the JS could not reopen
   it. The markup no longer has anything to hold shut: index.html carries one
   <picture> and the dots are deleted. See the note there for why one true
   photograph beat four rented ones. */

/* ONE SCRIM. §27 and §42 stacked three gradients each; a fourth was added on
   top and the photograph went black. Horizontal on desktop where the copy is a
   column, VERTICAL on mobile where the copy is full-width and a left-to-right
   fade leaves the end of every line on open foliage. */
/* FRAMED ON THE WORK, NOT THE BUILDING. §27 crops at `62% 50%`, which puts the
   house across the left half — the half the scrim darkens and the headline sits
   on — and pushes the planting, hedging and stone path off to the right edge.
   Shifting the crop right and down brings the landscaping into the open half of
   the frame, which is the half a reader actually looks at.

   THE GRADE. The source is flat and slightly hazy: a touch more contrast and a
   small black-point lift give it depth without the oversaturated look that
   makes stock photography obvious. Warmth is deliberate — a Sacramento yard at
   the end of the day is warm, and the cool cast on the original reads as
   overcast.

   THE HONEST LIMIT, recorded because the next person will wonder: this is the
   best framing available, not the best photograph. Every project image in the
   library is 1000px on its long edge — 3.8x upscale at full bleed on a retina
   laptop, measured — so the four 1920x1080 heroes are the only usable
   full-bleed assets, and this is the only one of the four showing built
   landscape rather than a house. A genuinely better hero is one afternoon with
   a camera on a finished job. */
/* RE-GRADED FOR THE DUSK FRAME. The previous values — `78% 62%` and
   brightness 1.16 / sepia 0.06 — were tuned to hero-3, a flat hazy phone shot
   that needed contrast and warmth pushed into it. Hero-1 is the opposite
   problem: a properly exposed blue-hour photograph that is simply dark, with
   lit windows already at the top of the range. Global brightness would blow
   those windows before it lifted the lawn.

   So the lift is small and the work is done by contrast REDUCTION, which
   opens the shadows where the planting and paving live, plus a little
   saturation for the grass and the sky. No sepia: the warm cast is already
   in the frame, coming from the windows, and adding more turns the dusk sky
   muddy brown.

   ALL OF THE ABOVE IS ABOUT A PHOTOGRAPH THAT IS NO LONGER HERE, and it is
   kept only so the next person can see how the numbers drifted. The file was
   replaced on 2026-09-10. It is not a hazy midday frame (hero-3) and not a
   dark blue-hour one with lit windows either: it is a backlit golden-hour
   lakefront yard, mean luminance 86/255, sun breaking through the canopy.

   RE-GRADED FOR IT, 2026-09-16:
     - `brightness(1.14)` went. It was lifting a frame that is not dark, and
       what it actually lifted was the flare, toward clipping.
     - `contrast(0.94)` REVERSED to 1.06. Reducing contrast on a backlit frame
       removes the one quality it has — the separation between the lit canopy
       and the shadow under it. That reduction was written for a flat phone
       shot that needed it.
     - saturation eased to 1.06; the warmth is in the light already.
     - the crop moves UP, 58% -> 46%. 58% was "drop some empty sky" on a frame
       with empty sky. This frame's top is the canopy and the sun, which is
       the reason it was chosen; pushing the crop down was cutting off the
       subject. (On desktop the box is 16:9 against a 16:9 source, so this
       only bites on the taller mobile box — which is where it was worst.) */
.hero--photo .hero__slide {
  object-position: 50% 46% !important;
  filter: contrast(1.06) saturate(1.06) !important;
}
/* MOBILE SCRIM, lightened with the desktop one and for the same reason: these
   numbers were set against a bright midday frame. Vertical rather than
   horizontal because on a phone the copy is full width, and a left-to-right
   fade would leave the end of every line sitting on open foliage.
   It stays heavier than the desktop gradient on purpose — the mobile header
   paints nothing at all over a photo hero, so the logo and the two pills are
   relying on this and on their own plates. */
.hero__media::after {
  background:
    /* FIRST IN THE LIST PAINTS ON TOP, and the bottom edge has to win there:
       if the reading scrim sat over it, the last row of pixels would be the
       scrim's tint rather than --paper-2 and the seam would come back as a
       smudge instead of a line. Shorter than the desktop fade because the
       phone box is taller and the seam sits closer to the thumb. */
    linear-gradient(to top,
      var(--paper-2) 0%,
      rgba(29,34,38,0.55) 6%,
      rgba(29,34,38,0.00) 20%),
    linear-gradient(to bottom,
      rgba(14,20,25,0.66) 0%, rgba(14,20,25,0.60) 45%,
      rgba(14,20,25,0.40) 72%, rgba(14,20,25,0.20) 100%) !important;
}
@media (min-width: 900px) {
  /* THREE LAYERS, each with one job.

     1. TOP-RIGHT CORNER DIM. The left-to-right scrim is spent by 88% of the
        frame, which is precisely where the phone number sits — measured at
        1.6:1 against blown sky. A corner vignette is the fix rather than a
        full-width band because it darkens only the ~18% of frame the number
        occupies and dies out diagonally, so it reads as lens falloff instead
        of as a header. Measured at the phone number: 1.62:1 worst pixel
        before, 3.60 worst / 7.41 mean after — the glyph halo carries the
        rest. (Worst-pixel over a photograph is pessimistic by construction:
        it finds the brightest pixel anywhere in the box, including the gaps
        between letters where nothing is drawn.)
     2. TOP-DOWN. Short and weak, spent by 13% of the frame. It gives the logo
        side a floor now that the header paints nothing.
     3. LEFT-TO-RIGHT. The original reading scrim: it darkens the third of the
        frame the headline sits on and leaves the rest a photograph. */
  /* HALVED FOR A DUSK FRAME. Every value here was set against hero-3, a bright
     midday photograph that needed a heavy veil before white type would sit on
     it. Hero-1 arrives dark: measured raw luminance under the headline is
     0.072, and the old scrim crushed that to 0.031 — a photograph paid for,
     then painted out. The left third read as a black rectangle with type on
     it, which is not the same thing as a photograph with type on it.

     Measured before: headline 13:1 mean, phone 13.27:1 worst. Both are far
     past what large white type needs, and contrast bought by hiding the image
     is contrast bought twice. Roughly halved throughout; re-measured after.

     The corner dim stays, because the top right being slightly down is a
     deliberate ask — but at 0.34 rather than 0.62, since the dusk sky behind
     the phone number is doing most of that work by itself now. */
  .hero__media::after {
    background:
      radial-gradient(ellipse 60% 26% at 100% 0%,
        rgba(8,12,16,0.34) 0%, rgba(8,12,16,0.16) 45%, rgba(8,12,16,0.00) 100%),
      linear-gradient(to bottom,
        rgba(10,14,18,0.34) 0%, rgba(10,14,18,0.14) 6%,
        rgba(10,14,18,0.00) 13%),
      linear-gradient(90deg,
        rgba(14,20,25,0.60) 0%, rgba(14,20,25,0.48) 28%, rgba(14,20,25,0.24) 52%,
        rgba(14,20,25,0.06) 72%, rgba(14,20,25,0.00) 88%) !important;
  }
}
.hero--photo .hero__inner { position: relative; z-index: 2; }

@media (min-width: 901px) {
  /* THE HEIGHT COMES FROM THE PHOTOGRAPH. §42 sizes the box off 56.25vw —
     16:9 of the viewport width — so `cover` has nothing to crop. A viewport
     cap and a fixed-ratio image cannot both win; the image wins. `--head-h`
     is retuned from §42's 111px (measured against a floating pill that no
     longer exists) to the bar's real height. */
  :root { --head-h: 72px; }
  /* NEVER TALLER THAN THE SCREEN. Sizing off 56.25vw gives the image its full
     16:9 so `cover` crops nothing — but on a laptop that is 891px against an
     821px viewport, so the photograph runs past the fold and is never seen
     whole. An image you cannot see all of is worse than one cropped on
     purpose: the crop is a decision, the fold is an accident.
     `min()` takes whichever is smaller, so on a tall window the image is
     uncropped and on a short one the hero ends exactly at the fold and
     `object-position` decides what is kept. `svh` rather than `vh` because on
     a phone `vh` measures the viewport WITHOUT the browser chrome, which is
     the one measurement that is wrong the moment the address bar is showing. */
  .hero--photo {
    min-height: min(calc(56.25vw + var(--head-h)), 100svh) !important;
    max-height: 100svh;
    /* CENTRED, not §42's `align-items: start`. That was right when the hero was
       short; in a 916px frame it left 144px above the headline and 406px of
       nothing below. `end` would drop the copy onto the fact strip. */
    align-items: center !important;
    padding-top: calc(var(--head-h) + clamp(1.5rem, 4vh, 3rem)) !important;
    padding-bottom: clamp(2rem, 6vh, 4.5rem) !important;
  }
  .hero--photo .hero__copy { max-width: min(940px, 76%) !important; }
}

/* The live-status pill, and the only accent in the hero. */
.hero__badge {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin: 0 0 1.1rem;
  /* SMALLER AND THINNER THAN IT WAS. Owner: "too big pill obstructing
     background image too much" — measured at 408x34 once "Book now" was added
     beside the tenure, which is a glass bar across the photograph rather than
     a badge on it. Every dimension here is doing the same job: less type, less
     tracking, less padding, and a fill at 6% instead of 10% so the image reads
     through it. The blur stays — it is what keeps the text legible over a
     photograph, and it costs no area. */
  padding: 0.3rem 0.72rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.16);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  backdrop-filter: blur(10px) saturate(140%);
  color: var(--bone);
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
}
/* THIS BLOCK WAS THE OLD STATUS DOT, ORPHANED WHEN ITS MARKUP WAS REMOVED —
   and it was silently overriding the hero's SLIDESHOW controls, which carry
   the same `.hero__dot` class and are defined 2,800 lines earlier as 30x3
   bars. Same specificity, later in the file, so this one won.
   Measured before assuming the worst: the controls are hidden site-wide by
   `.hero__dots { display: none !important }` further down, so nothing was
   visibly wrong and this is a latent collision rather than a live bug. It is
   still two unrelated rules fighting over one class name, and it would have
   surfaced the moment anyone unhid the controls.
   The replacement below is named for what it is, so it cannot collide again.

   ---- the availability dot -------------------------------------------------
   Owner: "'Book now' with a green pulsing dot signifying SUBTLY we're
   available." Subtle is the specification: the dot is 7px and the pulse is a
   ring expanding out of it over 2.4s, so the dot itself never moves or
   changes size. A dot that throbs is a notification badge; a ring that breathes
   reads as a light left on. */
.hero__book {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: inherit;
  text-decoration: none;
  /* Inherits the pill's uppercase tracking, so it reads as one line of type
     with a rule through it rather than as a button glued to a label. */
}
.hero__book:hover { color: #fff; }
.hero__book:hover .hero__live { background: var(--clay-hi); }

.hero__live {
  position: relative;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  /* Brighter than --rust and --sage both. This sits on a photograph under a
     dark scrim, where #095E41 disappears and #9ACBB2 reads as grey-green. It
     was the brand green lifted two steps — and then the brand stopped having
     a light green at all (the badge is peat, bone and steel), so the one
     pixel still from a different palette was this one. It is bone now: a
     bone dot still says "on" at 7 pixels, and it is the buttons' colour. */
  background: var(--volt);
  box-shadow: 0 0 0 2px rgba(74, 222, 147, 0.20);
  flex: 0 0 auto;
}
.hero__live::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(74, 222, 147, 0.9);
  animation: hero-live 2.4s var(--ease) infinite;
}
@keyframes hero-live {
  0%        { transform: scale(1);   opacity: 0.85; }
  70%, 100% { transform: scale(3.2); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero__live::after { animation: none; opacity: 0; }
}

/* ---- the service-area ticker ---
   NAMED `arearail`, NOT `areas`, and that is not a style preference. `.areas`
   already exists in this file as a four-column list component — `columns: 4` —
   so this section inherited it and every child was laid out into a 330px
   column. 1320 / 4 = 330, which is the number I stared at through two wrong
   guesses at flex sizing before walking the ancestor chain and reading the
   computed width. Third class collision found in this codebase this week,
   after `.hero__dot` and the orphaned status-dot rule.
   -------------------------------------------
   A thin strip under the hero naming every town, with a quiet label over it.
   Owner's design; the reasons it is built this way are in the markup.

   THE ANIMATION MOVES THE TRACK, NOT THE TEXT: one transform on a single
   composited element, so sixteen links cost the same as one and nothing
   reflows. The track holds two identical runs and travels exactly -50%, which
   is what makes the loop seamless — at the end of the cycle run two sits
   exactly where run one started.

   60s is deliberate. Fast enough to read as alive, slow enough that a name is
   legible and a link is catchable, and the whole strip is 34px tall so it
   never competes with the hero above it. */
.arearail {
  background: var(--paper-2);
  border-bottom: 1px solid var(--line);
  /* Tight. Stacking the caption under the label took the strip from 48px to
     78px, and the owner asked for a sliver — so the padding comes back out and
     the two head lines are set to sit close. */
  /* Air. At 0.35rem the strip was 47px holding two stacked lines and a rail,
     and the owner's word for the result was clutter. Height is the cheapest
     thing to give a busy row. */
  padding: 0.7rem 0;
  overflow: hidden;
}
/* ITS OWN ELEMENT, NOT SHARED WITH `.wrap`. The first version put this class
   on the same div as `.wrap` and the row collapsed to 330px inside a 1,440px
   section — the marquee was invisible on desktop and I changed the flex basis
   twice guessing at why before measuring the chain. `.wrap` already governs
   width and centring; giving it a second display role is what broke it. */
/* OWNS ITS OWN CONTAINER, and does not use `.wrap`. Inside this section
   `.wrap` computed to 330px against a 1,440px parent — measured on the
   ancestor chain, not guessed — and I changed the flex basis twice before
   measuring, which is the lesson rather than the fix. Whatever is doing that
   to `.wrap` here, this strip does not need it: it is one row that needs a
   max-width and a gutter, and it can state both itself. */
.arearail__inner {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
/* `.arearail__more` was the closing item inside the scroll and is gone: a
   statement of reach that only comes round once a minute is not a statement of
   reach. It is `.arearail__note` above the fold of the strip now. */
/* The label and its caption travel together and never scroll. */
/* A hairline and real space between the statement and the ribbon, so the eye
   reads them as two things rather than one run-on line. */
.arearail__head {
  flex: 0 0 auto;
  padding-right: 1.4rem;
  border-right: 1px solid var(--line);
}
/* SPECIFICITY, not shorthand. Written as `font: 500 0.66rem/...` this computed
   to 17.6px on desktop — larger than the label above it — because a later,
   more specific paragraph rule was overriding the shorthand's size while the
   narrow-viewport longhand further down still applied. The caption was bigger
   than its own heading at 1440 and correct at 390, which is the shape of a
   losing selector rather than a wrong value. Scoped to the section so it wins,
   and the size is stated on its own so a future shorthand cannot take it with
   it. */
.arearail .arearail__note {
  margin: 0.2rem 0 0;
  font-family: var(--body);
  font-size: 0.7rem;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.01em;
  color: var(--stone);
  white-space: nowrap;
}
/* The invitation is the point of the caption, so it carries the emphasis and
   the phone number behind it. Underlined rather than button-shaped: the strip
   is a caption, and a button here would outrank the hero's own call. */
.arearail__note a {
  color: var(--mark);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.arearail__note a:hover { color: var(--ink); }
/* The label is the way to the full list. Owner: make it clickable so nobody
   reads sixteen scrolling names as the limit — the section it points at has all
   twenty-five and says plainly that the list is not a boundary. Styled as the
   label still, with the underline only on hover: it is a signpost, not a
   button, and the caption below it already carries the invitation. */
.arearail__label a { color: inherit; text-decoration: none; }
.arearail__label a:hover { color: var(--mark); text-decoration: underline; text-underline-offset: 3px; }
.arearail__label {
  flex: 0 0 auto;
  margin: 0;
  /* SOFTENED. This was 700 weight, uppercase and tracked out to 0.16em, which
     on a label this small is a shout — the owner's word was aggressive, and
     the treatment was doing more of that than the words were. Sentence case at
     620 with normal tracking reads as a caption introducing a list, which is
     all it is. */
  font-family: var(--display), system-ui, sans-serif;
  font-size: 0.92rem;
  font-weight: 620;
  line-height: 1.25;
  letter-spacing: -0.005em;
  color: var(--ink);
  white-space: nowrap;
}
/* The fade tells the eye the strip continues rather than ending, and it is a
   mask so it works over the paper tone without matching a colour to it. */
.arearail__viewport {
  position: relative;
  /* `1 1 auto` sized this from its CONTENT, and the content is a 3,856px track
     — so the flex item asked for more than the row had, lost, and settled at
     zero width. The strip was invisible on desktop and 189px wide on a phone.
     `1 1 0` makes it take the space that is left instead of bidding for the
     space it wants, which is the whole point of the row. */
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
          mask-image: linear-gradient(to right, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
}
.arearail__track {
  display: flex;
  width: max-content;
  animation: arearail-scroll 60s linear infinite;
}
.arearail__run { display: flex; align-items: center; }
/* The separator is a pseudo-element so the dot is never selectable text and
   never read aloud — it is punctuation between items, not content. */
.arearail__run > * { position: relative; padding: 0 1.35rem; white-space: nowrap; }
/* Barely there. At 3px solid these punctuated every gap as loudly as the names
   they separated; the spacing already does the separating. */
.arearail__run > *::after {
  content: '';
  position: absolute; right: -1px; top: 50%;
  width: 2px; height: 2px; margin-top: -1px;
  border-radius: 50%;
  background: var(--line);
}
/* EVERY TOWN LOOKS THE SAME. Eight were links rendering in ink and eight were
   plain spans in stone, so the ribbon read as a speckle of emphasis with no
   meaning behind it — a visitor cannot tell which towns have pages and does
   not care. One weight, one colour, and the link only announces itself on
   hover, where it is useful. */
/* SCOPED LONGHAND, and this is the third time the same override has bitten in
   this one component. The town names were specified at 0.74rem/500 and were
   rendering at 17.6px/400 — a later, more specific paragraph rule beats a
   `font:` shorthand, and the shorthand loses SILENTLY because nothing in the
   cascade reports a dropped size.
   That oversized type in a thin strip is what the owner was seeing as clutter.
   The same signature as the caption bug an hour earlier: wrong at 1440, right
   at 390, because the narrow-viewport longhand further down still won its own
   fight. Any `font:` shorthand in this file is now suspect on sight. */
.arearail .arearail__run a,
.arearail .arearail__run span {
  font-family: var(--body);
  font-size: 0.74rem;
  font-weight: 500;
  line-height: 1.7;
  letter-spacing: 0.04em;
  color: var(--stone);
  text-decoration: none;
}
.arearail__run a:hover { color: var(--mark); text-decoration: underline; text-underline-offset: 3px; }
/* Stops for anyone trying to use it. Without this a link is a moving target. */
.arearail:hover .arearail__track,
.arearail:focus-within .arearail__track { animation-play-state: paused; }
@keyframes arearail-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
@media (max-width: 700px) {
  /* The head stacks above the rail on a phone: 36 characters of caption beside
     a marquee leaves the marquee about 120px, which is one town name and a
     half. Full width for both, one under the other. */
  .arearail__inner { flex-direction: column; align-items: flex-start; gap: 0.3rem; }
  /* `flex: 1 1 0` ON THE VIEWPORT IS A ROW DECLARATION, and this container is a
     column here — so flex-basis applied to HEIGHT and the viewport collapsed to
     0px with `overflow: hidden` over an 18px track. The towns were not narrow
     on a phone, they were clipped out of existence, which is why they looked
     absent rather than cramped.
     Reset to auto so the row sizes to its content, and full width because in a
     column the cross axis no longer stretches it for us. */
  .arearail__viewport { flex: 0 0 auto; width: 100%; }
  .arearail__label { font-size: 0.82rem; }
  .arearail .arearail__note { font-size: 0.66rem; }
  .arearail .arearail__run a,
  .arearail .arearail__run span { font-size: 0.7rem; }
  .arearail__run > * { padding: 0 0.9rem; }
}
/* An endlessly moving strip is an accessibility problem, not a taste one. With
   motion reduced the track stops, the duplicate run is hidden so nothing reads
   twice, and the names wrap onto as many lines as they need. */
@media (prefers-reduced-motion: reduce) {
  .arearail__track { animation: none; width: auto; }
  .arearail__run:nth-child(2) { display: none; }
  .arearail__run { flex-wrap: wrap; }
  .arearail__viewport { -webkit-mask-image: none; mask-image: none; }
}

/* A hairline between the offer and the credential. Not a bullet or a pipe
   character — those are text, and a screen reader reads them. */
.hero__badge-sep {
  width: 1px;
  height: 0.85em;
  background: rgba(255, 255, 255, 0.28);
  flex: 0 0 auto;
}

/* The proof bar: rating, reviews, builds, licence. Figures large, labels
   small — the number is the claim and the label says what it counts. */
.hero__meta--proof > div { align-items: flex-start; }
.hero__meta--proof dt {
  font-size: 0.68rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--faint);
}
.hero__meta--proof dd { display: flex; flex-direction: column; gap: 0.1rem; margin: 0.25rem 0 0; }
.hero__meta--proof dd b {
  font-family: var(--display);
  font-size: clamp(1.35rem, 2.2vw, 1.85rem);
  font-weight: 800; letter-spacing: -0.02em;
  color: var(--ink); line-height: 1.05;
}
.hero__meta--proof dd span { font-size: 0.8rem; color: var(--stone); }
.hero__meta--proof .stars { color: var(--mark); font-size: 0.95rem; letter-spacing: 0.12em; }

/* ---- 4.1: PROOF STATS SPREAD FULL-WIDTH, LARGER NUMERALS ------------------
   The four figures sat in a left-aligned flex row, so they bunched into the
   left half of the bar under a wide empty right. On dzt-landscaping.com the
   same four span the whole width at a larger size; this matches that — a
   four-track grid that fills the bar, numerals stepped up, and a little more
   air above and below. Scoped to .hero-facts so the hero's own meta list,
   which shares .hero__meta, is untouched. */
.hero-facts .hero__meta--proof {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(1.25rem, 3vw, 2.75rem);
  align-items: start;
}
.hero-facts .hero__meta { padding-block: clamp(1.5rem, 2.6vw, 2.25rem); }
.hero-facts .hero__meta--proof dt { font-size: 0.72rem; }
.hero-facts .hero__meta--proof dd b { font-size: clamp(1.7rem, 3.1vw, 2.7rem); }
.hero-facts .hero__meta--proof dd span { font-size: 0.9rem; }
.hero-facts .hero__meta--proof .stars { font-size: 1.1rem; }
@media (max-width: 640px) {
  .hero-facts .hero__meta--proof { grid-template-columns: repeat(2, 1fr); gap: 1.4rem 1.2rem; }
  .hero-facts .hero__meta--proof dd b { font-size: 1.5rem; }
}

@media (max-width: 700px) {
  .hero__meta--proof { gap: 1rem 1.6rem; }
  .hero__meta--proof dd b { font-size: 1.2rem; }

  /* MOBILE ONLY — the owner's read, and it is the correct one: at 1400px the
     pill is a badge sitting in a corner of a wide photograph, and at 390px the
     same pill is a bar across most of the frame. Desktop is untouched.
     What changes is only what buys width and weight: less tracking, smaller
     type, tighter padding, and a fill at 5% rather than 10% so the image reads
     through it. The blur stays — it costs no area and it is what keeps the
     text legible over a photograph. */
  .hero__badge {
    gap: 0.4rem;
    padding: 0.26rem 0.62rem;
    font-size: 0.58rem;
    letter-spacing: 0.07em;
    background: rgba(255, 255, 255, 0.05);
    border-color: rgba(255, 255, 255, 0.14);
  }
  /* The dot has to stay legible as a green light rather than shrink into a
     speck, so it is held at 6px while the type comes down around it. */
  .hero__live { width: 6px; height: 6px; }

  /* AND THE TENURE COMES OFF THE PILL ENTIRELY on a phone. Shrinking the type
     was not going to be enough: "12+ YEARS IN GREATER SACRAMENTO" is thirty
     characters, and at 390px that is what was making the pill 304px wide —
     78% of the screen, laid across the photograph.
     Nothing is lost by dropping it here, which is the only reason this is
     allowed. The proof bar directly under the hero already states "12+ /
     Years licensed", so on the narrow layout where the two sit almost on top
     of each other the pill was repeating its neighbour. What is left is the
     one thing the proof bar does NOT say — that we are available now — which
     is what the owner asked the dot to signal in the first place. */
  .hero__badge-sep,
  .hero__badge > span[data-cfg="booking"] { display: none; }
}

/* ---- 4. surfaces: paper takes no boxes ---------------------------------- */

/* THE RULE THIS SECTION EXISTS FOR. A card on #F5F4F0 has nothing behind it to
   refract, so frosting it composites to a white rectangle — which is what
   twenty-odd of them looked like. Content is separated by space and a hairline
   instead. §26's `@supports` block frosts eleven selectors; this is what
   overrides it, and it is deliberate rather than an accident of ordering. */
.card, .feature, .stat, .compare__col, .quote, .quotebox__body {
  background: transparent !important;
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
  box-shadow: none !important;
  border: 0 !important;
  border-radius: 0 !important;
  padding-inline: 0 !important;
}
.card::after, .quotebox::after, .feature::after,
.quote::after, .steps-strip li::after { display: none !important; }

/* A rule above each block rather than a box around it: in a grid that reads as
   a column rule and a row rule at once, which is what organises a set. */
.card, .feature, .stat, .compare__col {
  border-top: 1px solid var(--line) !important;
  padding-top: 1.35rem !important;
}
.card:hover, .feature:hover, .quote:hover { box-shadow: none !important; border-color: var(--line) !important; }

/* THE COMPARE PANEL, finished properly. The rule above took the fill off
   `.compare__col`, but `--us` was the DARK column: its heading is `--bone` and
   its list items `--bone-mute`, so removing the slate underneath left
   near-white type on #ECEAE3 paper. Measured 1.2:1 — the "What we build today"
   column was, in plain terms, not there. Two halves of the same list cannot be
   two different materials; on paper they are both paper, and the 1px grid gap
   is the only thing that needs to separate them. */
.compare {
  gap: 0 !important;
  background: transparent !important;
  border: 0 !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}
/* `.compare` used a 1px grid gap over a `--line` parent to draw its divider.
   With the columns transparent that trick inverts — the line shows through the
   whole panel instead of just the seam — so the divider becomes a border on
   the second column, which is what it was always pretending to be. */
.compare__col {
  background: transparent !important;
  padding: 0 !important;
  border-top: 0 !important;
}
@media (min-width: 720px) {
  .compare { column-gap: clamp(2rem, 5vw, 4rem) !important; }
  .compare__col + .compare__col {
    border-left: 1px solid var(--line) !important;
    padding-left: clamp(2rem, 5vw, 4rem) !important;
  }
}
@media (max-width: 719.98px) {
  .compare { row-gap: 1.75rem !important; }
  .compare__col + .compare__col {
    border-top: 1px solid var(--line) !important;
    padding-top: 1.75rem !important;
  }
}
.compare__col h3,
.compare__col--us h3 { color: var(--ink) !important; }
.compare__col li,
.compare__col--us li { color: var(--stone) !important; }
.compare__col--us li::before { color: var(--mark) !important; }
.compare__col--us .form-note,
.compare__col .form-note { color: var(--faint) !important; }

/* Chips and list items are NOT surfaces, and §37 line 2402 already said so:
   "without this each city kept a 1px outline and the list read as a table". */
.areas li, .tag, .filter, .steps-strip li {
  background: none;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* A frame round a photograph is the one border that adds nothing — an image
   has its own edges. */
.proj__img {
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
  border-radius: var(--r) !important;
  overflow: hidden;
}
.proj:hover .proj__img { box-shadow: none !important; }

/* WHAT KEEPS A FILL: things a person clicks. These are now the only filled
   rectangles on the pale ground, which is exactly what makes them read as
   interactive rather than decorative. */
.opt, .quotebox, .field input, .field textarea, .field select {
  background: var(--white) !important;
  border: 1px solid var(--line) !important;
  border-radius: var(--r) !important;
  box-shadow: none !important;
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}
.quotebox { border-radius: var(--r-lg) !important; overflow: hidden; }
.btn { box-shadow: none !important; }

/* ---- the estimate form -------------------------------------------------
 *
 * This is the one thing on the site that has to be USED rather than read, and
 * it was carrying every tell of a form template: a grey title bar above a
 * white body, three floating dashes for progress, seven bordered boxes in a
 * grid with a hole where the seventh should have been, hollow default-looking
 * radio rings, and a Next button adrift in the bottom-right corner.
 *
 * ONE SURFACE. The `--paper-2` header band was the biggest tell — a card with
 * a title bar is the shape of every contact form built since about 2014, and
 * it split one panel into two greys for no reason. The panel is now a single
 * white sheet and the progress rail does the dividing, which is a job it was
 * already halfway doing.
 */
.quotebox__head {
  display: grid !important;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 0.85rem 1rem;
  background: transparent !important;
  border-bottom: 0 !important;
  padding: clamp(1.4rem, 3vw, 2rem) clamp(1.4rem, 3vw, 2.25rem) 0 !important;
}
.quotebox__head > h3 { grid-column: 1; margin-right: 0 !important; }
.steps__label { grid-column: 2; }
/* Full width and flush, so it reads as "how far along am I" rather than as
   three decorative dashes beside a heading. */
.steps { grid-column: 1 / -1; gap: 3px; }
.steps span {
  flex: 1 1 auto;
  width: auto;
  height: 3px;
  border-radius: 999px;
  background: var(--line-2);
}
.quotebox__body { padding-top: clamp(1.25rem, 2.5vw, 1.75rem) !important; }

/* THE OPTIONS. A hollow 20px ring in `--line-2` is what an unstyled radio
   looks like, and at 1.5px it competed with the label beside it. The control
   is smaller and quieter; the SELECTED state is what got louder, because that
   is the only state a person needs to be able to read across the grid at a
   glance. Ring drawn with an inset shadow rather than a thicker border so
   selecting an option does not shift the text by half a pixel. */
.opt {
  padding: 0.85rem 1rem !important;
  gap: 0.75rem !important;
  align-items: center;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease),
              box-shadow 0.2s var(--ease);
}
.opt:hover { border-color: var(--stone) !important; background: var(--paper) !important; }
.opt__box { width: 18px; height: 18px; border-width: 1px; border-color: var(--line-2); }
.opt__box::after { width: 8px; height: 8px; }
.opt:has(input:checked) {
  border-color: var(--mark) !important;
  background: rgba(9, 94, 65, 0.05) !important;
  box-shadow: inset 0 0 0 1px var(--mark) !important;
}
/* Keyboard users get the same signal — `:focus-visible` on a visually hidden
   radio never reaches the label without this. */
.opt:has(input:focus-visible) {
  border-color: var(--mark) !important;
  box-shadow: 0 0 0 3px rgba(9, 94, 65, 0.18) !important;
}
/* SEVEN OPTIONS IN A TWO-COLUMN GRID leaves a hole beside the last one. An odd
   last child spans the full width instead, which turns the ragged corner into
   a deliberate final row. Works for any odd count, so adding an option later
   cannot reintroduce the gap. */
@media (min-width: 620px) {
  .opts--2 > .opt:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

/* THE FIELDS. Same 10px radius as every other control, and a focus ring that
   can actually be seen — 0.1 alpha over white was a suggestion of a ring. */
.field input, .field textarea, .field select {
  padding: 0.85rem 0.95rem !important;
}
.field input:focus, .field textarea:focus, .field select:focus {
  border-color: var(--mark) !important;
  box-shadow: 0 0 0 3px rgba(9, 94, 65, 0.16) !important;
}

/* THE FOOTER. Back and Next were floating at the bottom of the panel with
   nothing under them. A hairline turns the pair into a footer, which is also
   what tells you the step has ended. On a phone they go full width and stack
   Next first — the primary action should not be the one your thumb reaches
   second. */
.fnav {
  margin-top: 1.5rem !important;
  padding-top: 1.25rem !important;
  border-top: 1px solid var(--line);
  align-items: center;
}
@media (max-width: 560px) {
  .fnav { flex-direction: column-reverse; gap: 0.6rem !important; }
  .fnav .btn { width: 100%; justify-content: center; }
}
.btn:hover { box-shadow: none !important; transform: none; }
.btn::after { display: none !important; }

/* Reviews: three columns, a rule between what was said and who said it, and a
   monogram that anchors every card to the same baseline whatever the quote's
   length. */
.quote-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: clamp(1.5rem, 3vw, 2.5rem);
}
.quote { display: flex; flex-direction: column; gap: 1rem; padding: 0 !important; }
.quote__stars { color: var(--mark); font-size: 1.05rem; letter-spacing: 0.16em; line-height: 1; }
.quote blockquote {
  margin: 0; font-size: var(--step-0); line-height: 1.6;
  letter-spacing: 0; color: var(--ink); text-wrap: pretty;
}
.quote figcaption {
  margin-top: auto; padding-top: 1rem;
  border-top: 1px solid var(--line) !important;
  display: grid; gap: 0.15rem; font-size: 0.84rem;
}
.quote__who { display: flex; align-items: center; gap: 0.7rem; }
.quote__mono {
  display: grid; place-items: center;
  width: 34px; height: 34px; flex: none;
  border-radius: 999px;
  background: var(--slate-deep); color: var(--bone);
  font-weight: 700; font-size: 0.85rem;
}
.quote__src { color: var(--faint); padding-left: calc(34px + 0.7rem); }
.note-inline { margin: 0.6rem 0 0; font-size: 0.83rem; line-height: 1.5; color: var(--faint); }

/* ---- 5. type ------------------------------------------------------------ */

/* A display face at hero size needs negative tracking or it reads as a
   default. And a headline is ONE colour: the two-tone "Recent builds." device
   breaks a sentence at a point carrying no meaning, and at 4.6rem the colour
   change reads as a rendering error. The green is worth more where it is
   scarce — one CTA, the star rating, the booking dot. */
.hero h1, .h-xl, h1 { letter-spacing: -0.035em; }
h2 { letter-spacing: -0.022em; }
.eyebrow { letter-spacing: 0.16em !important; }
h1 .acid, h2 .acid { color: inherit; }
.hero__badge .acid, .hero__meta--proof .acid { color: var(--mark); }

/* ---- 6. motion ---------------------------------------------------------- */

/* [data-reveal] ran 20px over 0.7s, fired once 8% of an element had entered
   AND with the trigger line pulled 8% up the viewport — so on a full-height
   section the fade began once you were already inside it, and there was
   permanently a heading on screen at half opacity. Grey display type reads as
   a rendering fault. Half the distance, a third of the duration; main.js fires
   the observer 12% of a screen early so it has finished before anything is in
   a position to be read. */
/* Scoped for the same reason as the block above, though this one only tunes
   the motion rather than hiding anything: unscoped it leaves a 9px offset on a
   render that will never animate it away. */
.reveal-armed [data-reveal] {
  transform: translateY(9px);
  transition: opacity 0.28s var(--ease), transform 0.28s var(--ease) !important;
}
@media (prefers-reduced-motion: no-preference) {
  .reveal-armed [data-reveal] { will-change: opacity, transform; }
  .reveal-armed [data-reveal].is-in { will-change: auto; }
}
/* A card that rises on hover is the same trick as the frost, and on a touch
   screen it fires on tap so the thing you pressed jumps under your finger. */
.card:hover, .feature:hover, .quote:hover, .areas li:hover { transform: none; }

/* ---- 7. one radius scale ------------------------------------------------ */

/* SEVEN CORNER RADII WERE IN USE ON THE HOME PAGE: 2px on badges and inline
   links, 8px on eleven buttons, 12px on form options and project tiles, 18px
   on the estimate box, 50% on the logo, 99px on the consent buttons and 999px
   on the pills. The tokens declare 3 / 6 / 12 and almost nothing used them.
   Nobody reads a radius, but everybody sees a button at 8px sitting inside a
   panel at 18px next to an option at 12px — that mismatch is a large part of
   what reads as several themes at once.
 *
   Three values, and they mean three different things:
     PILL   (999px) — a status or a floating action. Fully round, so it can
                      never be mistaken for a panel.
     CONTROL (10px) — anything you click or type into: buttons, options,
                      inputs, and image tiles, which behave as targets here.
     PANEL   (14px) — a container that holds controls. One step larger than
                      what it contains, which is the only rule that keeps a
                      nested corner from looking wrong. */
:root { --r-sm: 4px; --r: 10px; --r-lg: 14px; }

.btn, .opt, .proj__img, .filter,
.field input, .field textarea, .field select { border-radius: var(--r) !important; }
.quotebox { border-radius: var(--r-lg) !important; }
.badge, .chip { border-radius: var(--r-sm) !important; }
.hero__badge, .ai__fab, .nav__toggle, .nav__phone,
.areas li { border-radius: 999px !important; }
/* `.tag` came out of this list. It is now a specification row under each
   catalogue entry — "Demo · Grading · Hardscape · Planting" — and a
   specification is squared, not a pill. The !important here was also beating
   the `.tag { border-radius: 0 }` further down, so the file contained the
   right instruction and an override that silently outranked it. */

/* ==========================================================================
   §46  THE ASSISTANT PANEL — one surface, and a way to ask for a call back

   Owner, 2026-08-11: "revolutionize the popup design for AI seems outdated"
   and "prompt customer to automatically input name and contact info saying
   that we will give them a call soon".

   WHAT READ AS OUTDATED, specifically — it was not the colours:

     A DARK TITLE BAR OVER A WHITE BODY. The same shape the estimate form had
     until §45 took it off: a card with a header slab is the silhouette of
     every support widget built since about 2014, and it splits one small
     panel into two materials for no reason. The head is now a title row on
     the same sheet with a hairline under it.

     GREY BUBBLES FOR THE ANSWER. A tinted rounded rectangle around every
     reply is the other half of that silhouette, and on this site it is also
     the "white box on paper" the owner has rejected three times. The
     ASSISTANT's text now sits directly on the panel, the way a page of prose
     does; only the VISITOR's own line keeps a pill, because that is the one
     distinction the eye actually needs. Nothing else changed about the
     transcript and no assertion in widget-test moved.

     THE DISCLOSURE AT BODY SIZE. Five lines of small legal print set in the
     same type as the answers, sitting above the conversation, was the first
     thing anyone read. It is small print now, and small print belongs next
     to the control it qualifies — down by the input, not over the content.

   THE PANEL KEEPS ITS SHADOW, deliberately, and it is the only shadow left
   on the site. §45 removed elevation everywhere because page cards were
   pretending to float. This one genuinely does float, over arbitrary content
   it does not control, and a floating layer with no elevation reads as a
   rendering fault.
   ========================================================================== */

/* THE FLOATING PANEL DID NOT FLOAT. Measured: panel #191E21 against a
   #161A1D page is 1.04:1 — three points of lightness, which at a glance is
   no difference at all, so the "floating" panel read as a rectangle cut into
   the page rather than a pane sitting over it. A dark re-point written to fix
   exactly this exists ~2,000 lines below and is ENTIRELY DEAD: it carries no
   !important and loses to these declarations every time, so anyone who edits
   it sees nothing change. Fixed here, where the !important actually is.

   A DROP SHADOW CANNOT MAKE DEPTH ON A DARK GROUND — a dark blur over a dark
   page is invisible, which is why the old one was doing nothing. What lifts a
   pane off a dark page is a RIM: one hairline of light along the top edge
   where a light would catch it, one dark ring cutting it out of the page, and
   only then a long shadow for distance. */
.ai__panel {
  border-radius: var(--r-lg) !important;
  border: 1px solid var(--line-2) !important;
  background: var(--paper-2) !important;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.07),
    0 0 0 1px rgba(0, 0, 0, 0.55),
    0 40px 80px -24px rgba(0, 0, 0, 0.85) !important;
  overflow: hidden;
}

/* The head joins the sheet. */
.ai__head {
  background: transparent !important;
  border-bottom: 1px solid var(--line) !important;
  padding: 0.95rem 1rem !important;
  align-items: flex-start;
}
.ai__title { color: var(--ink) !important; font-size: 0.98rem !important; letter-spacing: -0.01em; }
.ai__sub   { color: var(--stone) !important; font-size: 0.75rem !important; }
.ai__close { color: var(--faint) !important; }
.ai__close:hover { color: var(--ink) !important; }

/* The answer is prose, not a box. The question stays a pill. */
.ai__msg--bot {
  background: transparent !important;
  padding-inline: 0 !important;
  max-width: 100% !important;
  /* The inset highlight + 1px ring + drop shadow that §33 gives every surface
     is what was still drawing a box around the answer after the fill came off
     — the same bevel found on the mobile nav pills, in the same audit. */
  box-shadow: none !important;
}

/* The head subtitle said "Drainage, hardscape, planting — real answers, no
   sales pitch" directly above a greeting that now says we build all of it.
   Two claims about scope, stacked, before anything has been asked. The
   greeting is the better of the two and it stays. */
.ai__sub { display: none !important; }
.ai__msg--you {
  border-radius: 14px 14px 4px 14px !important;
  padding: 0.6rem 0.85rem !important;
}
.ai__msg--wait { padding-inline: 0 !important; }

/* Chips read as outlines, not filled tiles. */
.ai__chip {
  border-radius: 999px !important;
  background: transparent !important;
  border: 1px solid var(--line-2) !important;
  font-size: 0.79rem !important;
}
.ai__chip:hover { border-color: var(--mark) !important; background: rgba(9,94,65,0.05) !important; }

/* Small print, by the control it qualifies. */
/* STICKY, because it is a disclosure and it has to be seen. Moving it to the
   foot of the log was right — small print belongs by the control it qualifies
   — but the log scrolls, and with the greeting, the call card and the openers
   above it the disclosure sat below the fold on open. A disclosure a visitor
   has to scroll to find is the "buried in a footer" problem in miniature,
   which is the exact failure the comment above DISCLOSURE in assistant.js
   warns about. Sticky pins it to the bottom of the scroll area, so it is
   always the last thing above the input no matter how long the conversation
   gets. The opaque background is required: without it the transcript scrolls
   through the text. */
.ai__disclosure {
  flex: none;
  font-size: 0.7rem !important;
  line-height: 1.5;
  color: var(--faint) !important;
  padding: 0.6rem 1rem 0.2rem !important;
  border-top: 1px solid var(--line);
  background: var(--white);
}
.ai__disclosure a { color: var(--stone); }

/* ---- the callback card -------------------------------------------------
 * Offered AFTER an answer, never before one. A contact form that appears the
 * moment a panel opens is the pattern people close without reading, and it
 * would also make a liar of the panel's own promise of "real answers, no
 * sales pitch". Once a question has actually been answered, asking whether
 * they want a call is a continuation of the conversation rather than a toll
 * gate on it.
 */
/* NO BOX. A filled, bordered, rounded panel inside a filled, bordered,
   rounded panel is one frame too many in something this small — and it is the
   same rule §45 applies to the whole site: a hairline above a group organises
   it, a box around it just adds an edge. The INPUTS keep their fills, because
   those are the things a person clicks. */
.ai__cb {
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
  padding: 0.9rem 0 0.2rem;
  background: transparent;
  display: grid;
  gap: 0.5rem;
}
.ai__cb h4 {
  font-family: var(--display);
  font-size: 0.88rem;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.ai__cb p { font-size: 0.76rem; color: var(--stone); line-height: 1.5; }
.ai__cb input {
  width: 100%;
  font-size: 0.85rem;
  padding: 0.55rem 0.65rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--ink);
}
/* AN ACCESSIBILITY REGRESSION THAT WAS ALREADY SHIPPING. This removed the
   browser's focus ring and replaced it with a 3px glow in rgba(9,94,65) —
   the cream-era green. Over the panel's #1D2226 that composites to roughly
   #16281F: imperceptible. So the two callback fields, the ones a customer
   types their name and phone number into, had NO visible focus indicator at
   all, and nothing later re-added one. `:focus`, not `:focus-visible` —
   these are text inputs and a keyboard user is not the only one who needs to
   see where they are typing. */
.ai__cb input:focus {
  outline: 2px solid var(--mark);
  outline-offset: 1px;
  border-color: var(--mark);
  box-shadow: none;
}
.ai__cb-row { display: grid; gap: 0.45rem; }
@media (min-width: 380px) { .ai__cb-row { grid-template-columns: 1fr 1fr; } }
.ai__cb-go {
  font: 700 0.78rem/1 var(--display), system-ui, sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 0.6rem 0.9rem;
  border-radius: 999px;
  border: 0;
  background: var(--volt);
  color: var(--bone);
  cursor: pointer;
}
.ai__cb-go:hover { background: var(--clay-hi); color: var(--bone); }
.ai__cb-go:disabled { opacity: 0.55; cursor: default; }
.ai__cb-note { font-size: 0.72rem; color: var(--faint); }
/* Receipt after a callback is submitted — the form stays; this confirms it. */
/* 2.82:1, measured — #33703C on #191E21. This is the confirmation line the
   owner explicitly asked for ("simply notify them that we've recieve their
   number"), and it was the hardest thing in the panel to read. It was also
   the only green WORD in the whole widget, against this file's own rule that
   an action is green and a mark is steel. Ink, at 15:1. */
.ai__cb-note--ok { color: var(--ink); font-weight: 600; }
.ai__cb--done { border-top-color: var(--mark); }

/* The alternative to leaving a number, at the same weight as the button so the
   card offers two doors rather than one door and a footnote. */
.ai__cb-or { font-size: 0.78rem; color: var(--stone); }
.ai__cb-or a { color: var(--mark); font-weight: 650; text-underline-offset: 3px; }
.ai__assure { font-size: 0.8rem; color: var(--stone); }

/* The answer arrives word by word. Opacity only — the words occupy their space
   from the first frame, so nothing reflows as it reveals and the paragraph
   never jumps. `both` holds the pre-animation state, or every word would flash
   in at full opacity before its delay elapsed.

   NO TRANSFORM, AND NO inline-block. Both were here and they went together:
   transform does not apply to an inline box, so every word had been made a
   block to carry a 2px rise. At 63 words on a 420ms spread that is a ripple
   travelling through the paragraph — words arriving at slightly different
   baselines, which reads as text settling rather than text appearing, and it
   is the "sloppy" in the report. Dropping the transform lets the words go back
   to being ordinary inline text, so line breaking, wrapping and selection
   behave exactly as they do in the same paragraph unanimated.

   Slower per word than the 0.22s it replaced, over the same total budget: the
   fades now overlap into a wash across the line instead of 63 discrete
   flickers. */
.ai__w { animation: ai-word 0.55s var(--ease) both; }
@keyframes ai-word {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* THE BLOCK OPENS RATHER THAN APPEARING. The words fading in never stopped the
   message BOX from claiming its full height in a single frame — 44px of
   waiting dots replaced by 280px of answer, with everything below shoved down
   by the difference. That is the bang, it is layout rather than paint, and two
   rounds of softening the paint did not touch it.
   The height is set inline by grow() in assistant.js, which measures the
   finished turn and hands the transition its start and end. `overflow` is what
   keeps the text from spilling out of a box shorter than itself while it
   opens. A previous version rose 7px into place instead; a rise is a hint that
   something moved, and what was needed was for nothing to have to move. */
.ai__msg--grow {
  overflow: hidden;
  transition: height 0.52s var(--ease);
}
@media (prefers-reduced-motion: reduce) {
  .ai__msg--grow { transition: none; }
}

/* Everything that is NOT the sentence waits for the sentence. Source links and
   follow-up chips are pills and coloured text — the loudest things in the
   bubble — and they were at full opacity in the frame the first word started
   fading. The delay is the reveal budget, so they arrive as the last word
   lands: answer first, then what to do about it. */
.ai__msg--arriving .ai__src,
.ai__msg--arriving .ai__chips,
.ai__msg--arriving .ai__more,
.ai__enter { animation: ai-enter 0.4s var(--ease) both; animation-delay: 0.82s; }
@keyframes ai-enter {
  from { opacity: 0; transform: translateY(3px); }
  to   { opacity: 1; transform: none; }
}

/* ---- the wait ----------------------------------------------------------
   Three dots on a staggered breath. What was here was a static "…" that had
   itself been faded in once and then held still for the whole request —
   measured at 15px wide with no running animation on it or under it. The
   request it covers can run for seconds, and a frozen glyph for seconds is
   what a broken widget looks like.

   Sized and coloured as punctuation, not as a spinner: this is the assistant
   holding the floor, not a progress bar, and there is no progress to report. */
.ai__dots { display: inline-flex; gap: 0.28rem; align-items: center; padding: 0.15rem 0; }
.ai__dots i {
  width: 0.36rem;
  height: 0.36rem;
  border-radius: 50%;
  background: var(--stone);
  animation: ai-dot 1.25s var(--ease) infinite;
}
.ai__dots i:nth-child(2) { animation-delay: 0.16s; }
.ai__dots i:nth-child(3) { animation-delay: 0.32s; }
@keyframes ai-dot {
  0%, 70%, 100% { opacity: 0.28; transform: translateY(0); }
  35%           { opacity: 1;    transform: translateY(-2px); }
}

@media (prefers-reduced-motion: reduce) {
  .ai__w { animation: none; opacity: 1; transform: none; }
  /* No delayed entrance either. With no reveal to be late to, the delay would
     be a stall — the answer sitting there for 820ms with no links under it. */
  .ai__msg--arriving .ai__src,
  .ai__msg--arriving .ai__chips,
  .ai__msg--arriving .ai__more,
  .ai__enter { animation: none; opacity: 1; transform: none; }
  /* The dots stay, and stay legible — they are the only thing on screen
     saying the question was received. Held at three fixed weights so the row
     still reads as an indicator rather than as three identical specks. */
  .ai__dots i { animation: none; opacity: 0.75; }
  .ai__dots i:nth-child(2) { opacity: 0.5; }
  .ai__dots i:nth-child(3) { opacity: 0.3; }
}

/* Reset. Quiet by default — it is a way out, not an action anyone came for —
   and it only exists in the DOM once there is a transcript to clear. */
.ai__clear {
  /* The head is space-between with three children now, which spread the reset
     into the middle of the bar like a third piece of chrome. `auto` collects
     it and the close button into one group on the right, where the controls
     belong. */
  margin-left: auto;
  border: 0;
  background: none;
  padding: 0.2rem 0.45rem;
  margin-right: 0.15rem;
  font-family: var(--body);
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faint);
  cursor: pointer;
  border-radius: 999px;
}
.ai__clear:hover { color: var(--ink); background: rgba(27, 34, 41, 0.05); }
.ai__clear[hidden] { display: none; }
.ai__head { gap: 0.4rem; }

/* THE FIRST SCREEN OF AN ANSWER IS SIX LINES.
   Owner: "some AI chat responses are too long still initially." The paragraph
   split in assistant.js reduces a long answer to its first authored paragraph
   — and 41 entries across the two corpora have a first paragraph over 90
   words, the longest at 188. One paragraph is not a short answer when the
   paragraph is an essay.
   Height, not words: nothing is cut, so no sentence can be left as a
   preamble. 9.3em is exactly six lines at the .ai__msg line-height of 1.55,
   so the clamp lands BETWEEN lines rather than through one.
   The mask is what stops it reading as a rendering fault — text that stops
   dead at a hard edge looks broken, text that fades has obviously more behind
   it. It fades the content rather than blending into a colour because the
   panel is translucent over the page and there is no flat colour to match. */
.ai__lead--clamped {
  max-height: 9.3em;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 74%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 74%, transparent 100%);
}

/* The way out of every answer. Sits below the clamp, so it is visible whether
   the answer is two lines or two hundred — which is the point of it being a
   field on the response rather than a sentence inside the prose.
   Quieter than a button: this is the end of a sentence someone is reading, not
   a banner. The number itself carries the weight, because the number is the
   thing being offered. */
.ai__cta {
  margin-top: 0.6rem;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--stone);
}
.ai__cta-num {
  color: var(--mark);
  font-weight: 700;
  white-space: nowrap;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.ai__cta-num:hover { color: var(--ink); }

/* "Tell me more" — a continuation of the answer, so it reads as part of the
   message rather than as a control bolted under it: same left edge as the
   text, no fill, forest to mark it as the thing to press. */
.ai__more {
  justify-self: start;
  margin-top: 0.5rem;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--body);
  font-size: 0.82rem;
  font-weight: 650;
  line-height: 1.4;
  color: var(--mark);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.ai__more:hover { color: var(--ink); }


/* ==========================================================================
   §47  THE MOBILE FOOTER, RETHOUGHT — AND MEASURED

   Owner: "compacting into collapsible menus in a footer is odd... rethink
   design of footer for mobile." Right, and the accordion is gone. A footer is
   scanned, not operated; four drawers between a visitor and the warranty page
   is worse than a long footer.

   WHAT I TRIED AND MEASURED, recorded because the obvious idea was wrong and
   the next person will have it too:

     Accordion            872 -> 543px, and rejected. Correct on height, wrong
                          as an interaction.
     One full-width
     column, links
     wrapped inline       872 -> 968px. WORSE. The reasoning was that
                          `.foot-areas` fits eight links into 89px by wrapping,
                          so every group should. It does not carry over: at
                          390px a full-width row fits about two links, not
                          three, so six groups stacked vertically come out
                          taller than three rows of two columns. Areas is dense
                          because its labels are single short words, not
                          because wrapping is magic.

   WHERE THE FLOOR ACTUALLY IS. 17 navigation links plus 8 area links, each
   needing ~38px of touch height, is ~950px of link rows; in two columns that
   is ~475px, and the identity block and legal bar are another ~215px. Two
   columns is already the efficient arrangement and the groups already pack
   without waste — the pairs measure 198/198 and 213/213, so nothing is padding
   anything else out.

   SO THE HEIGHT IS THE CONTENT, and the only honest levers left are fewer
   links or smaller touch targets. Touch targets are not negotiable. What this
   block does instead is make the footer READ better at the height it has: a
   quieter heading so the links dominate their group, tighter leading between
   the identity block and the columns, and a real separation before the legal
   bar so the eye has somewhere to stop.
   ========================================================================== */
@media (max-width: 899.98px) {
  .foot-grid {
    grid-template-columns: 1fr 1fr;
    gap: 1.45rem 1.1rem;
    padding-block: 2rem 1.25rem;
  }

  /* A label over its links, not a title over a list. At 0.5 opacity the group
     names stop competing with the things people are actually looking for. */
  .foot-col h3 {
    font-size: 0.66rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    opacity: 0.5;
    margin-bottom: 0.2rem;
  }

  .foot-col ul { gap: 0; }
  .foot-col ul a { display: block; padding-block: 0.5rem; }

  /* Areas keeps the inline treatment it already had — single-word labels are
     the case where wrapping genuinely wins. */
  .foot-areas ul { display: flex; flex-wrap: wrap; gap: 0 0.9rem; }
  .foot-areas ul a { display: inline-block; }

  .foot-about { grid-column: 1 / -1; }
  .foot-about p { font-size: 0.85rem; max-width: none; margin-top: 0.7rem; }

  /* Somewhere for the eye to stop before the legal line. */
  .foot-bottom { border-top: 1px solid rgba(243, 245, 244, 0.10); padding-top: 0.9rem; }
}

/* ==========================================================================
   §48  THE CONSENT BAR

   A bar, not a modal. A modal over a landing page is a toll gate, and the
   thing it collects — permission to count page views — does not justify
   stopping someone reading. It sits at the bottom, it never covers the
   content, and the page works whether or not it is answered.

   BOTH ANSWERS LOOK THE SAME. "Essential only" and "Allow" are the same size,
   the same padding, the same distance from the thumb. Only the fill differs,
   and only so the primary reads as primary rather than as the safe one. A
   refusal styled as a footnote would be a dark pattern and would make any
   consent collected here worthless.
   ========================================================================== */
.consent {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 95;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.75rem 1.5rem;
  padding: 0.85rem clamp(1rem, 4vw, 2rem);
  background: rgba(16, 22, 27, 0.96);
  -webkit-backdrop-filter: blur(18px) saturate(180%);
  backdrop-filter: blur(18px) saturate(180%);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--bone);
  animation: consent-in 0.32s var(--ease) both;
}
@keyframes consent-in { from { transform: translateY(100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .consent { animation: none; } }

.consent__text {
  font-size: 0.82rem;
  line-height: 1.5;
  color: rgba(243, 245, 244, 0.82);
  max-width: 62ch;
}
.consent__text a { color: var(--bone); text-decoration: underline; text-underline-offset: 3px; }

.consent__actions { display: flex; gap: 0.6rem; flex: none; }
.consent__btn {
  font: 700 0.76rem/1 var(--display), system-ui, sans-serif;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.7rem 1.1rem;         /* ~40px, both of them */
  border-radius: 999px;
  border: 1px solid rgba(243, 245, 244, 0.34);
  background: transparent;
  color: var(--bone);
  cursor: pointer;
  white-space: nowrap;
}
.consent__btn:hover { border-color: var(--bone); background: rgba(243, 245, 244, 0.08); }
.consent__btn--go { background: var(--volt); border-color: var(--mark); }
.consent__btn--go:hover { background: var(--clay-hi); border-color: var(--ink); color: var(--bone); }

/* Lift the floating chrome clear of it — a notice that covers the phone
   number is worse than no notice. */
.has-consent-bar .ai { bottom: calc(1.25rem + 4.5rem); }
@media (max-width: 700px) {
  .consent { justify-content: stretch; gap: 0.6rem; }
  .consent__text { font-size: 0.78rem; }
  .consent__actions { width: 100%; }
  .consent__btn { flex: 1; text-align: center; }
  .has-consent-bar .ai { bottom: calc(0.75rem + 6.5rem); }
}

/* The opening line is a statement, not a reply — the display face at a heavier
   weight, one step up in size. Everything else in the log stays as it was:
   making the ANSWERS bold would be shouting, and the contrast between this
   line and them is what makes it read as the panel talking rather than the
   transcript. */
.ai__msg--greet p {
  font-family: var(--display), system-ui, sans-serif;
  font-weight: 700;
  font-size: 0.98rem;
  line-height: 1.4;
  letter-spacing: -0.015em;
  color: var(--ink);
}


/* ==========================================================================
   LOAM — WARM DARKS, A REAL GREEN, AND MID-TONES.

   The last palette was a near-neutral black with one high-chroma green on it,
   and that combination has exactly one register: sci-fi. It is not the green
   that does it, it is the SHAPE of the palette — two extremes and nothing in
   between. A pure black ground with a neon accent is a terminal, a game HUD, a
   crypto landing page. Three things fix it and all three are here:

     THE DARKS GO WARM. #121511 is brown-black — wet soil, charred oak — not
     #101110's neutral. A neutral black is a void; a warm one is a material,
     and this business works in materials.

     THERE ARE MID-TONES NOW. `--paper-3` at #262B25 is a real third value, and
     raised surfaces sit on it instead of on the page. Two values make a void
     with things floating in it; three make a room. This is the single biggest
     reason the last one read as an interface rather than as a brand.

     THE GREEN COMES DOWN OUT OF NEON. #8AE24F is a signal colour — 90% chroma,
     the green of a status LED. the old #5E9A6A is a leaf: same family, half the
     chroma, and it still clears 6.65:1 on the page and takes dark text at
     6.65:1 as a fill. It reads as something that grows rather than something
     that is powered.

   WHAT DID NOT CHANGE, because it was right: the site is dark end to end. That
   is what separates it from the reference and it is the correct ground for a
   page that is mostly photographs of yards.
   ========================================================================== */

:root {
  /* MATCHED TO THE MARK, 2026-09-07 — logo-8, the steel trim. Every value
     below moved by one small step and none of them moved far:

       TWO GROUNDS, BOTH FROM THE BADGE. The roundel is a green disc on a
       steel ring; the page is the same construction turned inside out. The
       body is steel — tones derived from the ring's hue (207 deg, low
       saturation) at 10 / 13 / 17.5 percent lightness for paper / paper-2 /
       paper-3 — first cut at 13.5 / 17 / 22, brought down on the owner's
       call to sit between the header's 6.5 and where it was: darker than a
       grey, a step above the peat bands and the header. The
       inverted bands, the header and the footer are peat — --slate and
       --slate-deep from the plate's own hue, a step above it — so the badge
       sits in its own material and the green buttons echo the bands rather
       than being the only green thing on a grey page. Steel page, green
       bands, bone type, badge-built buttons: four colours, all the badge's.

       THE TYPE IS THE BADGE'S WHITE. First #F2ECDD, the letters' warm bone —
       which on a cool steel page and a cool peat plate read yellow, in the
       hero headline and the badge at once. Then #F8F7F4, then pure white on the
       owner's call — one value shared by the page and the mark, and the end
       of this axis.

       THE QUIET ACCENT IS STEEL. Two aliases carried the secondary role in
       greens the mark never had: --sage (eyebrow rules on dark bands, the
       nav's current-page underline, focus rings) and --clay-soft (the
       eyebrows on the hero and the statement blocks, the topbar's accent,
       the footer badges). Both are the ring's galvanised grey now (#8A9AA0,
       6.3:1 on the ground), and the card/hover border (--line-warm) is the
       same grey at 30%. The first cut of this pass moved only --sage and
       reached almost nothing on the page — the eyebrows everyone actually
       sees are --clay-soft — which is why it looked like nothing had
       changed.

       THE BUTTONS ARE THE BADGE'S GREEN, EDGE TO EDGE. Owner's call, after
       a bone pass and a steel-edged pass: the fill is the stand's green
       lifted to where it reads as green rather than as black (#33703C, bone
       on it 5.1:1, 2.7:1 against the steel page), the label is the letters'
       bone, and the edge is the fill — a steel hairline on a green button was
       built, looked at, and read as trim. Hover goes a step DOWN the stand
       (#26552E, bone on it 7.4:1) because up fails AA. Every pressable thing
       on the site — pills, filters, the call bar, the chat send — is this
       green; steel is for marks and outlines, never for a fill.

     Measured after the change, not claimed: --volt on --paper 6.7 and on
     --paper-3 5.2 (the ground's own 6.6 standard, kept — the first cut of
     the needle hue came in at 5.5 and was lifted), --ink on --paper 15.5,
     --stone 7.4, --sage on --paper 6.3 and on --slate-deep 6.4.. The badge's
     plate and --slate-deep are the same value to the eye (1.03:1) — on a
     solid header the disc does not read as a disc, the ring is the edge,
     and that is the intent: the badge sits IN its header. */

  /* GROUNDS — warm, and three of them. `--paper` keeps its name because 6,800
     lines reference it; what it means is "the page". */
  --paper:       #161A1D;
  --paper-2:     #1D2226;
  --paper-3:     #262D33;   /* the mid-tone: cards, rails, raised blocks */
  --white:       #191E21;   /* form fields and the estimate card */

  /* INK IS LIGHT, AND WARM. */
  --ink:         #FFFFFF;
  --stone:       #AFA48F;
  --faint:       #9C9080;
  --bone:        #FFFFFF;
  --bone-mute:   rgba(255, 255, 255, 0.72);
  --bone-line:   rgba(255, 255, 255, 0.18);

  /* THE GREEN, AND A SECOND ONE UNDER IT.

     RE-MEASURED 2026-09-16, because every number that used to be in this
     block was wrong and one of them was dangerous. It claimed "--volt on
     --paper 15.5", and a later pass read that, believed it, and wrote a
     full-width green scroll rail across the top of the hero photograph. The
     real figure is 2.94:1. Measured, not claimed:

       --volt  on --paper .....  2.94   FILL ONLY. Never text, never a rule.
       --bone  on --volt ......  5.96   button labels — this is the pair that
                                        makes the green legal, and it is why
                                        labels are bone rather than --paper
                                        (--paper on --volt is the same 2.94).
       --volt  on --paper-3 ....  2.34   FILL ONLY on the mid-tone too.
       --mark  on --paper .....  7.32   the steel. THIS is the text colour for
                                        every "green" word, link, eyebrow and
                                        icon on the page.
       --ink   on --paper ..... 17.51   body and headings
       --stone on --paper .....  7.11   secondary copy

     The rule the numbers encode, stated once so it cannot be misread again:
     AN ACTION IS GREEN, A MARK IS STEEL. --volt fills a shape; --mark colours
     a glyph. Green type on this ground fails AA by more than half. */
  --head:        #0E1113;   /* THE HEADER, IN EVERY STATE: the same steel as the page at 6.5%
                               lightness — near-black. It was painting --paper when stuck, which on
                               the steel page is the lightest tone on the site; a header is never
                               lighter than what it floats over. */
  --mark:        #9EA9B3;   /* STEEL: every green WORD, link, eyebrow, star, border and icon.
                               The badge's ring. Fills stay --volt: an action is green,
                               a mark is steel. 6.3:1 on --paper, 4.6:1 on --paper-3. */
  --mark-hi:     #BAC3C9;   /* its hover: one stop up, 9.9:1 on --paper */
  --volt:        #33703C;   /* HOVER: every fill goes to --clay-hi with a bone label — audited, not assumed;
                               three chat controls were going bone-on-bone at 1.00:1.
                               EVERY PRESSABLE THING IS THE BADGE'S GREEN — pills, filters, the call
                               bar, the chat send and FAB, the consent button — with bone labels.
                               Steel was tried for these and read as grey; grey looks dead. */
  --volt-hi:     #26552E;   /* their hover: one stop up, 9.8:1 */
  --sage:        #8A9AA0;   /* the quiet green: planting, rules, tints */
  --clay:        #33703C;   /* PRIMARY BUTTONS ARE THE BADGE: the stand's top green as the fill,
                               the ring's steel as the edge, the letters' bone as the label. Bone on
                               it is 7.8:1; the steel edge is what makes a dark-green button read
                               on a dark-green page (the fill alone is 2.1:1 against the ground). */
  --clay-hi:     #26552E;   /* the primary's hover fill: a half-step up the stand; bone on it 4.7:1 (the full step, #3F7A46, measured 4.4 and missed AA) */
  --clay-soft:   #8A9AA0;   /* steel: the eyebrow dashes, topbar accent, badges */

  /* Bands still go darker than the page — depth, not a floating panel. */
  --slate:       #0D2318;   /* THE BANDS ARE PEAT: inverted sections carry the badge's green */
  --slate-deep:  #091A12;   /* header and footer: the disc sits in its own material */

  --line:        rgba(255, 255, 255, 0.13);
  --line-2:      rgba(255, 255, 255, 0.24);
  --line-warm:   rgba(138, 154, 160, 0.30);

  /* GEOMETRY — square, and wider than the reference's 1140px, because the
     pictures are the argument and they need the room. */
  --r-sm: 0px;
  --r:    0px;
  --r-lg: 0px;
  --max:  1280px;
  --nav-h: 216px;

  --display: 'Archivo', 'Helvetica Neue', Arial, sans-serif;
  --body:    'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

/* Shadows are invisible on a dark ground and were only ever separating light
   cards from a light page. Depth comes from the value of the ground now. */
:root { --sh-sm: none; --sh: none; --sh-lg: none; }

/* ---- Type -----------------------------------------------------------------
   THE AIRY SETTING WAS THE REFERENCE'S BEST TRICK AND IT GOES WITH THE REST.
   0.02em letter-spacing with word-spacing at 0.14em — roughly three times
   normal — is most of why their page reads as calm, and calm is not the
   claim. Word-spacing back to normal, tracking to zero, leading tightened
   from 1.62 to 1.5: the copy is a third shorter now, and short copy set
   loosely reads as thin rather than as confident.

   ARCHIVO CARRIES THE WEIGHT THAT ABORETO COULD NOT. It is a variable face
   from 400 to 900 that was already in the build and already preloaded on
   every page while Aboreto rendered over the top of it — so the heavy
   headline costs nothing that was not already being paid for. Hierarchy
   moves from size-and-space to weight-and-size, which is what lets the
   sections lose a third of their words without going flat.

   THE HEADLINE SCALE GOES UP HARD, from a 56px ceiling to 102px. There are
   six words in it now. */
body {
  font-family: var(--body);
  letter-spacing: 0;
  word-spacing: normal;
  line-height: 1.5;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--display);
  font-weight: 800;
  letter-spacing: -0.03em;
}

h1 { font-size: clamp(2.9rem, 7.6vw, 6.4rem);  line-height: 0.94; font-weight: 900; letter-spacing: -0.045em; }
h2 { font-size: clamp(2rem, 4.8vw, 3.85rem);   line-height: 1.0;  font-weight: 900; letter-spacing: -0.04em; }
h3 { font-size: clamp(1.15rem, 1.9vw, 1.5rem); line-height: 1.24; font-weight: 700; letter-spacing: -0.015em; }

/* The eyebrow is the one place tracking is still wide — it is a label, not
   a sentence, and it is the only thing on the page wearing the rust. */
.eyebrow {
  font-family: var(--body);
  letter-spacing: 0.2em;
  word-spacing: normal;
  font-size: 0.68rem;
  font-weight: 600;
  color: var(--mark);
}

/* Body copy sets narrower than the container so the eye is never asked to
   track 1280px of prose — the pictures get the width, the words do not. */
p { max-width: 62ch; }
.text-center p, .hero__lede, .cta-band p { max-width: none; }

/* ---- Buttons: the reference's shape, with contrast that works ------------
   THE LABEL IS THE GROUND, NOT THE INK. `--ink` is light on this palette, and
   light type on the volt fill measures 1.39:1 — the single worst thing the
   inversion broke, and invisible in a diff because the token names did not
   change. `--paper` on the same fill is 11.78:1. */
/* THIS IS THE RULE THAT DECIDES WHAT A PRIMARY BUTTON IS. At (0,4,0) it
   outranks every band-specific rule above it, which is how three passes of
   retuning the buttons elsewhere kept producing a steel button with a dark
   label in the hero. Badge construction, once, here: green fill, steel edge,
   bone label. */
.btn:not(.btn--ghost):not(.btn--clay):not(.btn--rust) {
  --btn-bg: var(--clay);
  --btn-fg: var(--bone);
  --btn-bd: var(--clay);
}
.btn:not(.btn--ghost):not(.btn--clay):not(.btn--rust):hover {
  --btn-bg: var(--clay-hi);
  --btn-bd: var(--mark-hi);
}
.btn {
  border-radius: 0;
  font-family: var(--body);
  font-weight: 600;
  letter-spacing: 0.11em;
  word-spacing: normal;
  text-transform: uppercase;
  padding: 1.35em 2.6em;
  background-image: none;
  box-shadow: none;
}
.btn--lg { padding: 1.55em 2.8em; }
.on-dark .btn:not(.btn--ghost), .hero--photo .btn:not(.btn--ghost),
.cta-band .btn:not(.btn--ghost), .site-foot .btn:not(.btn--ghost),
.dark-band .btn:not(.btn--ghost), .drawer .btn:not(.btn--ghost) {
  /* Buttons on the dark bands are the badge too: green fill, steel edge, bone
     label. This rule used to hand them the utility steel with a dark label,
     which is how the hero's primary call-to-action came out grey. */
  --btn-bg: var(--clay); --btn-bd: var(--clay); --btn-fg: var(--bone);
}
.filter, .areas li, .tag { border-radius: 0; }
.bg-ink2, .bg-surface { background: var(--paper-2); }

/* ---- A BIG HEADER --------------------------------------------------------
   The old nav was 82px with a 58px mark. A contractor's masthead should carry
   the name at a size you can read from the back of a truck. */
.site-head { padding-block: 0.85rem; }
.site-head .nav { min-height: var(--nav-h); }
.brand { gap: 1rem; }
.brand__name {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.62rem;
  letter-spacing: 0.06em;
}
.brand__sub {
  font-family: var(--body);
  font-size: 0.58rem;
  letter-spacing: 0.34em;
  word-spacing: normal;
}
.nav__links a {
  font-family: var(--body);
  font-size: 0.74rem;
  letter-spacing: 0.2em;
  word-spacing: normal;
  text-transform: uppercase;
}
@media (max-width: 699.98px) {
  :root { --nav-h: 76px; }
  .brand__name { font-size: 1.25rem; }
}

/* ---- The mark ------------------------------------------------------------
   THE ROUNDEL IS THE LOGO. Ring, two tree clusters, the modular DZT and
   "LANDSCAPING" — so it carries the name itself and the .brand link holds
   nothing else: setting the name in type beside it would say it twice, in two
   typefaces. Header and footer both take the full lockup; only the favicon and
   the home-screen icons drop the wordmark, because under about 56px it is a
   smudge and a smudge under the letters is the first thing the eye reads.
   Which file is which is in tools/build-logo-roundel.py.

   IT BRINGS ITS OWN GROUND. Every seam in the mark — between tiles, between
   tree tiers, around each rivet — is painted in the plate colour, so the
   artwork only holds together on its own plate. The file carries a disc of
   it trimmed just outside the ring, which is why one file works on the dark
   nav, on paper and over a photograph with no second version and no rim.

   SIZING LIVES IN §"A MASTHEAD, NOT A NAV BAR" AT THE END OF THIS FILE, not
   here. This block held a second set of widths (62/64/46px) that the later one
   overrode on every page — two places claiming to decide one number is how a
   stylesheet stops being readable, so only the layout-independent properties
   stay here. */
.brand__logo { display: block; flex: none; }

/* ---- WOVEN, NOT STACKED --------------------------------------------------
   THE SEAM IS A TREE LINE. Each dark band carries a silhouette that sits above
   its own top edge, so the band appears to rise into the section before it
   rather than start at a ruled boundary. `::before` on these three elements is
   already spent on the depth gradient and `::after` on a 1px hairline — the
   hairline is what this replaces, which is why no markup changed.

   The tile is seamless: the path starts and ends on the baseline, so repeat-x
   shows no join at any width. Two tiles because there are two dark values, and
   a silhouette has to be the exact colour of the thing it grows out of. */
/* THE SEAM IS A TREE LINE, and it sits INSIDE the top of each dark band.

   The first version hung the silhouette above the band and gave the previous
   section 32px of bottom padding to stand in. That worked visually and broke
   structurally: the proof bar's column dividers are full-height, so the extra
   padding ran four vertical rules straight down through the trees.

   Inverting it fixes the class of problem rather than that one instance. The
   tile paints the data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%20300%2034%27%20preserveAspectRatio%3D%27none%27%3E%3Cpath%20d%3D%27M0%200%20L0%2026%20L16.0%2026%20L21.0%2018.5%20L18.9%2018.5%20L24.0%209%20L29.1%2018.5%20L27.0%2018.5%20L32.0%2026%20L46.0%2026%20L49.8%2020.7%20L48.2%2020.7%20L52.0%2014%20L55.8%2020.7%20L54.2%2020.7%20L58.0%2026%20L74.5%2026%20L80.5%2016.8%20L77.9%2016.8%20L84.0%205%20L90.1%2016.8%20L87.5%2016.8%20L93.5%2026%20L105.0%2026%20L109.4%2019.8%20L107.5%2019.8%20L112.0%2012%20L116.5%2019.8%20L114.6%2019.8%20L119.0%2026%20L131.5%2026%20L136.8%2018.1%20L134.6%2018.1%20L140.0%208%20L145.4%2018.1%20L143.2%2018.1%20L148.5%2026%20L164.0%2026%20L167.8%2021.2%20L166.2%2021.2%20L170.0%2015%20L173.8%2021.2%20L172.2%2021.2%20L176.0%2026%20L189.0%2026%20L194.7%2017.6%20L192.2%2017.6%20L198.0%207%20L203.8%2017.6%20L201.3%2017.6%20L207.0%2026%20L221.5%2026%20L225.6%2020.3%20L223.8%2020.3%20L228.0%2013%20L232.2%2020.3%20L230.4%2020.3%20L234.5%2026%20L250.5%2026%20L255.2%2019.0%20L253.2%2019.0%20L258.0%2010%20L262.8%2019.0%20L260.8%2019.0%20L265.5%2026%20L278.5%2026%20L282.0%2021.2%20L280.5%2021.2%20L284.0%2015%20L287.5%2021.2%20L286.0%2021.2%20L289.5%2026%20L300%2026%20L300%200%20Z%27%20fill%3D%27%23F2EFE9%27%2F%3E%3C%2Fsvg%3E — the light ground above the trees — and leaves the
   conifers transparent, so the band's own dark colour shows through them. The
   strip is inside the band, overhangs nothing, and needs no cooperation from
   whatever section happens to come before it. The same conifer is in the logo,
   so the shape that ties the page together is the shape the business signs
   with. */
/* .site-foot joins the overflow list so its own seam is clipped to the band,
   exactly like the other two. */
.statement, .cta-band, .site-foot { overflow: hidden; }

.statement::after, .cta-band::after, .site-foot::after {
  /* 2026-09-14: the seam is now a TOPOGRAPHIC CONTOUR (thin sage survey lines) —
     on-brand for a grading contractor and consistent with the header's contour
     motif. The old conifer tree-line was retired (generator: scratchpad/
     treeline.mjs) after reading as clunky. */
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 60px;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%201400%2060%27%20preserveAspectRatio%3D%27none%27%3E%3Cpath%20d%3D%27M-20.0%2044.8%20L3.3%2044.8%20L26.7%2045.0%20L50.0%2045.2%20L73.3%2045.5%20L96.7%2045.7%20L120.0%2045.9%20L143.3%2045.9%20L166.7%2045.9%20L190.0%2045.9%20L213.3%2045.8%20L236.7%2045.7%20L260.0%2045.7%20L283.3%2045.8%20L306.7%2045.9%20L330.0%2046.3%20L353.3%2046.7%20L376.7%2047.2%20L400.0%2047.8%20L423.3%2048.3%20L446.7%2048.9%20L470.0%2049.3%20L493.3%2049.5%20L516.7%2049.6%20L540.0%2049.5%20L563.3%2049.3%20L586.7%2049.0%20L610.0%2048.6%20L633.3%2048.2%20L656.7%2047.9%20L680.0%2047.8%20L703.3%2047.9%20L726.7%2048.3%20L750.0%2048.9%20L773.3%2049.8%20L796.7%2051.0%20L820.0%2052.2%20L843.3%2053.6%20L866.7%2054.9%20L890.0%2056.2%20L913.3%2057.2%20L936.7%2058.0%20L960.0%2058.6%20L983.3%2058.8%20L1006.7%2058.8%20L1030.0%2058.4%20L1053.3%2057.9%20L1076.7%2057.2%20L1100.0%2056.3%20L1123.3%2055.5%20L1146.7%2054.6%20L1170.0%2053.8%20L1193.3%2053.0%20L1216.7%2052.3%20L1240.0%2051.7%20L1263.3%2051.0%20L1286.7%2050.4%20L1310.0%2049.6%20L1333.3%2048.9%20L1356.7%2048.0%20L1380.0%2047.1%20L1403.3%2046.1%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.50%27%20stroke-width%3D%271.4%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2037.1%20L3.3%2037.1%20L26.7%2037.3%20L50.0%2037.5%20L73.3%2037.7%20L96.7%2038.0%20L120.0%2038.1%20L143.3%2038.2%20L166.7%2038.2%20L190.0%2038.1%20L213.3%2038.0%20L236.7%2038.0%20L260.0%2037.9%20L283.3%2038.0%20L306.7%2038.2%20L330.0%2038.5%20L353.3%2038.9%20L376.7%2039.4%20L400.0%2039.9%20L423.3%2040.4%20L446.7%2040.9%20L470.0%2041.3%20L493.3%2041.5%20L516.7%2041.6%20L540.0%2041.6%20L563.3%2041.3%20L586.7%2041.0%20L610.0%2040.7%20L633.3%2040.3%20L656.7%2040.1%20L680.0%2040.0%20L703.3%2040.1%20L726.7%2040.4%20L750.0%2041.0%20L773.3%2041.9%20L796.7%2042.9%20L820.0%2044.1%20L843.3%2045.4%20L866.7%2046.6%20L890.0%2047.8%20L913.3%2048.8%20L936.7%2049.6%20L960.0%2050.1%20L983.3%2050.3%20L1006.7%2050.2%20L1030.0%2049.9%20L1053.3%2049.4%20L1076.7%2048.7%20L1100.0%2047.9%20L1123.3%2047.1%20L1146.7%2046.3%20L1170.0%2045.6%20L1193.3%2044.8%20L1216.7%2044.2%20L1240.0%2043.6%20L1263.3%2043.0%20L1286.7%2042.3%20L1310.0%2041.7%20L1333.3%2040.9%20L1356.7%2040.1%20L1380.0%2039.2%20L1403.3%2038.3%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.43%27%20stroke-width%3D%271%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2029.4%20L3.3%2029.5%20L26.7%2029.6%20L50.0%2029.8%20L73.3%2030.0%20L96.7%2030.2%20L120.0%2030.3%20L143.3%2030.4%20L166.7%2030.4%20L190.0%2030.4%20L213.3%2030.3%20L236.7%2030.2%20L260.0%2030.2%20L283.3%2030.3%20L306.7%2030.4%20L330.0%2030.7%20L353.3%2031.1%20L376.7%2031.5%20L400.0%2032.0%20L423.3%2032.5%20L446.7%2033.0%20L470.0%2033.4%20L493.3%2033.6%20L516.7%2033.7%20L540.0%2033.6%20L563.3%2033.4%20L586.7%2033.1%20L610.0%2032.7%20L633.3%2032.4%20L656.7%2032.2%20L680.0%2032.1%20L703.3%2032.2%20L726.7%2032.5%20L750.0%2033.1%20L773.3%2033.9%20L796.7%2034.8%20L820.0%2036.0%20L843.3%2037.1%20L866.7%2038.3%20L890.0%2039.4%20L913.3%2040.4%20L936.7%2041.1%20L960.0%2041.6%20L983.3%2041.8%20L1006.7%2041.7%20L1030.0%2041.4%20L1053.3%2040.9%20L1076.7%2040.3%20L1100.0%2039.6%20L1123.3%2038.8%20L1146.7%2038.0%20L1170.0%2037.3%20L1193.3%2036.7%20L1216.7%2036.0%20L1240.0%2035.5%20L1263.3%2034.9%20L1286.7%2034.3%20L1310.0%2033.7%20L1333.3%2033.0%20L1356.7%2032.2%20L1380.0%2031.4%20L1403.3%2030.6%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.36%27%20stroke-width%3D%271.4%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2021.8%20L3.3%2021.8%20L26.7%2021.9%20L50.0%2022.1%20L73.3%2022.3%20L96.7%2022.5%20L120.0%2022.6%20L143.3%2022.7%20L166.7%2022.7%20L190.0%2022.6%20L213.3%2022.5%20L236.7%2022.5%20L260.0%2022.5%20L283.3%2022.5%20L306.7%2022.7%20L330.0%2022.9%20L353.3%2023.3%20L376.7%2023.7%20L400.0%2024.2%20L423.3%2024.6%20L446.7%2025.1%20L470.0%2025.4%20L493.3%2025.6%20L516.7%2025.7%20L540.0%2025.6%20L563.3%2025.4%20L586.7%2025.2%20L610.0%2024.8%20L633.3%2024.5%20L656.7%2024.3%20L680.0%2024.2%20L703.3%2024.3%20L726.7%2024.6%20L750.0%2025.1%20L773.3%2025.9%20L796.7%2026.8%20L820.0%2027.8%20L843.3%2028.9%20L866.7%2030.0%20L890.0%2031.0%20L913.3%2031.9%20L936.7%2032.6%20L960.0%2033.0%20L983.3%2033.2%20L1006.7%2033.2%20L1030.0%2032.9%20L1053.3%2032.5%20L1076.7%2031.9%20L1100.0%2031.2%20L1123.3%2030.5%20L1146.7%2029.8%20L1170.0%2029.1%20L1193.3%2028.5%20L1216.7%2027.9%20L1240.0%2027.4%20L1263.3%2026.8%20L1286.7%2026.3%20L1310.0%2025.7%20L1333.3%2025.1%20L1356.7%2024.4%20L1380.0%2023.6%20L1403.3%2022.8%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.30%27%20stroke-width%3D%271%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2014.1%20L3.3%2014.1%20L26.7%2014.2%20L50.0%2014.4%20L73.3%2014.6%20L96.7%2014.7%20L120.0%2014.8%20L143.3%2014.9%20L166.7%2014.9%20L190.0%2014.9%20L213.3%2014.8%20L236.7%2014.7%20L260.0%2014.7%20L283.3%2014.8%20L306.7%2014.9%20L330.0%2015.2%20L353.3%2015.5%20L376.7%2015.9%20L400.0%2016.3%20L423.3%2016.7%20L446.7%2017.1%20L470.0%2017.4%20L493.3%2017.6%20L516.7%2017.7%20L540.0%2017.6%20L563.3%2017.5%20L586.7%2017.2%20L610.0%2016.9%20L633.3%2016.6%20L656.7%2016.4%20L680.0%2016.4%20L703.3%2016.4%20L726.7%2016.7%20L750.0%2017.2%20L773.3%2017.9%20L796.7%2018.7%20L820.0%2019.7%20L843.3%2020.7%20L866.7%2021.7%20L890.0%2022.7%20L913.3%2023.5%20L936.7%2024.1%20L960.0%2024.5%20L983.3%2024.7%20L1006.7%2024.7%20L1030.0%2024.4%20L1053.3%2024.0%20L1076.7%2023.4%20L1100.0%2022.8%20L1123.3%2022.2%20L1146.7%2021.5%20L1170.0%2020.9%20L1193.3%2020.3%20L1216.7%2019.8%20L1240.0%2019.3%20L1263.3%2018.8%20L1286.7%2018.3%20L1310.0%2017.7%20L1333.3%2017.1%20L1356.7%2016.5%20L1380.0%2015.8%20L1403.3%2015.0%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.23%27%20stroke-width%3D%271.4%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%206.4%20L3.3%206.4%20L26.7%206.5%20L50.0%206.7%20L73.3%206.8%20L96.7%207.0%20L120.0%207.1%20L143.3%207.1%20L166.7%207.1%20L190.0%207.1%20L213.3%207.0%20L236.7%207.0%20L260.0%207.0%20L283.3%207.0%20L306.7%207.2%20L330.0%207.4%20L353.3%207.7%20L376.7%208.0%20L400.0%208.4%20L423.3%208.8%20L446.7%209.2%20L470.0%209.5%20L493.3%209.7%20L516.7%209.7%20L540.0%209.7%20L563.3%209.5%20L586.7%209.3%20L610.0%209.0%20L633.3%208.7%20L656.7%208.6%20L680.0%208.5%20L703.3%208.6%20L726.7%208.8%20L750.0%209.3%20L773.3%209.9%20L796.7%2010.7%20L820.0%2011.6%20L843.3%2012.5%20L866.7%2013.4%20L890.0%2014.3%20L913.3%2015.1%20L936.7%2015.6%20L960.0%2016.0%20L983.3%2016.2%20L1006.7%2016.1%20L1030.0%2015.9%20L1053.3%2015.5%20L1076.7%2015.0%20L1100.0%2014.4%20L1123.3%2013.8%20L1146.7%2013.2%20L1170.0%2012.6%20L1193.3%2012.1%20L1216.7%2011.6%20L1240.0%2011.2%20L1263.3%2010.7%20L1286.7%2010.2%20L1310.0%209.8%20L1333.3%209.2%20L1356.7%208.6%20L1380.0%208.0%20L1403.3%207.3%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.16%27%20stroke-width%3D%271%27%20stroke-linecap%3D%27round%27%2F%3E%3C%2Fsvg%3E");
  background-repeat: no-repeat;
  background-position: left top;
  background-size: 100% 60px;
  border: 0;
  z-index: 2;
  pointer-events: none;
}

/* One drawing per width band, so the conifers render near 1:1 and keep their
   shape instead of being squashed thin on a phone. */
@media (max-width: 1024px) {
  .statement::after, .cta-band::after, .site-foot::after {
    height: 50px;
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%201400%2060%27%20preserveAspectRatio%3D%27none%27%3E%3Cpath%20d%3D%27M-20.0%2044.8%20L3.3%2044.8%20L26.7%2045.0%20L50.0%2045.2%20L73.3%2045.5%20L96.7%2045.7%20L120.0%2045.9%20L143.3%2045.9%20L166.7%2045.9%20L190.0%2045.9%20L213.3%2045.8%20L236.7%2045.7%20L260.0%2045.7%20L283.3%2045.8%20L306.7%2045.9%20L330.0%2046.3%20L353.3%2046.7%20L376.7%2047.2%20L400.0%2047.8%20L423.3%2048.3%20L446.7%2048.9%20L470.0%2049.3%20L493.3%2049.5%20L516.7%2049.6%20L540.0%2049.5%20L563.3%2049.3%20L586.7%2049.0%20L610.0%2048.6%20L633.3%2048.2%20L656.7%2047.9%20L680.0%2047.8%20L703.3%2047.9%20L726.7%2048.3%20L750.0%2048.9%20L773.3%2049.8%20L796.7%2051.0%20L820.0%2052.2%20L843.3%2053.6%20L866.7%2054.9%20L890.0%2056.2%20L913.3%2057.2%20L936.7%2058.0%20L960.0%2058.6%20L983.3%2058.8%20L1006.7%2058.8%20L1030.0%2058.4%20L1053.3%2057.9%20L1076.7%2057.2%20L1100.0%2056.3%20L1123.3%2055.5%20L1146.7%2054.6%20L1170.0%2053.8%20L1193.3%2053.0%20L1216.7%2052.3%20L1240.0%2051.7%20L1263.3%2051.0%20L1286.7%2050.4%20L1310.0%2049.6%20L1333.3%2048.9%20L1356.7%2048.0%20L1380.0%2047.1%20L1403.3%2046.1%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.50%27%20stroke-width%3D%271.4%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2037.1%20L3.3%2037.1%20L26.7%2037.3%20L50.0%2037.5%20L73.3%2037.7%20L96.7%2038.0%20L120.0%2038.1%20L143.3%2038.2%20L166.7%2038.2%20L190.0%2038.1%20L213.3%2038.0%20L236.7%2038.0%20L260.0%2037.9%20L283.3%2038.0%20L306.7%2038.2%20L330.0%2038.5%20L353.3%2038.9%20L376.7%2039.4%20L400.0%2039.9%20L423.3%2040.4%20L446.7%2040.9%20L470.0%2041.3%20L493.3%2041.5%20L516.7%2041.6%20L540.0%2041.6%20L563.3%2041.3%20L586.7%2041.0%20L610.0%2040.7%20L633.3%2040.3%20L656.7%2040.1%20L680.0%2040.0%20L703.3%2040.1%20L726.7%2040.4%20L750.0%2041.0%20L773.3%2041.9%20L796.7%2042.9%20L820.0%2044.1%20L843.3%2045.4%20L866.7%2046.6%20L890.0%2047.8%20L913.3%2048.8%20L936.7%2049.6%20L960.0%2050.1%20L983.3%2050.3%20L1006.7%2050.2%20L1030.0%2049.9%20L1053.3%2049.4%20L1076.7%2048.7%20L1100.0%2047.9%20L1123.3%2047.1%20L1146.7%2046.3%20L1170.0%2045.6%20L1193.3%2044.8%20L1216.7%2044.2%20L1240.0%2043.6%20L1263.3%2043.0%20L1286.7%2042.3%20L1310.0%2041.7%20L1333.3%2040.9%20L1356.7%2040.1%20L1380.0%2039.2%20L1403.3%2038.3%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.43%27%20stroke-width%3D%271%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2029.4%20L3.3%2029.5%20L26.7%2029.6%20L50.0%2029.8%20L73.3%2030.0%20L96.7%2030.2%20L120.0%2030.3%20L143.3%2030.4%20L166.7%2030.4%20L190.0%2030.4%20L213.3%2030.3%20L236.7%2030.2%20L260.0%2030.2%20L283.3%2030.3%20L306.7%2030.4%20L330.0%2030.7%20L353.3%2031.1%20L376.7%2031.5%20L400.0%2032.0%20L423.3%2032.5%20L446.7%2033.0%20L470.0%2033.4%20L493.3%2033.6%20L516.7%2033.7%20L540.0%2033.6%20L563.3%2033.4%20L586.7%2033.1%20L610.0%2032.7%20L633.3%2032.4%20L656.7%2032.2%20L680.0%2032.1%20L703.3%2032.2%20L726.7%2032.5%20L750.0%2033.1%20L773.3%2033.9%20L796.7%2034.8%20L820.0%2036.0%20L843.3%2037.1%20L866.7%2038.3%20L890.0%2039.4%20L913.3%2040.4%20L936.7%2041.1%20L960.0%2041.6%20L983.3%2041.8%20L1006.7%2041.7%20L1030.0%2041.4%20L1053.3%2040.9%20L1076.7%2040.3%20L1100.0%2039.6%20L1123.3%2038.8%20L1146.7%2038.0%20L1170.0%2037.3%20L1193.3%2036.7%20L1216.7%2036.0%20L1240.0%2035.5%20L1263.3%2034.9%20L1286.7%2034.3%20L1310.0%2033.7%20L1333.3%2033.0%20L1356.7%2032.2%20L1380.0%2031.4%20L1403.3%2030.6%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.36%27%20stroke-width%3D%271.4%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2021.8%20L3.3%2021.8%20L26.7%2021.9%20L50.0%2022.1%20L73.3%2022.3%20L96.7%2022.5%20L120.0%2022.6%20L143.3%2022.7%20L166.7%2022.7%20L190.0%2022.6%20L213.3%2022.5%20L236.7%2022.5%20L260.0%2022.5%20L283.3%2022.5%20L306.7%2022.7%20L330.0%2022.9%20L353.3%2023.3%20L376.7%2023.7%20L400.0%2024.2%20L423.3%2024.6%20L446.7%2025.1%20L470.0%2025.4%20L493.3%2025.6%20L516.7%2025.7%20L540.0%2025.6%20L563.3%2025.4%20L586.7%2025.2%20L610.0%2024.8%20L633.3%2024.5%20L656.7%2024.3%20L680.0%2024.2%20L703.3%2024.3%20L726.7%2024.6%20L750.0%2025.1%20L773.3%2025.9%20L796.7%2026.8%20L820.0%2027.8%20L843.3%2028.9%20L866.7%2030.0%20L890.0%2031.0%20L913.3%2031.9%20L936.7%2032.6%20L960.0%2033.0%20L983.3%2033.2%20L1006.7%2033.2%20L1030.0%2032.9%20L1053.3%2032.5%20L1076.7%2031.9%20L1100.0%2031.2%20L1123.3%2030.5%20L1146.7%2029.8%20L1170.0%2029.1%20L1193.3%2028.5%20L1216.7%2027.9%20L1240.0%2027.4%20L1263.3%2026.8%20L1286.7%2026.3%20L1310.0%2025.7%20L1333.3%2025.1%20L1356.7%2024.4%20L1380.0%2023.6%20L1403.3%2022.8%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.30%27%20stroke-width%3D%271%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2014.1%20L3.3%2014.1%20L26.7%2014.2%20L50.0%2014.4%20L73.3%2014.6%20L96.7%2014.7%20L120.0%2014.8%20L143.3%2014.9%20L166.7%2014.9%20L190.0%2014.9%20L213.3%2014.8%20L236.7%2014.7%20L260.0%2014.7%20L283.3%2014.8%20L306.7%2014.9%20L330.0%2015.2%20L353.3%2015.5%20L376.7%2015.9%20L400.0%2016.3%20L423.3%2016.7%20L446.7%2017.1%20L470.0%2017.4%20L493.3%2017.6%20L516.7%2017.7%20L540.0%2017.6%20L563.3%2017.5%20L586.7%2017.2%20L610.0%2016.9%20L633.3%2016.6%20L656.7%2016.4%20L680.0%2016.4%20L703.3%2016.4%20L726.7%2016.7%20L750.0%2017.2%20L773.3%2017.9%20L796.7%2018.7%20L820.0%2019.7%20L843.3%2020.7%20L866.7%2021.7%20L890.0%2022.7%20L913.3%2023.5%20L936.7%2024.1%20L960.0%2024.5%20L983.3%2024.7%20L1006.7%2024.7%20L1030.0%2024.4%20L1053.3%2024.0%20L1076.7%2023.4%20L1100.0%2022.8%20L1123.3%2022.2%20L1146.7%2021.5%20L1170.0%2020.9%20L1193.3%2020.3%20L1216.7%2019.8%20L1240.0%2019.3%20L1263.3%2018.8%20L1286.7%2018.3%20L1310.0%2017.7%20L1333.3%2017.1%20L1356.7%2016.5%20L1380.0%2015.8%20L1403.3%2015.0%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.23%27%20stroke-width%3D%271.4%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%206.4%20L3.3%206.4%20L26.7%206.5%20L50.0%206.7%20L73.3%206.8%20L96.7%207.0%20L120.0%207.1%20L143.3%207.1%20L166.7%207.1%20L190.0%207.1%20L213.3%207.0%20L236.7%207.0%20L260.0%207.0%20L283.3%207.0%20L306.7%207.2%20L330.0%207.4%20L353.3%207.7%20L376.7%208.0%20L400.0%208.4%20L423.3%208.8%20L446.7%209.2%20L470.0%209.5%20L493.3%209.7%20L516.7%209.7%20L540.0%209.7%20L563.3%209.5%20L586.7%209.3%20L610.0%209.0%20L633.3%208.7%20L656.7%208.6%20L680.0%208.5%20L703.3%208.6%20L726.7%208.8%20L750.0%209.3%20L773.3%209.9%20L796.7%2010.7%20L820.0%2011.6%20L843.3%2012.5%20L866.7%2013.4%20L890.0%2014.3%20L913.3%2015.1%20L936.7%2015.6%20L960.0%2016.0%20L983.3%2016.2%20L1006.7%2016.1%20L1030.0%2015.9%20L1053.3%2015.5%20L1076.7%2015.0%20L1100.0%2014.4%20L1123.3%2013.8%20L1146.7%2013.2%20L1170.0%2012.6%20L1193.3%2012.1%20L1216.7%2011.6%20L1240.0%2011.2%20L1263.3%2010.7%20L1286.7%2010.2%20L1310.0%209.8%20L1333.3%209.2%20L1356.7%208.6%20L1380.0%208.0%20L1403.3%207.3%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.16%27%20stroke-width%3D%271%27%20stroke-linecap%3D%27round%27%2F%3E%3C%2Fsvg%3E");
    background-size: 100% 50px;
  }
}
@media (max-width: 600px) {
  .statement::after, .cta-band::after, .site-foot::after {
    height: 42px;
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%201400%2060%27%20preserveAspectRatio%3D%27none%27%3E%3Cpath%20d%3D%27M-20.0%2044.8%20L3.3%2044.8%20L26.7%2045.0%20L50.0%2045.2%20L73.3%2045.5%20L96.7%2045.7%20L120.0%2045.9%20L143.3%2045.9%20L166.7%2045.9%20L190.0%2045.9%20L213.3%2045.8%20L236.7%2045.7%20L260.0%2045.7%20L283.3%2045.8%20L306.7%2045.9%20L330.0%2046.3%20L353.3%2046.7%20L376.7%2047.2%20L400.0%2047.8%20L423.3%2048.3%20L446.7%2048.9%20L470.0%2049.3%20L493.3%2049.5%20L516.7%2049.6%20L540.0%2049.5%20L563.3%2049.3%20L586.7%2049.0%20L610.0%2048.6%20L633.3%2048.2%20L656.7%2047.9%20L680.0%2047.8%20L703.3%2047.9%20L726.7%2048.3%20L750.0%2048.9%20L773.3%2049.8%20L796.7%2051.0%20L820.0%2052.2%20L843.3%2053.6%20L866.7%2054.9%20L890.0%2056.2%20L913.3%2057.2%20L936.7%2058.0%20L960.0%2058.6%20L983.3%2058.8%20L1006.7%2058.8%20L1030.0%2058.4%20L1053.3%2057.9%20L1076.7%2057.2%20L1100.0%2056.3%20L1123.3%2055.5%20L1146.7%2054.6%20L1170.0%2053.8%20L1193.3%2053.0%20L1216.7%2052.3%20L1240.0%2051.7%20L1263.3%2051.0%20L1286.7%2050.4%20L1310.0%2049.6%20L1333.3%2048.9%20L1356.7%2048.0%20L1380.0%2047.1%20L1403.3%2046.1%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.50%27%20stroke-width%3D%271.4%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2037.1%20L3.3%2037.1%20L26.7%2037.3%20L50.0%2037.5%20L73.3%2037.7%20L96.7%2038.0%20L120.0%2038.1%20L143.3%2038.2%20L166.7%2038.2%20L190.0%2038.1%20L213.3%2038.0%20L236.7%2038.0%20L260.0%2037.9%20L283.3%2038.0%20L306.7%2038.2%20L330.0%2038.5%20L353.3%2038.9%20L376.7%2039.4%20L400.0%2039.9%20L423.3%2040.4%20L446.7%2040.9%20L470.0%2041.3%20L493.3%2041.5%20L516.7%2041.6%20L540.0%2041.6%20L563.3%2041.3%20L586.7%2041.0%20L610.0%2040.7%20L633.3%2040.3%20L656.7%2040.1%20L680.0%2040.0%20L703.3%2040.1%20L726.7%2040.4%20L750.0%2041.0%20L773.3%2041.9%20L796.7%2042.9%20L820.0%2044.1%20L843.3%2045.4%20L866.7%2046.6%20L890.0%2047.8%20L913.3%2048.8%20L936.7%2049.6%20L960.0%2050.1%20L983.3%2050.3%20L1006.7%2050.2%20L1030.0%2049.9%20L1053.3%2049.4%20L1076.7%2048.7%20L1100.0%2047.9%20L1123.3%2047.1%20L1146.7%2046.3%20L1170.0%2045.6%20L1193.3%2044.8%20L1216.7%2044.2%20L1240.0%2043.6%20L1263.3%2043.0%20L1286.7%2042.3%20L1310.0%2041.7%20L1333.3%2040.9%20L1356.7%2040.1%20L1380.0%2039.2%20L1403.3%2038.3%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.43%27%20stroke-width%3D%271%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2029.4%20L3.3%2029.5%20L26.7%2029.6%20L50.0%2029.8%20L73.3%2030.0%20L96.7%2030.2%20L120.0%2030.3%20L143.3%2030.4%20L166.7%2030.4%20L190.0%2030.4%20L213.3%2030.3%20L236.7%2030.2%20L260.0%2030.2%20L283.3%2030.3%20L306.7%2030.4%20L330.0%2030.7%20L353.3%2031.1%20L376.7%2031.5%20L400.0%2032.0%20L423.3%2032.5%20L446.7%2033.0%20L470.0%2033.4%20L493.3%2033.6%20L516.7%2033.7%20L540.0%2033.6%20L563.3%2033.4%20L586.7%2033.1%20L610.0%2032.7%20L633.3%2032.4%20L656.7%2032.2%20L680.0%2032.1%20L703.3%2032.2%20L726.7%2032.5%20L750.0%2033.1%20L773.3%2033.9%20L796.7%2034.8%20L820.0%2036.0%20L843.3%2037.1%20L866.7%2038.3%20L890.0%2039.4%20L913.3%2040.4%20L936.7%2041.1%20L960.0%2041.6%20L983.3%2041.8%20L1006.7%2041.7%20L1030.0%2041.4%20L1053.3%2040.9%20L1076.7%2040.3%20L1100.0%2039.6%20L1123.3%2038.8%20L1146.7%2038.0%20L1170.0%2037.3%20L1193.3%2036.7%20L1216.7%2036.0%20L1240.0%2035.5%20L1263.3%2034.9%20L1286.7%2034.3%20L1310.0%2033.7%20L1333.3%2033.0%20L1356.7%2032.2%20L1380.0%2031.4%20L1403.3%2030.6%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.36%27%20stroke-width%3D%271.4%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2021.8%20L3.3%2021.8%20L26.7%2021.9%20L50.0%2022.1%20L73.3%2022.3%20L96.7%2022.5%20L120.0%2022.6%20L143.3%2022.7%20L166.7%2022.7%20L190.0%2022.6%20L213.3%2022.5%20L236.7%2022.5%20L260.0%2022.5%20L283.3%2022.5%20L306.7%2022.7%20L330.0%2022.9%20L353.3%2023.3%20L376.7%2023.7%20L400.0%2024.2%20L423.3%2024.6%20L446.7%2025.1%20L470.0%2025.4%20L493.3%2025.6%20L516.7%2025.7%20L540.0%2025.6%20L563.3%2025.4%20L586.7%2025.2%20L610.0%2024.8%20L633.3%2024.5%20L656.7%2024.3%20L680.0%2024.2%20L703.3%2024.3%20L726.7%2024.6%20L750.0%2025.1%20L773.3%2025.9%20L796.7%2026.8%20L820.0%2027.8%20L843.3%2028.9%20L866.7%2030.0%20L890.0%2031.0%20L913.3%2031.9%20L936.7%2032.6%20L960.0%2033.0%20L983.3%2033.2%20L1006.7%2033.2%20L1030.0%2032.9%20L1053.3%2032.5%20L1076.7%2031.9%20L1100.0%2031.2%20L1123.3%2030.5%20L1146.7%2029.8%20L1170.0%2029.1%20L1193.3%2028.5%20L1216.7%2027.9%20L1240.0%2027.4%20L1263.3%2026.8%20L1286.7%2026.3%20L1310.0%2025.7%20L1333.3%2025.1%20L1356.7%2024.4%20L1380.0%2023.6%20L1403.3%2022.8%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.30%27%20stroke-width%3D%271%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%2014.1%20L3.3%2014.1%20L26.7%2014.2%20L50.0%2014.4%20L73.3%2014.6%20L96.7%2014.7%20L120.0%2014.8%20L143.3%2014.9%20L166.7%2014.9%20L190.0%2014.9%20L213.3%2014.8%20L236.7%2014.7%20L260.0%2014.7%20L283.3%2014.8%20L306.7%2014.9%20L330.0%2015.2%20L353.3%2015.5%20L376.7%2015.9%20L400.0%2016.3%20L423.3%2016.7%20L446.7%2017.1%20L470.0%2017.4%20L493.3%2017.6%20L516.7%2017.7%20L540.0%2017.6%20L563.3%2017.5%20L586.7%2017.2%20L610.0%2016.9%20L633.3%2016.6%20L656.7%2016.4%20L680.0%2016.4%20L703.3%2016.4%20L726.7%2016.7%20L750.0%2017.2%20L773.3%2017.9%20L796.7%2018.7%20L820.0%2019.7%20L843.3%2020.7%20L866.7%2021.7%20L890.0%2022.7%20L913.3%2023.5%20L936.7%2024.1%20L960.0%2024.5%20L983.3%2024.7%20L1006.7%2024.7%20L1030.0%2024.4%20L1053.3%2024.0%20L1076.7%2023.4%20L1100.0%2022.8%20L1123.3%2022.2%20L1146.7%2021.5%20L1170.0%2020.9%20L1193.3%2020.3%20L1216.7%2019.8%20L1240.0%2019.3%20L1263.3%2018.8%20L1286.7%2018.3%20L1310.0%2017.7%20L1333.3%2017.1%20L1356.7%2016.5%20L1380.0%2015.8%20L1403.3%2015.0%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.23%27%20stroke-width%3D%271.4%27%20stroke-linecap%3D%27round%27%2F%3E%3Cpath%20d%3D%27M-20.0%206.4%20L3.3%206.4%20L26.7%206.5%20L50.0%206.7%20L73.3%206.8%20L96.7%207.0%20L120.0%207.1%20L143.3%207.1%20L166.7%207.1%20L190.0%207.1%20L213.3%207.0%20L236.7%207.0%20L260.0%207.0%20L283.3%207.0%20L306.7%207.2%20L330.0%207.4%20L353.3%207.7%20L376.7%208.0%20L400.0%208.4%20L423.3%208.8%20L446.7%209.2%20L470.0%209.5%20L493.3%209.7%20L516.7%209.7%20L540.0%209.7%20L563.3%209.5%20L586.7%209.3%20L610.0%209.0%20L633.3%208.7%20L656.7%208.6%20L680.0%208.5%20L703.3%208.6%20L726.7%208.8%20L750.0%209.3%20L773.3%209.9%20L796.7%2010.7%20L820.0%2011.6%20L843.3%2012.5%20L866.7%2013.4%20L890.0%2014.3%20L913.3%2015.1%20L936.7%2015.6%20L960.0%2016.0%20L983.3%2016.2%20L1006.7%2016.1%20L1030.0%2015.9%20L1053.3%2015.5%20L1076.7%2015.0%20L1100.0%2014.4%20L1123.3%2013.8%20L1146.7%2013.2%20L1170.0%2012.6%20L1193.3%2012.1%20L1216.7%2011.6%20L1240.0%2011.2%20L1263.3%2010.7%20L1286.7%2010.2%20L1310.0%209.8%20L1333.3%209.2%20L1356.7%208.6%20L1380.0%208.0%20L1403.3%207.3%27%20fill%3D%27none%27%20stroke%3D%27%238AA093%27%20stroke-opacity%3D%270.16%27%20stroke-width%3D%271%27%20stroke-linecap%3D%27round%27%2F%3E%3C%2Fsvg%3E");
    background-size: 100% 42px;
  }
}

/* THE SPINE. One hairline the height of the page, with the sections hanging
   off it, so the eye reads a thread rather than a stack. Wide screens only:
   under 1100px there is no margin to put it in and it would crowd the text. */
@media (min-width: 1100px) {
  main { position: relative; }
  main::before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: max(2rem, calc((100% - var(--max)) / 2 - 2.6rem));
    width: 1px;
    background: linear-gradient(to bottom,
      transparent 0,
      var(--line) 4rem,
      var(--line) calc(100% - 4rem),
      transparent 100%);
    pointer-events: none;
    z-index: 0;
  }
  main > section { position: relative; z-index: 1; }
}

/* ---- A BIG FOOTER -------------------------------------------------------- */
.site-foot { padding-block: clamp(4.5rem, 9vw, 8rem) clamp(2.5rem, 4vw, 3.5rem); }
.site-foot .foot-grid { gap: clamp(2.5rem, 5vw, 4.5rem); }
.site-foot h4 {
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.02rem;
  letter-spacing: 0.06em;
  margin-bottom: 1.1rem;
}
.site-foot a { color: var(--bone-mute); }
.site-foot a:hover:not(.btn) { color: var(--mark); }
/* :not(.btn) because the footer CTA is an anchor too, and this blanket rule was
   painting the brand green onto its own bone fill at 3.33:1. The button keeps
   the dark label its fill was chosen for. */
.site-foot .btn:hover { color: var(--bone); }
.site-foot .foot-about p { max-width: 34ch; line-height: 1.7; }

/* ---- Bands ---------------------------------------------------------------- */
.statement h2, .cta-band h2 { color: var(--bone); }
.statement .eyebrow, .cta-band .eyebrow { color: var(--mark); }
.hero--photo h1 { text-shadow: 0 2px 34px rgba(12, 18, 22, 0.5); }

@media (prefers-reduced-motion: reduce) {
  .statement::after, .cta-band::after, .site-foot::after { transition: none; }
}

/* ---- The single offer ----------------------------------------------------
   One control where the hero previously ran two. "Get Your Free Estimate"
   against "What We Build" splits intent at the moment it is highest; the
   reference site runs one named offer down the whole page and nothing else,
   which is the strongest single thing on it. The note does the work the second
   button was doing — saying what happens next — without competing for the
   click. */
.hero__offer { display: flex; flex-direction: column; align-items: flex-start; gap: 1.1rem; }
.hero__offer .btn { align-self: flex-start; }

.hero__offer-note {
  margin: 0;
  max-width: 38ch;
  font-size: 0.94rem;
  line-height: 1.6;
  color: var(--bone);
  border-left: 2px solid var(--mark);
  padding-left: 1rem;
  text-shadow: 0 1px 16px rgba(12, 18, 22, 0.7);
}

/* ---- The objection block -------------------------------------------------
   Name what the customer is actually afraid of before listing what you sell.
   The fears are all about project management rather than landscaping, which is
   correct — nobody lies awake about plant selection. Taken wholesale from the
   reference, which is the best-argued part of their page.

   EVERY ANSWER ALREADY SHIPPED HERE before this block existed: the first two
   are the statement band almost verbatim, the last two name questions the FAQ
   answers. No licence, bond, insurance or warranty claim appears — config.js
   holds all four empty at the owner's direction. */
.objection { background: var(--paper-3); border-block: 1px solid var(--line); }

.objection__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  margin-top: 1.9rem;
}

.objection__item { background: var(--paper-2); padding: 1.7rem 1.5rem 1.8rem; position: relative; }
/* The hairline the scroll pass draws across the top of each item. Declared
   here, not in the animation block, so the rule exists on browsers with no
   scroll timeline too — they get it drawn, just not drawing. */
.objection__item::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;
  background: var(--volt);
}

.objection__fear {
  display: block;
  font-family: var(--body);
  font-size: 0.64rem;
  letter-spacing: 0.22em;
  word-spacing: normal;
  text-transform: uppercase;
  color: var(--mark);
  margin-bottom: 0.75rem;
}

.objection__item p { margin: 0; font-size: 0.98rem; line-height: 1.65; color: var(--ink); }
.objection__item b { font-weight: 400; color: var(--mark); }

/* ---- Capability cards became photographs ---------------------------------
   The grid was eight cards of icon + three sentences: 257 words of text where
   a customer is trying to work out whether this company builds the thing they
   want. Six cards now, each led by a photograph of that kind of work, and the
   copy cut to one line — the picture makes the claim and the line qualifies it.

   FULL-BLEED, so the card reads as a photograph with a caption rather than a
   box with a thumbnail in it. The card's padding is a clamp, so the negative
   margin has to be the same clamp rather than a guessed number, or the image
   sits inset by a few pixels at some widths and not others. */
.card__shot {
  --card-pad: clamp(1.35rem, 2.2vw, 1.65rem);
  display: block;
  margin: calc(-1 * var(--card-pad)) calc(-1 * var(--card-pad)) 0.15rem;
  overflow: hidden;
  background: var(--paper-3);
}

.card__shot img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: transform 0.5s var(--ease);
}

/* The whole card is the hover target, not the image, so the movement reads as
   one object responding rather than a picture squirming inside a frame. */
.card:hover .card__shot img { transform: scale(1.035); }

@media (prefers-reduced-motion: reduce) {
  .card__shot img, .card:hover .card__shot img { transition: none; transform: none; }
}


/* ==========================================================================
   THE PICTURES ARE THE ARGUMENT.

   A landscaping site is a portfolio wearing a brochure's clothes. The old
   layout ran 290px cards three-up with 4:3 crops — a 360x270 photograph of a
   built backyard, which is a thumbnail of the only evidence the business has.
   Meanwhile the paragraph beside it got 62 characters a line and four lines
   to say what the picture says instantly.

   THIS INVERTS THE RATIO EVERYWHERE, and it is a ratio rather than a set of
   one-off sizes: every picture roughly triples in area, every block of copy
   loses about a third of its words, and the space that frees up goes back
   into the pictures rather than into padding.

     Cards go two-up instead of three, at 3:2 rather than 4:3 — 620x413 in a
     1280 container, which is 3.4x the pixels and the first size at which a
     flagstone joint is legible.

     The hero takes the whole first screen. A photograph that ends at 84vh is
     a banner; one that ends at the fold is the page.

     `.band--photo` is new and is the thing the old design had no answer for:
     a full-bleed photograph between two sections, edge to edge, with no
     container and no card. Sections are separated by pictures now instead of
     by rules.
   ========================================================================== */

/* ---- The first screen is the photograph --------------------------------- */
.hero { min-height: min(100svh, 940px); }
@media (min-width: 901px) {
  .hero--photo { min-height: 100svh; }
}

/* ---- Cards: two up, and the crop gets the height ------------------------ */
@media (min-width: 860px) {
  .grid-3 { grid-template-columns: repeat(2, 1fr); }
  .grid-3 > .card:last-child:nth-child(odd) { grid-column: 1 / -1; }
  .grid-3 > .card:last-child:nth-child(odd) .card__shot img { aspect-ratio: 21 / 9; }
}
.grid { gap: clamp(1.25rem, 2.4vw, 2.25rem); }
.card__shot img { aspect-ratio: 3 / 2; }

/* THE CAPTION IS A LABEL, NOT A PARAGRAPH. Two lines maximum under a picture
   this size, because a third line is the reader deciding to look away. */
.card h3 { margin-top: 0.9rem; }
.card p  { font-size: var(--step-0); color: var(--stone); max-width: 46ch; }

/* ---- A full-bleed photograph as a section divider ------------------------
   Breaks the container on purpose: `100vw` off a centred child is the only
   way out of a max-width wrapper without restructuring every page's markup.
   The height is viewport-relative so it stays a picture and never collapses
   to a strip on a laptop. */
.band--photo {
  position: relative;
  left: 50%;
  width: 100vw;
  margin-left: -50vw;
  margin-block: clamp(3rem, 6vw, 6rem);
  overflow: hidden;
  background: var(--slate-deep);
}
.band--photo img {
  display: block;
  width: 100%;
  height: clamp(340px, 62vh, 720px);
  object-fit: cover;
}
.band--photo figcaption {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: clamp(1.5rem, 4vw, 3rem) var(--gutter);
  padding-inline: max(var(--gutter), calc((100vw - var(--max)) / 2));
  /* The caption sits on whatever the photograph happens to be doing at the
     bottom of the frame, which on a bright shot is foliage in full sun. A
     single soft gradient is not enough at that point — this is two stops of
     it, so the eyebrow clears AA on the worst case rather than the average. */
  background: linear-gradient(to top,
              rgba(10,8,7,0.94) 0%,
              rgba(10,8,7,0.74) 42%,
              rgba(10,8,7,0.30) 72%,
              transparent 100%);
  padding-top: clamp(4rem, 12vw, 9rem);
  color: var(--bone);
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(1.3rem, 3vw, 2.4rem);
  letter-spacing: -0.03em;
  line-height: 1.05;
}
/* On the label tokens. This used `var(--step-0)` — a BODY size for a label
   role, which is why the band caption sat 1.6px larger than every other
   label on the page and read as a small heading instead of a caption's first
   half. `display: block` also goes: the caption is a baseline row now, so
   the eyebrow sits beside its statement rather than above it. */
.band--photo figcaption span {
  font-family: var(--body);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--mark);
  margin-bottom: 0;
}

/* Not every band is the same height. Four identical strips down a page is a
   pattern the eye stops reading; one tall one breaks the meter. */
.band--photo--tall img { height: clamp(420px, 78vh, 900px); }
.band--photo:first-of-type { margin-top: 0; }

/* On an interior page the band sits directly under the heading and should
   read as part of it, not as a separate slab floating below. */
.pagehero + .band--photo { margin-top: 0; }

/* ---- Air, now that there is less to read -------------------------------- */
.section { padding-block: clamp(4.5rem, 9vw, 9rem); }
.section-head { margin-bottom: clamp(2.5rem, 4.5vw, 4rem); }
.lede { font-size: var(--step-1); color: var(--stone); max-width: 40ch; }

/* ---- The one green left, and the one place it is loud ------------------- */
.eyebrow::before { background: var(--mark); }


/* ==========================================================================
   A MASTHEAD, NOT A NAV BAR.

   62px of logo in an 96px bar is the default every site template ships with,
   and it is what made this header interchangeable with any other contractor's.
   The bar goes to 128px and the mark to 104px — big enough that the roundel's
   ring, its trees and its DZT all resolve, which is the whole reason for
   having drawn them. The nav sits on the baseline rather than the centre, so
   the mark has the height to itself and the links read as a footing under it.

   THE EMBER HAIRLINE IS THE SIGNATURE. One 2px rule the width of the page,
   under everything, in the brand colour — a surveyor's line. It is the
   cheapest possible thing that is not on anybody else's site.
   ========================================================================== */
:root { --nav-h: 216px; }

.site-head .nav { min-height: var(--nav-h); align-items: center; }

/* THE MARK IS THE MASTHEAD. 196px is past the point where the roundel is a
   badge in the corner — it is the thing at the top of the page.

   AND IT IS THE FULL LOCKUP AT ALL THREE BREAKPOINTS, INCLUDING 88px. That is
   only true because the wordmark got shorter: "LANDSCAPE BUILDERS" set 33.5
   high died under about 140px, and "LANDSCAPING" set 46 high is still a word
   at 64. The name is the reason the mark is in the masthead at all, so the
   size the name survives is the size that decides this. Home-screen icons
   (logo-180, logo-192) are the one place that still drops it — a launcher
   draws them at 48-64pt, where no cap height works. See LOCKUPS and
   SITE_FILES in tools/build-logo-roundel.py.

   THE BAR IS 216px, WHICH IS THE MARK PLUS 10px OF AIR EACH SIDE. That is the
   whole rule: `--nav-h` is not chosen, it is derived, so the mark can never
   grow into a bar that is too short to hold it. On the home page the height
   costs nothing at all — the hero pulls up underneath by exactly --nav-h, so
   the photograph still starts at the top of the window and the header floats
   on it. */
.brand__logo { width: 196px; height: 196px; }
.site-foot .brand__logo { width: 268px; height: 268px; }

/* §33 pins the bar at 72px with !important to reclaim the first screen, and
   that argument still holds — it is just that the mark is now the thing worth
   spending the height on. Same weapon, one step later in the file. */
@media (min-width: 1081px) {
  .site-head .nav { min-height: var(--nav-h) !important; }
}

/* THE RULE RIDES THE BOTTOM OF THE PANEL, not the viewport, so it tracks the
   header whether it is floating over a photograph or sitting on the bar. */
.site-head .nav { position: relative; }
.site-head .nav::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: linear-gradient(90deg,
              var(--mark) 0 clamp(64px, 12vw, 168px),
              rgba(242,239,231,0.18) clamp(64px, 12vw, 168px));
}

/* Nav links: the underline WIPES IN from the left rather than fading, and it
   is the ember. A fade is what a template does. */
.nav__links a {
  position: relative;
  text-transform: uppercase;
  letter-spacing: 0.13em;
  font-size: 0.76rem;
  font-weight: 600;
}
.nav__links a::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -0.55em;
  height: 2px;
  background: var(--mark);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.42s var(--ease);
}
.nav__links a:hover::after,
.nav__links a[aria-current="page"]::after { transform: scaleX(1); }

@media (max-width: 1080px) {
  :root { --nav-h: 132px; }
  .site-head .nav { min-height: 132px !important; }
  .brand__logo { width: 112px; height: 112px; }
  .site-foot .brand__logo { width: 180px; height: 180px; }
}
/* A phone has 390px of width and a 96px mark takes a quarter of it away from
   the phone number and the menu button, which are what the page is for on a
   handset. */
@media (max-width: 560px) {
  :root { --nav-h: 104px; }
  .site-head .nav { min-height: 104px !important; }
  .brand__logo { width: 88px; height: 88px; }
  .site-foot .brand__logo { width: 148px; height: 148px; }
}


/* ==========================================================================
   SCROLL AS A CAMERA MOVE.

   All of this is scroll-driven CSS — `animation-timeline: view()` and
   `scroll()` — so the browser runs it against scroll position rather than a
   clock, and there is no scroll listener, no rAF loop and no layout thrash
   anywhere on the page. Every rule sits inside an @supports gate: a browser
   without the timeline gets the page with no motion rather than a page stuck
   at the first keyframe, and the reduced-motion query switches it off for
   anyone who has asked.

   THE MOVES, AND WHY THEY ARE THESE MOVES:

     THE PICTURES OPEN. A photograph that fades in is a photograph appearing;
     one that unmasks from the middle outward is a photograph ARRIVING. The
     bands widen from 76% to full measure as they cross, which also means the
     page has two widths in it rather than one — the thing that stops a column
     of pictures reading as a stack of slabs.

     THE ROWS DRAW THEMSELVES IN. Each capability picture wipes in from its own
     outer edge and the copy beside it slides in from the opposite one, so a
     row assembles from both sides toward its middle. Alternating rows wipe
     from alternating sides, so the direction changes every time.

     THE HEADINGS UNMASK LEFT TO RIGHT, the way a word is read, rather than
     fading, which is what every template on the internet does.

     AND THERE IS A RAIL. Three pixels of ember across the top of the window,
     filling with the scroll — one element, one keyframe, no script. It is the
     brand colour doing a job no competitor's page is doing.

   THE PARALLAX IS DELIBERATELY SMALL. 8% of travel over a screen reads as
   depth; 30% reads as a plugin and makes people seasick.
   ========================================================================== */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* --- The scroll rail --------------------------------------------------- */
    body::before {
      content: '';
      position: fixed;
      top: 0; left: 0;
      width: 100%;
      height: 3px;
      z-index: 200;
      background: var(--volt);
      transform-origin: 0 50%;
      animation: rail linear both;
      animation-timeline: scroll(root block);
    }
    @keyframes rail { from { transform: scaleX(0); } to { transform: scaleX(1); } }

    /* --- The bands: open out, and drift ----------------------------------- */
    .band--photo {
      animation: band-open linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 42%;
    }
    @keyframes band-open {
      from { clip-path: inset(0 12% 0 12%); }
      to   { clip-path: inset(0 0 0 0); }
    }

    .band--photo { --band-h: clamp(400px, 68vh, 780px); }
    .band--photo--tall { --band-h: clamp(460px, 82vh, 900px); }
    .band--photo picture { display: block; overflow: hidden; height: var(--band-h); }
    .band--photo img {
      height: calc(var(--band-h) * 1.22);
      margin-top: calc(var(--band-h) * -0.11);
      animation: band-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
      will-change: transform;
    }
    @keyframes band-drift {
      from { transform: translate3d(0, -8%, 0); }
      to   { transform: translate3d(0,  8%, 0); }
    }

    .band--photo figcaption {
      animation: caption-rise linear both;
      animation-timeline: view();
      animation-range: entry 22% entry 82%;
    }
    @keyframes caption-rise {
      from { opacity: 0; transform: translate3d(0, 2.2rem, 0); }
      to   { opacity: 1; transform: none; }
    }

    /* --- The hero: the copy leaves before the picture does ----------------
       NOT the photograph. `.hero__media` is the slideshow's own stage — five
       absolutely-positioned slides crossfading on `.is-active` — and putting a
       view() timeline on it takes the element over from the slideshow. */
    .hero--photo .hero__inner, .hero--photo .wrap {
      animation: hero-lift linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 70%;
    }
    @keyframes hero-lift {
      from { opacity: 1; transform: none; }
      to   { opacity: 0; transform: translate3d(0, -3rem, 0); }
    }

    /* --- Capability rows: the picture wipes, the copy meets it ------------- */
    .bp-grid .card__shot {
      animation: wipe-from-left linear both;
      animation-timeline: view();
      animation-range: entry 4% entry 58%;
    }
    .bp-grid .card:nth-of-type(even) .card__shot { animation-name: wipe-from-right; }
    @keyframes wipe-from-left  { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
    @keyframes wipe-from-right { from { clip-path: inset(0 0 0 100%); } to { clip-path: inset(0 0 0 0); } }

    .bp-grid .card h3, .bp-grid .card p {
      animation: slide-in-right linear both;
      animation-timeline: view();
      animation-range: entry 12% entry 66%;
    }
    .bp-grid .card p { animation-range: entry 20% entry 74%; }
    .bp-grid .card:nth-of-type(even) h3,
    .bp-grid .card:nth-of-type(even) p { animation-name: slide-in-left; }
    @keyframes slide-in-right { from { opacity: 0; transform: translate3d(2.5rem, 0, 0); } to { opacity: 1; transform: none; } }
    @keyframes slide-in-left  { from { opacity: 0; transform: translate3d(-2.5rem, 0, 0); } to { opacity: 1; transform: none; } }

    /* --- Steps and quotes: still a lift, just a better one ----------------- */
    .steps-strip li, .quote-grid > * {
      animation: card-in linear both;
      animation-timeline: view();
      animation-range: entry 8% entry 62%;
    }
    @keyframes card-in {
      from { opacity: 0; transform: translate3d(0, 2.6rem, 0) scale(0.985); }
      to   { opacity: 1; transform: none; }
    }

    /* --- Headings: read left to right ------------------------------------- */
    .section-head h2, .statement h2, .cta-band h2 {
      animation: unmask linear both;
      animation-timeline: view();
      animation-range: entry 10% entry 62%;
    }
    @keyframes unmask {
      from { clip-path: inset(0 100% -20% 0); transform: translate3d(-0.6rem, 0, 0); }
      to   { clip-path: inset(0 -6% -20% 0);  transform: none; }
    }
    /* The eyebrow rule draws itself before the heading opens. */
    .section-head .eyebrow::before, .statement .eyebrow::before {
      animation: draw-rule linear both;
      animation-timeline: view();
      animation-range: entry 6% entry 40%;
      transform-origin: left center;
    }
    @keyframes draw-rule { from { transform: scaleX(0); } to { transform: scaleX(1); } }

    /* The JS reveal and the scroll timeline would otherwise both animate the
       same nodes and fight over the transform. Where the timeline exists it
       wins, so the JS pass is neutralised rather than removed — the fallback
       has to stay intact for browsers that never reach this block. */
    .js .card[data-reveal], .js .steps-strip li[data-reveal],
    .js .section-head [data-reveal], .js .statement [data-reveal] {
      opacity: 1;
      transform: none;
      transition: none;
    }
  }
}


/* ==========================================================================
   SCROLL AS A CAMERA MOVE — SECOND PASS.

   Everything above animates the pictures and the headings. This pass takes the
   sections that had no motion at all: the objection block, the numbered steps,
   the testimonials, the form, the closing band and the footer — plus a timed
   entrance for the hero, which is the one part of the page that is already on
   screen when it loads and therefore cannot be driven by scroll.

   STAGGER IS DONE WITH RANGES, NOT DELAYS. A scroll-driven animation has no
   clock, so `animation-delay` means nothing to it. Offsetting each sibling's
   `animation-range` start is the equivalent: item two begins a few percent of
   the viewport later than item one, which reads as a stagger while remaining
   entirely a function of scroll position. Scrub back up and it runs backwards,
   which a delay could never do.

   TWO ANIMATIONS ON ONE ELEMENT IS FINE WHEN THEY TOUCH DIFFERENT PROPERTIES.
   The capability shots wipe (clip-path) and settle (transform) at once. What
   you cannot do is animate the same property from two names, so nothing here
   does.
   ========================================================================== */

/* --- THE HERO ENTRANCE. Timed, not scrolled ------------------------------
   The hero is above the fold: a view() timeline on it would either have
   already finished before the first paint or never start. So this one is on a
   clock, it runs once, and it is the only timed animation on the page.

   `both` matters. Without a fill the elements are visible for one frame at
   their natural position before the animation takes hold, which is a flash of
   the finished layout followed by it re-assembling. */
@media (prefers-reduced-motion: no-preference) {
  .hero--photo .hero__badge,
  .hero--photo .hero__copy h1,
  .hero--photo .hero__lede,
  .hero--photo .hero__book,
  .hero--photo .hero__copy blockquote {
    animation: hero-in 0.9s cubic-bezier(0.16, 0.84, 0.44, 1) both;
  }
  .hero--photo .hero__badge          { animation-delay: 0.05s; }
  .hero--photo .hero__copy h1        { animation-delay: 0.16s; }
  .hero--photo .hero__lede           { animation-delay: 0.30s; }
  .hero--photo .hero__book           { animation-delay: 0.40s; }
  .hero--photo .hero__copy blockquote { animation-delay: 0.50s; }
  @keyframes hero-in {
    from { opacity: 0; transform: translate3d(0, 1.6rem, 0); }
    to   { opacity: 1; transform: none; }
  }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* --- The hero photograph pulls back as you leave it -------------------
       8% over a whole screen. The band images already drift; this is the same
       move on the one image that does not scroll past, only out. */
    .hero--photo .hero__slide {
      animation: hero-push linear both;
      animation-timeline: view();
      animation-range: exit 0% exit 100%;
    }
    @keyframes hero-push {
      from { transform: scale(1.08); }
      to   { transform: scale(1); }
    }

    /* --- The objection block ---------------------------------------------
       Four claims that were arriving all at once. Each one now draws its own
       rule, states the fear from the left and answers underneath, and each
       starts a little after the one before it. */
    .objection__item {
      animation: card-in linear both;
      animation-timeline: view();
      animation-range: entry 6% entry 56%;
    }
    .objection__item:nth-child(2) { animation-range: entry 11% entry 61%; }
    .objection__item:nth-child(3) { animation-range: entry 16% entry 66%; }
    .objection__item:nth-child(4) { animation-range: entry 21% entry 71%; }

    .objection__item::before {
      animation: rule-draw linear both;
      animation-timeline: view();
      animation-range: entry 8% entry 64%;
      transform-origin: 0 50%;
    }
    .objection__item:nth-child(2)::before { animation-range: entry 13% entry 69%; }
    .objection__item:nth-child(3)::before { animation-range: entry 18% entry 74%; }
    .objection__item:nth-child(4)::before { animation-range: entry 23% entry 79%; }
    @keyframes rule-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }

    .objection__fear {
      animation: slide-in-left linear both;
      animation-timeline: view();
      animation-range: entry 10% entry 62%;
    }
    .objection__item:nth-child(2) .objection__fear { animation-range: entry 15% entry 67%; }
    .objection__item:nth-child(3) .objection__fear { animation-range: entry 20% entry 72%; }
    .objection__item:nth-child(4) .objection__fear { animation-range: entry 25% entry 77%; }

    /* --- The capability shots settle as they wipe ------------------------- */
    .bp-grid .card__shot img {
      animation: shot-settle linear both;
      animation-timeline: view();
      animation-range: entry 4% entry 70%;
    }
    @keyframes shot-settle {
      from { transform: scale(1.09); }
      to   { transform: scale(1); }
    }

    /* --- The numbered steps ----------------------------------------------
       The numeral is the point of this strip, so it gets its own move: it
       arrives fractionally ahead of the words it belongs to. */
    .steps-strip__num {
      animation: num-in linear both;
      animation-timeline: view();
      animation-range: entry 4% entry 48%;
      display: inline-block;
    }
    .steps-strip li:nth-child(2) .steps-strip__num { animation-range: entry 8% entry 52%; }
    .steps-strip li:nth-child(3) .steps-strip__num { animation-range: entry 12% entry 56%; }
    .steps-strip li:nth-child(4) .steps-strip__num { animation-range: entry 16% entry 60%; }
    .steps-strip li:nth-child(5) .steps-strip__num { animation-range: entry 20% entry 64%; }
    @keyframes num-in {
      from { opacity: 0; transform: translate3d(0, 0.5em, 0) scale(0.82); }
      to   { opacity: 1; transform: none; }
    }
    .steps-strip li:nth-child(2) { animation-range: entry 12% entry 66%; }
    .steps-strip li:nth-child(3) { animation-range: entry 16% entry 70%; }
    .steps-strip li:nth-child(4) { animation-range: entry 20% entry 74%; }
    .steps-strip li:nth-child(5) { animation-range: entry 24% entry 78%; }

    /* --- Testimonials ----------------------------------------------------- */
    .quote-grid > *:nth-child(2) { animation-range: entry 13% entry 67%; }
    .quote-grid > *:nth-child(3) { animation-range: entry 18% entry 72%; }
    /* `display: inline-block` USED TO BE IN HERE, AND IT WAS LOAD-BEARING BY
       ACCIDENT. `.quote__stars { display: none }` a few thousand lines above
       hides the per-review star rows, and this declaration — inside
       @supports(view()) AND prefers-reduced-motion: no-preference — was the
       only thing undoing it. So a visitor with "reduce motion" switched on got
       a review band with NO STARS, and the same person with it off got stars.
       A motion preference was deciding whether content existed.
       Animating opacity and transform needs no display change at all, so the
       declaration is gone from here and the stars are made visible for
       everyone, outside every gate. */
    .quote__stars, .stars {
      animation: stars-in linear both;
      animation-timeline: view();
      animation-range: entry 12% entry 54%;
      transform-origin: 0 50%;
    }
    @keyframes stars-in {
      from { opacity: 0; transform: scale(0.7); }
      to   { opacity: 1; transform: none; }
    }

    /* --- The estimate form ------------------------------------------------
       Fields rise in the order they are filled in, which is the one stagger
       on this page that matches what the user is about to do. */
    .field, .field-row, .opt {
      animation: card-in linear both;
      animation-timeline: view();
      animation-range: entry 4% entry 46%;
    }
    .field:nth-of-type(2), .field-row:nth-of-type(2) { animation-range: entry 8% entry 50%; }
    .field:nth-of-type(3), .field-row:nth-of-type(3) { animation-range: entry 12% entry 54%; }
    .field:nth-of-type(4), .field-row:nth-of-type(4) { animation-range: entry 16% entry 58%; }
    .field:nth-of-type(n+5) { animation-range: entry 20% entry 62%; }

    .quotebox__head {
      animation: wipe-from-left linear both;
      animation-timeline: view();
      animation-range: entry 6% entry 46%;
    }
    .quotebox__body {
      animation: card-in linear both;
      animation-timeline: view();
      animation-range: entry 12% entry 58%;
    }

    /* --- The closing band -------------------------------------------------
       The two buttons are the last thing on the page that matters, so they
       arrive last and separately. */
    .cta-band .lede {
      animation: card-in linear both;
      animation-timeline: view();
      animation-range: entry 14% entry 64%;
    }
    .cta-band__actions .btn {
      animation: card-in linear both;
      animation-timeline: view();
      animation-range: entry 20% entry 70%;
    }
    .cta-band__actions .btn:nth-child(2) { animation-range: entry 26% entry 76%; }

    /* --- The footer -------------------------------------------------------
       It is the bottom of the page and it never moved. The mark scales in
       because at 268px it is the largest thing down there and it should not
       simply be present. */
    .site-foot .brand__logo {
      animation: foot-mark linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 60%;
    }
    @keyframes foot-mark {
      from { opacity: 0; transform: scale(0.9); }
      to   { opacity: 1; transform: none; }
    }
    .foot-col {
      animation: card-in linear both;
      animation-timeline: view();
      animation-range: entry 4% entry 54%;
    }
    .foot-col:nth-child(2) { animation-range: entry 8% entry 58%; }
    .foot-col:nth-child(3) { animation-range: entry 12% entry 62%; }
    .foot-col:nth-child(4) { animation-range: entry 16% entry 66%; }

    /* --- Ledes and statement notes trail their headings -------------------
       The heading unmasks; the paragraph under it should not arrive with it,
       or the pair reads as one block rather than as a thought and its
       elaboration. */
    .section-head .lede, .statement__note, .objection__item p {
      animation: card-in linear both;
      animation-timeline: view();
      animation-range: entry 16% entry 68%;
    }

    /* --- The chapter numerals --------------------------------------------- */
    .section:not(.section--tight):not(.cta-band) > .wrap::before {
      animation: chapter-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 40%;
    }
    /* To full, not to 0.5. The numeral is drawn as a 1.5px text-stroke on a
       transparent fill, so it is already a faint thing — taking it to half
       opacity on top of that would leave it almost invisible at the one moment
       it is meant to register. */
    @keyframes chapter-in {
      from { opacity: 0; transform: translate3d(0, -0.4em, 0); }
      to   { opacity: 1; transform: none; }
    }

    /* --- The photo bands light up as they arrive --------------------------
       A hair of brightness and saturation across the entry. It is not visible
       as an effect; what it does is make the band feel like it turns on. */
    .band--photo img {
      animation: band-drift linear both, band-light linear both;
      animation-timeline: view(), view();
      animation-range: entry 0% exit 100%, entry 0% entry 70%;
    }
    @keyframes band-light {
      from { filter: brightness(0.82) saturate(0.86); }
      to   { filter: brightness(1) saturate(1); }
    }

    /* The JS reveal and the timeline would otherwise fight over the same
       transform on these, exactly as they do for the nodes listed above. */
    .js .quotebox[data-reveal], .js .cta-band__actions[data-reveal],
    .js .objection__item[data-reveal], .js .foot-col[data-reveal] {
      opacity: 1;
      transform: none;
      transition: none;
    }
  }
}

/* ==========================================================================
   THE MASTHEAD ON PAPER, AND THE MASTHEAD ON A PHOTOGRAPH.

   The bar was a 30% blue-black glass panel — `rgba(16,22,27,0.3)` — laid over
   whatever was behind it. Over a photograph that is a scrim and it works. Over
   cream it is neither the page nor a band: it renders as a muddy grey slab,
   and at 128px there is a great deal of it with nothing in it. That is the
   empty header.

   So the header stops being one thing. On a photo hero it is a warm scrim that
   fades to nothing. On an interior page it is simply the paper the page is
   already made of, with the ember rule doing the separating. Neither case
   needs a panel.
   ========================================================================== */
body:not(:has(.hero--photo)) .site-head .nav {
  background: var(--head) !important;
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
  border: 0 !important;
  box-shadow: none !important;
}
body:not(:has(.hero--photo)) .site-head .nav__links a,
body:not(:has(.hero--photo)) .site-head .nav__phone { color: var(--ink) !important; }
body:not(:has(.hero--photo)) .site-head .nav__links a:hover { background: none !important; color: var(--mark) !important; }
body:not(:has(.hero--photo)) .site-head .nav__links a[aria-current="page"] { color: var(--mark) !important; }
body:not(:has(.hero--photo)) .site-head .nav::before { display: none !important; }
body:not(:has(.hero--photo)) .site-head .nav::after {
  background: linear-gradient(90deg,
              var(--mark) 0 clamp(64px, 12vw, 168px),
              var(--line) clamp(64px, 12vw, 168px));
}

/* THE SCRIM IS A DESKTOP MOVE, and this is the width gate it was missing.
   §33 deliberately paints NOTHING on a phone over a photograph: the header
   goes `position: absolute`, the nav box collapses to `min-height: 0`, the
   mark floats and the phone and menu are pills carrying their own contrast,
   with the type held up by halos rather than by a plate. That is a better
   answer on a small screen than any band, and it was right.

   Ungated, the gradient below overrode it — and because the mobile nav box is
   content height rather than bar height, what it painted was a fade that
   started under the top edge and stopped part way down the mark: a band that
   matched nothing, which is exactly what it looked like. */
@media (min-width: 1081px) {
  /* The gradient itself is set once, further down with the stuck state it
     transitions to. This only has to clear §33's glass panel out of the way. */
  body:has(.hero--photo) .site-head .nav {
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    border: 0 !important;
    box-shadow: none !important;
  }
}

/* On a phone the photo header keeps §33's no-paint treatment, and only takes
   the bar HEIGHT from the token — so `--nav-h` stays honest, which matters
   because the drawer is positioned with `inset: var(--nav-h) 0 0` and would
   otherwise open over the mark. */
@media (max-width: 1080px) {
  body:has(.hero--photo) .site-head { padding-block: 0 !important; }
  body:has(.hero--photo) .site-head .nav {
    min-height: var(--nav-h) !important;
    background: none !important;
  }
}

/* A PAPER BAR NEEDS INK ICONS. §33 sets `color: var(--bone) !important` on the
   phone and menu buttons for the dark mobile bar they used to sit on. Turning
   that bar to paper without turning them over left cream glyphs on cream. */
body:not(:has(.hero--photo)) .site-head .nav__toggle,
body:not(:has(.hero--photo)) .site-head .nav__phone,
body:has(.hero--photo) .site-head.is-stuck .nav__toggle,
body:has(.hero--photo) .site-head.is-stuck .nav__phone { color: var(--ink) !important; }
body:not(:has(.hero--photo)) .site-head .nav__toggle span,
body:not(:has(.hero--photo)) .site-head .nav__toggle span::before,
body:not(:has(.hero--photo)) .site-head .nav__toggle span::after,
body:has(.hero--photo) .site-head.is-stuck .nav__toggle span,
body:has(.hero--photo) .site-head.is-stuck .nav__toggle span::before,
body:has(.hero--photo) .site-head.is-stuck .nav__toggle span::after { background: var(--ink) !important; }


/* ==========================================================================
   ONE PICTURE AT A TIME.

   Six 620px cards two-up is still six photographs arriving at once, in one
   section, competing with each other — the clutter is the grid, not the size.
   A portfolio does not want a contact sheet; it wants one build, then the next
   build, with a screen of scroll between them.

   So the capability grid stops being a grid. Each card becomes a full row: one
   large photograph and three or four words about it, alternating side to side
   so the eye has somewhere new to land each time. Six small pictures in one
   screen become six large ones spread over six.

   AND THE BANDS COME IN OFF THE EDGES. Full-bleed made every photograph the
   same shape and the same width as the viewport, which is its own kind of
   monotony and gives the page no margin to breathe in. They sit in the
   container now, at the width the rest of the page is measured by.
   ========================================================================== */

/* ---- Bands: contained, not bled --------------------------------------- */
.band--photo {
  left: auto;
  width: min(100% - (2 * var(--gutter)), var(--max));
  margin-inline: auto;
  margin-block: clamp(4rem, 9vw, 10rem);
}
.band--photo img { height: clamp(400px, 68vh, 780px); }
.band--photo--tall img { height: clamp(460px, 82vh, 900px); }
.band--photo figcaption { padding-inline: clamp(1.5rem, 4vw, 3rem); }
.pagehero + .band--photo { margin-top: clamp(2rem, 4vw, 4rem); }

/* ---- The capability grid becomes a sequence ---------------------------- */
.bp-grid .grid-3 { display: block; }
.bp-grid .card {
  display: grid;
  gap: clamp(1.5rem, 3.5vw, 3.5rem);
  align-items: center;
  margin-block: clamp(4rem, 10vw, 10rem);
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
}
.bp-grid .card:first-child { margin-top: clamp(2rem, 5vw, 4rem); }
.bp-grid .card:last-child  { margin-bottom: 0; }

/* ALTERNATING BY SWAPPING THE TRACKS, NOT BY REORDERING THE ITEMS. Moving the
   picture into column two with `order` left it in the 0.58fr track and it came
   out a third of the size — the wide track has to move with it. Placement is
   explicit, so the DOM order (picture, heading, line) is untouched and a screen
   reader reads every row the same way. */
@media (min-width: 900px) {
  .bp-grid .card { grid-template-columns: 1.42fr 0.58fr; }
  .bp-grid .card__shot,
    .bp-grid .card__media { grid-column: 1; grid-row: 1 / span 2; }
  /* `align-self` CAME OFF THESE TWO AND IT WAS A REAL BUG, not tidying.
     These rules were written when the heading and the paragraph were DIRECT
     grid children of `.card`, where `align-self` works on the block axis and
     `end` meant "sit the heading on the bottom of the picture's row". They
     are now inside a `.card__copy` flex COLUMN, where align-self works on the
     cross axis — which is horizontal — so `end` stopped meaning "bottom" and
     started meaning "shove this to the right edge". And because a flex item
     shrinks to its content, the shove is as wide as the title is short:
     measured, "Outdoor Living" sat 150px right of its own column while
     "Hardscape & Masonry" looked fine because it happens to fill the width.
     The `grid-column` halves are inert for the same reason — these elements
     are not grid items any more — and are left only because this block still
     reads as the record of the pre-wrap layout. */
  .bp-grid .card h3    { margin-top: 0; }
  .bp-grid .card p     { margin-top: 0.6rem; }

  .bp-grid .card:nth-of-type(even) { grid-template-columns: 0.58fr 1.42fr; }
  .bp-grid .card:nth-of-type(even) .card__shot,
    .bp-grid .card:nth-of-type(even) .card__media { grid-column: 2; }
  .bp-grid .card:nth-of-type(even) h3,
  .bp-grid .card:nth-of-type(even) p { grid-column: 1; }
}

/* THE WRAPPER INHERITS THE PICTURE'S JOB. `.bp-grid .card` is a two-column
   grid with the photo placed explicitly in column one across both rows —
   so wrapping the picture to hang a thumbnail strip on it made the WRAPPER
   the grid item and left the placement pointing at something that is no
   longer one. The card collapsed to a single stacked column and the photo
   shrank to whatever its content asked for. Named alongside the picture in
   every rule that places it, rather than duplicating the layout. */
.bp-grid .card__shot,
  .bp-grid .card__media { margin: 0; background: var(--paper-3); }
.bp-grid .card__shot img { aspect-ratio: 4 / 3; width: 100%; height: auto; }
.bp-grid .card h3 {
  margin: 0;
  font-size: clamp(1.75rem, 3.6vw, 2.9rem);
  line-height: 1.02;
  letter-spacing: -0.04em;
}
.bp-grid .card p {
  margin-top: 0.85rem;
  font-size: var(--step-1);
  color: var(--stone);
  max-width: 34ch;
}

/* The row itself does not animate — its picture and its copy do, separately
   and from opposite sides. Animating the row as one block would move the two
   together and lose the assembly. */


/* ==========================================================================
   THE PHOTOGRAPH STARTS AT THE TOP OF THE PAGE.

   §33 pulls the hero up under the sticky header only below 900px; above it the
   header took 128px of layout and the picture began underneath, which put a
   band of nothing across the top of the first screen. The pull-up applies at
   every width now, so the header floats ON the photograph rather than above
   it, and the first thing on the page is the picture.

   THE SCRIM COMES OFF ONCE THE HERO IS GONE. `.is-stuck` lands at 48px of
   scroll, and past that the bar is sitting over cream, where a dark gradient
   is a grey smear across the content. Scrim while there is a photograph
   behind it; paper once there is not.
   ========================================================================== */
.hero--photo {
  /* --head-h is measured by main.js and still reports the 72px bar this file
     replaced, so the pull-up has to come off --nav-h, which IS the height. */
  margin-top: calc(-1 * var(--nav-h));
  padding-top: calc(var(--nav-h) + clamp(2.5rem, 7vh, 5rem));
}

/* THE SCRIM AND THE STUCK STATE ARE DESKTOP-ONLY, and gating them is the whole
   fix. On a phone §33's header is `position: absolute` — it scrolls away with
   the hero rather than sticking, so there is no stuck state to style, and the
   no-paint treatment is what should survive. An earlier version of this block
   set the gradient ungated and it beat the mobile rule further up the file
   purely by coming later, which is the band that appeared under the mark. */
@media (min-width: 1081px) {
  /* NO TRANSITION ON `background`. It was there to soften the scrim-to-paper
     switch and it broke the switch instead: the shorthand expands to include
     background-image, the gradient and the flat colour cannot interpolate, and
     Chrome held the computed background at transparent — even against an
     inline `!important`, which is how it was finally caught. §22 has carried
     `transition: background 0.4s` on this element since long before any of
     this — it was fine when both states were rgba colours, and it is not fine
     now that one of them is a gradient. Box-shadow keeps its transition; the
     bar changes state at 48px of scroll, where a fade is not what anyone is
     looking at anyway. */
  /* HALVED, AND IN THE PAGE'S OWN BLACK. Two veils were stacking over the top
     quarter of the hero: this one across the full 216px of nav, plus a
     top-down layer inside .hero__media::after. Compounded they reached ~0.74
     at y=0 and were still 0.30 at y=180 — over the exact region the markup
     says the frame was chosen for ("canopy over the whole width, the sun
     breaking through it"). We bought type contrast by painting out the
     photograph we were paying to show.
     The other layer is deleted outright and this one drops to 0.34, spread
     further down so it has no visible edge. rgba(10,8,7) was a warm near-black
     belonging to no token in this file; this is --paper's own value, which is
     one fewer black on the page. Nav legibility comes back from the glyphs
     instead — see the text-shadow rule further down. */
  body:has(.hero--photo) .site-head .nav {
    background: linear-gradient(to bottom,
      rgba(22, 26, 29, 0.34) 0%,
      rgba(22, 26, 29, 0.13) 48%,
      rgba(22, 26, 29, 0.00) 100%) !important;
    transition: box-shadow 0.4s !important;
  }
  body:has(.hero--photo) .site-head.is-stuck .nav {
    background: var(--head) !important;
  }
  body:has(.hero--photo) .site-head.is-stuck .nav__links a,
  body:has(.hero--photo) .site-head.is-stuck .nav__phone { color: var(--ink) !important; }
  body:has(.hero--photo) .site-head.is-stuck .nav__links a:hover,
  body:has(.hero--photo) .site-head.is-stuck .nav__links a[aria-current="page"] { color: var(--mark) !important; background: none !important; }
  body:has(.hero--photo) .site-head.is-stuck .nav::after {
    background: linear-gradient(90deg,
                var(--mark) 0 clamp(64px, 12vw, 168px),
                var(--line) clamp(64px, 12vw, 168px));
  }
}

/* §21 sets `#build .card h3` — an id, which outranks any number of classes,
   so the row headings stayed at 1.12rem while the picture beside them went to
   823px. Matched on the id rather than beaten with !important. */
#build.bp-grid .card h3 {
  font-size: clamp(1.75rem, 3.4vw, 2.8rem);
  line-height: 1.02;
  letter-spacing: -0.04em;
}
#build.bp-grid .card p { font-size: var(--step-1); }


/* A band's own margin and the next section's padding were stacking to 19rem of
   nothing between a photograph and the heading under it. Whichever comes
   second gives up its share. */
.band--photo + .section, .band--photo + section { padding-top: 0; }
.section + .band--photo, section + .band--photo { margin-top: clamp(1rem, 2vw, 2.5rem); }
.band--photo { margin-block: clamp(2.5rem, 5vw, 6rem); }
.statement + .band--photo { margin-top: 0; }


/* ==========================================================================
   THE PAGE GETS A STRUCTURE YOU CAN SEE.

   Sections separated by nothing but vertical space is what every template
   does, and it is why the page still read as a stack of blocks. Three devices,
   none of them decoration:

     CHAPTER NUMERALS in the left margin, from a CSS counter, so the page reads
     as an argument in order — 01 how we build, 02 what goes wrong, 03 what we
     make — instead of as sections that happen to follow each other. Outlined
     rather than filled, because a solid 120px numeral would outweigh the
     headline it is labelling.

     MID-TONE SURFACES. `--paper-3` exists now, so anything raised sits on it
     instead of on the page. This is what makes the dark read as a room with
     things in it rather than as a void with things floating in it.

     A CURTAIN, NOT A FADE. Every photograph is covered by a panel of the brand
     green that slides off as the picture enters — from the outer edge, so it
     runs the same direction the row is built in. A fade is what a plugin does;
     something physically uncovering the work is the reveal this business
     actually sells.
   ========================================================================== */

/* ---- Chapter numerals ---------------------------------------------------- */
body { counter-reset: chapter; }
.section:not(.section--tight):not(.cta-band) { counter-increment: chapter; }

@media (min-width: 1180px) {
  .section:not(.section--tight):not(.cta-band) > .wrap { position: relative; }
  /* INSIDE THE CONTAINER, TOP RIGHT. In the margin was the first idea and it
     does not survive the measure: at 1280px inside a 1440px window there are
     80px of margin and a 72px numeral is 90px wide, so it hung off the edge of
     the screen. Top-right of the wrap is space every section head already has
     spare, and it puts the number where a chapter number goes in print. */
  .section:not(.section--tight):not(.cta-band) > .wrap::before {
    content: counter(chapter, decimal-leading-zero);
    position: absolute;
    top: -0.18em;
    right: 0;
    font-family: var(--display);
    font-weight: 900;
    font-size: clamp(3rem, 5vw, 5.5rem);
    line-height: 1;
    letter-spacing: -0.05em;
    /* Outlined. A filled numeral this size competes with the h2 beside it. */
    color: transparent;
    -webkit-text-stroke: 1.5px var(--line-2);
    pointer-events: none;
    user-select: none;
  }
}

/* ---- Raised things sit on the mid-tone ----------------------------------- */
/* `.steps-strip li` CAME OUT OF THIS LIST, and it had to. Four thousand lines
   above, §37 made a deliberate decision for the process steps — "columns under
   a rule, not boxes" — setting them to `background: none; border: 0;
   border-top: 1px solid var(--line-2); padding: 1.1rem 0 0`. This rule has the
   same specificity (0,1,1), sits later in the file, and quietly reversed half
   of it: the fill came back but THE PADDING DID NOT. What shipped was five
   #262D33 slabs 205px wide with the heading and the body copy welded flat
   against the left and right edges of their own fill, and the top rule
   downgraded from 0.24 alpha to 0.13. It also defeated a rule a few hundred
   lines up that says in as many words that chips and list items are not
   surfaces.
   `.objection__grid > *` comes out too — that class is in none of the 25 HTML
   files; the block has been a checklist since 2026-09-15. */
.card:not(.bp-grid .card), .quote, .quote-grid > *, .acc__item {
  background: var(--paper-3);
  border-color: var(--line);
}
.bg-ink2, .bg-surface { background: var(--paper-2); }
.statement { background: var(--paper-2); }

/* ---- The curtain --------------------------------------------------------- */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    .bp-grid .card__shot, .band--photo picture { position: relative; }
    .bp-grid .card__shot::after, .band--photo picture::after {
      content: '';
      position: absolute;
      inset: 0;
      z-index: 2;
      background: var(--mark);
      transform-origin: left center;
      animation: curtain linear both;
      animation-timeline: view();
      animation-range: entry 6% entry 58%;
    }
    /* Odd rows are uncovered left-to-right, even rows right-to-left, so the
       reveal runs the same way the alternating row is read. */
    .bp-grid .card:nth-of-type(even) .card__shot::after { transform-origin: right center; }
    @keyframes curtain { from { transform: scaleX(1); } to { transform: scaleX(0); } }

    /* The clip-path wipe the curtain replaces. */
    .bp-grid .card__shot { animation: none; }
  }
}


/* ==========================================================================
   THE ASSISTANT, AFTER THE INVERSION.

   §47 and §56 built this widget for a cream page: a white glass panel, dark
   type, and a forest launcher with a light label. Turning the site dark took
   the tokens with it and left every one of those pairings backwards. Measured
   on what was actually shipping:

       --bone on --volt ............ 2.20   the launcher's own label
       --ink on --ink (hover) ...... 1.00   the launcher vanished on hover,
                                            because `--ink` used to be near-black
                                            and is now near-white
       --ink on #fff ............... 1.21   every word in the open panel

   None of it showed in a diff — the token names never changed, only their
   values. This is the whole widget re-pointed at the dark system, and the rule
   the palette already had applies here too: ON A COLOURED FILL THE LABEL IS
   THE GROUND, NOT THE INK.
   ========================================================================== */

/* ---- The launcher -------------------------------------------------------- */
.ai__fab {
  background: var(--volt);
  color: var(--bone);                    /* 6.65:1 */
  background-image: none;
  box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.35),
    0 18px 38px -18px rgba(0, 0, 0, 0.7);
}
.ai__fab:hover {
  background: var(--volt-hi);
  color: var(--bone);
  box-shadow:
    0 3px 8px rgba(0, 0, 0, 0.4),
    0 26px 52px -22px rgba(0, 0, 0, 0.75);
}
/* `.ai__fab svg { color: var(--paper) }` WAS HERE. It painted the launcher's
   glyph in the page's near-black on the green fill — 2.94:1, against a label
   beside it at 5.96:1, in the same button. And below 720px the label is
   clipped, so on every phone that icon IS the button. Deleted; the SVG
   inherits currentColor from the rule above, which is the label's own bone. */

/* ---- The panel ----------------------------------------------------------- */
/* THE DARK RE-POINT THAT USED TO BE HERE WAS DEAD. It restated the panel's
   background, border, backdrop-filter and shadow with no !important, against
   a rule ~2,000 lines earlier that sets all four WITH !important — so it lost
   every declaration and had never once painted. The fix now lives at that
   earlier rule, where it can actually win. Only the backdrop-filter reset
   survives here, because it has to sit after the @supports glass block. */
.ai__panel {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.ai__head { background: var(--slate-deep); border-bottom: 1px solid var(--line); }
/* `.ai__head *` came off this. At (0,2,0) the universal half beat
   `.ai__clear { color: var(--faint) }`, so the DESTRUCTIVE reset control
   rendered pure white — louder than the close X beside it and exactly as
   loud as the panel title, in a panel whose own notes say the reset should
   be "quiet by default". */
.ai__head { color: var(--bone); }
.ai__sub { color: var(--bone-mute); }
.ai__close { color: var(--bone-mute); }
.ai__close:hover { color: var(--bone); }

/* The transcript: the visitor on the fill, the assistant on the mid-tone. */
.ai__msg--you { background: var(--volt); color: var(--bone); }
.ai__msg--bot { background: var(--paper-3); color: var(--ink); box-shadow: none; }
.ai__msg--wait { color: var(--stone); }
.ai__msg--greet p { color: var(--ink); }

/* ---- Everything that used to be white ------------------------------------ */
.ai__form { background: transparent; border-top: 1px solid var(--line); }
.ai__input, .ai__chip {
  background: var(--white);
  color: var(--ink);
  border: 1px solid var(--line-2);
  box-shadow: none;
}
.ai__input::placeholder { color: var(--faint); }
.ai__input:hover, .ai__chip:hover { background: var(--paper-3); }
.ai__input:focus { outline: 2px solid var(--mark); outline-offset: 1px; }
.ai__send { background: var(--volt); color: var(--bone); }
.ai__send:hover { background: var(--volt-hi); color: var(--bone); }

/* The call-back button inside the panel is a filled CTA like any other, and
   the same rule applies to it: the label is the ground. It was the last thing
   in the widget still set in bone on the green — 2.2:1. */
.ai__cb-go { background: var(--volt); color: var(--bone); border-color: var(--mark); }
.ai__cb-go:hover { background: var(--volt-hi); border-color: var(--clay-hi); color: var(--bone); }
.ai__cb-input, .ai__cb input {
  background: var(--white); color: var(--ink); border: 1px solid var(--line-2);
}
.ai__cb-input::placeholder, .ai__cb input::placeholder { color: var(--faint); }

/* §56's reduced-transparency fallback painted the panel #fff and the inputs
   #fff, which is now the brightest thing on a dark page. Same values as above,
   so the fallback and the default agree. */
@media (prefers-reduced-transparency: reduce) {
  .ai__panel { background: var(--paper-2); }
  .ai__msg--bot { background: var(--paper-3); }
  .ai__input, .ai__chip { background: var(--white); }
}

/* ---------------------------------------------------------------------------
   A SECTION WITH MORE THAN ONE PHOTO

   THE STRIP SITS ON THE IMAGE, NOT UNDER IT. The first version made it a row
   of its own in the card's grid, which did exactly what a new row does: it
   took the full width, left-aligned a few small pictures against all that
   space, and pushed the heading and the copy down the card. Reported as "the
   catalog part ended up in the leftside pushing the section name and wording
   down", which is precisely what it was.

   Overlaid, it costs the layout nothing. The heading stays where it has always
   been, the card is the same height as every card beside it, and the controls
   are attached to the thing they control instead of floating below it.

   A SCRIM UNDER THEM because these photographs are not a uniform tone: one is
   a dusk shot with a lit walk, another is bright winter sun on concrete, and
   small thumbnails on top of either need something to sit on.
--------------------------------------------------------------------------- */
.card__media { position: relative; display: block; }
/* The picture already bleeds to the card edge with negative margins; the
   wrapper inherits that job so the overlay can be positioned against the same
   box the photo actually occupies. */
.card__media .card__shot { margin-left: 0; margin-right: 0; margin-top: 0; }
/* No negative margin here: inside .bp-grid the photo is a placed grid item
   and bleeds by virtue of the card having no padding at that breakpoint. The
   first version copied the picture's old margins onto the wrapper and pulled
   it out of its own cell. */
.proj__img { position: relative; }

/* BELOW THE PHOTO, NOT ON IT. The overlaid version was tidy and nobody would
   ever have pressed it: thumbnails sitting in the bottom corner of a picture
   read as part of the picture, and a control a customer does not notice is a
   control that is not there. Reported exactly that way.

   On the home page the strip lives inside `.card__media`, which IS column one
   of the card's two-column grid — so it sits under the photo and the heading
   in column two does not move. On the projects page `.proj__img` is a clipped
   4:3 box, so the strip is a sibling underneath it instead. */
.shot-strip {
  display: flex; gap: 8px; margin-top: 10px;
  overflow-x: auto; scrollbar-width: none;
}
.shot-strip::-webkit-scrollbar { display: none; }
/* `.proj` puts 1.1rem between its rows, which is right between a photo and a
   paragraph and much too far between a photo and its own thumbnails. */
.proj .shot-strip { margin-top: -0.5rem; }

.shot-thumb {
  flex: 0 0 auto; width: 74px; aspect-ratio: 4 / 3;
  padding: 0; border: 0; border-radius: 6px; overflow: hidden;
  background: none; cursor: pointer;
  /* Dimmed, not hidden: every thumbnail has to look pressable, so the unselected
     ones stay legible rather than fading toward the background. */
  opacity: 0.68; filter: saturate(0.9);
  outline: 2px solid transparent; outline-offset: 2px;
  transition: opacity 0.2s var(--ease), outline-color 0.2s var(--ease), filter 0.2s;
}
.shot-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shot-thumb:hover, .shot-thumb:focus-visible { opacity: 1; filter: none; }
.shot-thumb.is-on { opacity: 1; filter: none; outline-color: var(--mark, currentColor); }
[data-shots] picture { cursor: zoom-in; }

/* One photo, one card, nothing overlaid — a section with a single image is
   untouched because it has no .shot-strip to begin with. */

/* ---- the large view ---- */
.shotbox {
  border: 0; padding: 0; background: transparent;
  max-width: min(1200px, 94vw); max-height: 92vh;
}
.shotbox::backdrop { background: rgba(8, 10, 9, 0.86); }
.shotbox img {
  display: block; width: 100%; height: auto;
  max-height: 82vh; object-fit: contain; border-radius: 10px;
}
.shotbox__cap {
  margin: 0.6rem 2px 0; color: #e9ece9; font-size: 0.8rem; line-height: 1.45;
}
.shotbox__x {
  position: absolute; top: -2.4rem; right: 0;
  width: 2rem; height: 2rem; padding: 0; line-height: 1;
  border: 0; border-radius: 999px; cursor: pointer;
  background: rgba(255, 255, 255, 0.14); color: #fff; font-size: 1.3rem;
}
.shotbox__x:hover { background: rgba(255, 255, 255, 0.26); }
@media (prefers-reduced-motion: no-preference) {
  .shotbox[open] { animation: shotIn 0.18s var(--ease); }
  @keyframes shotIn { from { opacity: 0; transform: scale(0.98); } }
}

/* ============================================================
   DESKTOP HEADER — smooth shrink to a slim bar on scroll
   Owner, 2026-09-15. Unscrolled, the desktop header is a tall ~216px band with
   a 196px roundel. The moment the page scrolls (main.js adds .is-stuck past
   48px) it should collapse to a standard slim header, logo and all, in one
   smooth motion. Scoped to >=1081px so the MOBILE header and logo are left
   exactly as they are. min-height carries !important to beat the base
   `.site-head .nav { min-height: var(--nav-h) !important }`.
   ============================================================ */
@media (min-width: 1081px) {
  /* FREEZE THE FLOW HEIGHT. The header keeps reserving its full height whether
     stuck or not (min-height = --nav-h + its own 1.7rem padding-block), so the
     shrink never changes the document's layout — no jumping sections, and the
     .is-stuck state can't oscillate at the 48px threshold. Only the glass bar
     and logo INSIDE the header shrink; the space that opens up above the content
     is made click-through so nothing underneath is blocked. */
  .site-head {
    min-height: calc(var(--nav-h) + 1.7rem);
    pointer-events: none;
  }
  /* Keep padding constant so it does not animate on its own 0.35s timing and
     fight the shrink. One clean, symmetric ease for both shrink and unshrink. */
  .site-head, .site-head.is-stuck { padding-block: 0.85rem; }
  /* Pin the brand to the unscrolled logo width so the logo scales IN PLACE when
     it shrinks, instead of the brand collapsing and sliding the centred nav
     links sideways (that lateral slide, not the speed, was the jank). */
  .site-head .nav .brand { width: 196px; }
  .site-head .nav { pointer-events: auto; }
  /* STEADY, SCROLL-LINKED SHRINK. The bar height (216px -> 74px) and the logo
     (196px -> 62px) interpolate from `--shrink`, which main.js runs 0 -> 1 over
     the first 180px of scroll — so the header shrinks and grows continuously
     WITH the scroll instead of snapping between two sizes at one threshold. The
     short transition only smooths the discrete scroll steps; it is not what
     drives the change.
     The doubled `.site-head.site-head` and the !important match the specificity
     the old `.site-head.is-stuck` rules needed to win over the base
     `min-height: var(--nav-h) !important` and the photo-hero header rules. */
  .site-head.site-head .nav {
    min-height: calc(var(--nav-h) - (var(--nav-h) - 74px) * var(--shrink, 0)) !important;
    transition: min-height 0.22s ease-out;
  }
  .site-head.site-head .brand__logo {
    width: calc(196px - 134px * var(--shrink, 0)) !important;
    height: calc(196px - 134px * var(--shrink, 0)) !important;
    transition: width 0.22s ease-out, height 0.22s ease-out;
  }
}
@media (min-width: 1081px) and (prefers-reduced-motion: reduce) {
  /* The size still tracks scroll (it is user-driven), but with no easing tail. */
  .site-head.site-head .nav, .site-head.site-head .brand__logo { transition: none; }
}

/* ============================================================
   MOBILE — band photos show the WHOLE frame, not a cropped strip
   Owner, 2026-09-15. The full-bleed 16:9 bands were forced into a tall portrait
   box (fixed height + object-fit:cover), which cut the left and right off the
   shot on a phone. Below 700px let the image size to its own 16:9 so the full
   width of the photo is visible; drop the parallax drift (it assumes the image
   overflows its box). Desktop is unchanged.
   ============================================================ */
@media (max-width: 700px) {
  .band--photo,
  .band--photo picture { height: auto; }
  .band--photo img {
    height: auto;
    margin-top: 0;
    object-fit: contain;
    transform: none;
    animation: none;
  }
}

/* ============================================================
   MOBILE — reviews scroll sideways, with the next card peeking
   Owner, 2026-09-15. The review grid stacked one card per row on a phone, so
   you could not tell more than one review existed. Below 700px it becomes a
   horizontal snap-scroller and each card is 85% wide, so the edge of the next
   review always shows — a clear cue there is a second and third one to swipe to.
   ============================================================ */
@media (max-width: 700px) {
  .quote-grid {
    display: flex;
    grid-template-columns: none;
    gap: 1rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scroll-padding-inline: var(--gutter);
    padding-bottom: 0.75rem;
    /* a thin scrollbar left visible on platforms that show one = extra cue */
  }
  .quote-grid > .quote {
    flex: 0 0 85%;
    scroll-snap-align: start;
  }
}

/* ============================================================
   AI CHAT — big Call / Text buttons under the greeting
   Owner, 2026-09-15. The two fastest ways to reach a person are now the loudest
   thing in the panel (replacing the small "or call / text" text link). Call is
   the solid primary; Text is the outlined secondary; the number reads beneath.
   ============================================================ */
.ai__contact {
  display: flex;
  gap: 0.55rem;
  margin: 0.1rem 0 0.1rem;
}
.ai__contact-btn {
  flex: 1 1 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  padding: 0.85rem 1rem;
  border-radius: 0;   /* was a hardcoded 12px — see the radius note below */
  font-family: var(--display), system-ui, sans-serif;
  font-weight: 800;
  font-size: 1.02rem;
  letter-spacing: -0.01em;
  text-decoration: none;
  transition: transform 0.1s ease, background 0.2s ease, border-color 0.2s ease;
}
.ai__contact-btn svg { width: 19px; height: 19px; }
.ai__contact-btn:active { transform: translateY(1px); }
.ai__contact-btn--call { background: var(--volt); color: var(--bone); }
.ai__contact-btn--call:hover { background: var(--clay-hi); color: var(--bone); }
.ai__contact-btn--text {
  background: rgba(255, 255, 255, 0.06);
  color: var(--bone);
  border: 1.5px solid rgba(255, 255, 255, 0.24);
}
.ai__contact-btn--text:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.42);
}
.ai__contact-num {
  margin: 0.25rem 0 0.1rem;
  text-align: center;
  font-family: var(--display), system-ui, sans-serif;
  font-weight: 800;
  font-size: 1.02rem;
  letter-spacing: 0.01em;
  color: var(--bone);
}

/* ============================================================
   OBJECTION SECTION — relatable intro + "what it looks like" checklist
   Owner, 2026-09-15. Replaced the four fear-quote cards (read as halfassed /
   not relatable) with an aspirational lead and a clean checklist of what working
   with DZT actually looks like. Original copy; keeps the real commitments.
   ============================================================ */
.objection__intro { max-width: 46rem; }
.objection__intro .lede { max-width: 44ch; }
.objection__list {
  list-style: none;
  margin: clamp(2rem, 4vw, 3.25rem) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1.1rem, 2.5vw, 1.8rem) clamp(1.6rem, 3.5vw, 3.5rem);
  max-width: 60rem;
}
/* 2x2 on a real screen keeps four items balanced instead of 3 + a lonely one. */
@media (min-width: 700px) {
  .objection__list { grid-template-columns: 1fr 1fr; }
}
.objection__list li {
  position: relative;
  padding-left: 2.4rem;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--stone);
}
.objection__list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.05rem;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 999px;
  background-color: var(--mark);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") no-repeat center / 62%;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E") no-repeat center / 62%;
  background-image: none;
}
.objection__list strong {
  display: block;
  font-family: var(--display), system-ui, sans-serif;
  font-weight: 800;
  font-size: 1.06rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin-bottom: 0.2rem;
}

/* The hero-page rule at ~7366 forces the header nav's transition to box-shadow
   only (!important), which silently killed the bar's height animation — so only
   the logo animated and the bar snapped, reading as "unsmooth". Restore the
   min-height transition in sync with the logo here (same duration/easing),
   keeping the box-shadow transition the original rule wanted. */
@media (min-width: 1081px) {
  body:has(.hero--photo) .site-head .nav {
    transition: min-height 0.5s cubic-bezier(0.33, 1, 0.68, 1),
                box-shadow 0.4s !important;
  }
}

/* Kill the black strip at the very top. The desktop header now reserves
   --nav-h + its 1.7rem padding (frozen so the shrink never reflows the page),
   but the hero only pulled up by --nav-h — leaving that difference as a black
   band above the photo. Pull the hero up by the full header height and pad it
   back down, so the hero reaches the top edge. Desktop only (the freeze is). */
@media (min-width: 1081px) {
  .hero--photo {
    margin-top: calc(-1 * (var(--nav-h) + 1.7rem));
    padding-top: calc(var(--nav-h) + 1.7rem + clamp(2.5rem, 7vh, 5rem));
  }
}

/* Scrolled off the hero: a SOLID dark bar. An earlier attempt at a fade layer
   left the nav's own background as the translucent scrim, so over the photo
   bands the header showed the content THROUGH it and read as a glitch. The
   ::before layer that fade relied on is display:none, so it never rendered.
   Just make the scrolled bar a solid, opaque dark (darker than --head). Desktop
   hero only. */
@media (min-width: 1081px) {
  body:has(.hero--photo) .site-head.is-stuck .nav {
    background: #07090b !important;
  }
}

/* Sections felt vertically squished. Main culprit: a section right after a photo
   band had padding-top:0, jamming its heading against the image. Restore real top
   padding there, and give section headers a bit more breathing room. Both mobile
   and desktop. */
.band--photo + .section, .band--photo + section { padding-top: clamp(3.5rem, 7vw, 6rem); }
.section-head { margin-bottom: clamp(2.75rem, 5vw, 4.5rem); }
@media (max-width: 700px) {
  .section { padding-block: clamp(3.25rem, 11vw, 5rem); }
  .band--photo + .section, .band--photo + section { padding-top: clamp(2.5rem, 9vw, 3.75rem); }
  .section-head { margin-bottom: clamp(1.9rem, 6vw, 2.75rem); }
}
/* ==========================================================================
   THE HERO, AFTER THE PHOTOGRAPH CHANGED

   Two fixes, both about the picture rather than the palette.
   ========================================================================== */

/* ---- 1. the availability dot: no mint anywhere ------------------------- */
/* The comment above .hero__live says "It is bone now" and then sets
   `background: var(--volt)`, which is the button green — a green dot drawn on
   green foliage, which is no dot at all. Worse, the pulse ring it throws is
   rgba(74,222,147): a mint from an ancestor palette, and the only mint pixel
   left in the build. Bone for both, which is what that comment always
   intended, matches the type beside it, and still reads as "on" at 7px
   against leaves. The logo's greens are #285331 and darker — none of them can
   do this job on a photograph of a garden, which is the whole reason the
   original note chose bone. */
.hero__live {
  background: var(--bone);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.22);
}
.hero__live::after { border-color: rgba(255, 255, 255, 0.85); }
.hero__book:hover .hero__live { background: #FFFFFF; }

/* ---- 2. the scrim is tuned to a photograph that is gone ---------------- */
/* Every number in the scrim below was set against a different image. The
   comments in §3 describe hero-1 as "a properly exposed blue-hour photograph
   that is simply dark, with lit windows already at the top of the range" —
   and that file was replaced on 2026-09-10 by a golden-hour lakefront frame,
   mean luminance 86/255, sun breaking through the canopy. A veil built for
   dusk, laid over a frame shot in sun, darkens the third of the image with
   the most light in it; measured under the headline the photograph was
   arriving at the eye as a dark green rectangle.
   Lighter on the left where the headline sits, and spent by 78% instead of
   88%, so the flare and the water stay a photograph.

   THE TOP-DOWN LAYER IS GONE. It existed to "give the logo side a floor now
   that the header paints nothing" — but the header does paint: there is a
   gradient on `.site-head .nav` over a photo hero. So the two were stacking,
   and the sun was being erased twice. One veil at the top, and it belongs to
   the header, which is the element that needs it.

   AND THE PHOTOGRAPH NOW HAS A BOTTOM EDGE. It was cut off with a ruler and
   butted straight into the city ticker below it, which is a hard horizontal
   seam across the full width of the page at the exact moment the first screen
   ends. The last layer fades the frame into `--paper-2` — the ticker's OWN
   ground, so the bottom 7% of the photograph IS the strip under it and no new
   colour enters the page. */
@media (min-width: 900px) {
  .hero__media::after {
    background:
      /* the bottom edge is FIRST because first paints on top — it has to
         resolve to exactly --paper-2 at y=100% or the seam returns as a
         tinted smudge instead of a hard line */
      linear-gradient(to top,
        var(--paper-2) 0%,
        rgba(29, 34, 38, 0.62) 7%,
        rgba(29, 34, 38, 0.22) 17%,
        rgba(29, 34, 38, 0.00) 30%),
      /* THE CORNER DIM CARRIES THE PHONE NUMBER NOW, and it was measured.
         After halving the header veil and deleting the top-down layer, the
         nav row reads at 8.6:1 mean — but the worst pixel behind the phone
         number is 1.36:1, because the sun flare in this frame sits directly
         behind it. A corner vignette is the right instrument rather than
         another full-width band: it darkens only the ~18% of frame the number
         and the button occupy and dies out diagonally, so it reads as lens
         falloff instead of as a header, and the canopy and flare in the middle
         of the frame keep every stop of the light we just gave them back. */
      radial-gradient(ellipse 62% 30% at 100% 0%,
        rgba(8, 12, 16, 0.46) 0%, rgba(8, 12, 16, 0.22) 45%, rgba(8, 12, 16, 0.00) 100%),
      linear-gradient(90deg,
        rgba(12, 18, 16, 0.52) 0%, rgba(12, 18, 16, 0.40) 26%, rgba(12, 18, 16, 0.18) 50%,
        rgba(12, 18, 16, 0.04) 66%, rgba(12, 18, 16, 0.00) 78%) !important;
  }
}

/* ==========================================================================
   THE CHAPTER NUMERAL WAS DRAWN THROUGH THE COPY

   The counter is a good device and the size was a collision. It is absolutely
   positioned top-right of the content block at clamp(3rem, 5vw, 5.5rem) —
   which is exactly where a section head's right column lives. Measured at
   1456px, "02" was struck straight through "One contractor over all of them."
   A numeral that size is either the loudest thing in the section or a defect,
   and it was never meant to be the loudest thing.

   Set the way a drawing sets a sheet number instead: small, solid, sitting in
   the margin ABOVE the block rather than inside it, where it is legible,
   unmistakably an index, and cannot reach any text. Same corner, same
   counter, same face as the rest of the page's labels — a twentieth of the
   area, and the outline trick retired with it, because a hairline outline is
   what you reach for when a numeral is too big to fill.
   ========================================================================== */
@media (min-width: 1180px) {
  .section:not(.section--tight):not(.cta-band) > .wrap::before {
    top: -2.4em;
    font-size: 0.82rem;
    font-weight: 700;
    letter-spacing: 0.18em;
    color: var(--faint);
    -webkit-text-stroke: 0;
  }
}

/* ==========================================================================
   THE FIRST SCREEN

   Everything here is about one thing: the top of this page was assembled from
   decisions taken at different times, and it shows in the measurements rather
   than in any single rule.
   ========================================================================== */

/* ---- 1. one datum down the left ---------------------------------------- */
/* Measured at 1440: the roundel's left edge sits 56px from the viewport, and
   the headline's "A" sits at 136px. At 1920 the headline moves to 376px while
   the roundel stays at 56px. They are two grids — the header is full-bleed to
   the gutter, the hero copy is inside a centred `.wrap` — built months apart,
   and the eye reads the resulting stagger as carelessness even when it cannot
   name it. A hero's left edge is the strongest line on a page; there should
   be one of it.

   The headline also grows. It was clamped to 4.9rem under a 216px masthead,
   so the logo was the largest thing on the first screen of a page whose job
   is to say what the company builds. At 1440 the copy column is 956px and the
   longest line measures ~805px, so it does not wrap.

   `!important` on .hero__copy is not a shortcut: the rule it has to beat
   (the 1081px block above) is itself `!important`. The h1 selector is (0,3,1)
   and clears the base rule at (0,2,0) on specificity alone. */
@media (min-width: 1081px) {
  .hero--photo .hero__inner {
    max-width: none !important;
    margin-inline: 0 !important;
    padding-inline: var(--gutter) !important;
  }
  .hero--photo .hero__copy { max-width: min(1080px, 72%) !important; }
  /* THE HEADLINE SIZE WAS RAISED HERE AND REVERTED. clamp(3rem, 8vw, 7.4rem)
     put it at 115px at 1440. Owner's call on sight: too big. It keeps the
     shared 56px left edge from the rule above — that part was not the problem
     — and goes back to the size it shipped at, clamp(2.4rem, 5.1vw, 4.9rem),
     which is set on `.hero--photo h1` a few thousand lines up and applies on
     its own now that nothing here overrides it. */
}

/* A 34px blur at 0.5 alpha behind 115px type is not a shadow, it is a grey
   cloud around every letter. The tight-plus-wide pair is the one this file
   already proved for the nav glyphs. */
.hero--photo h1 {
  text-shadow:
    0 1px 2px rgba(10, 14, 18, 0.42),
    0 4px 26px rgba(10, 14, 18, 0.30);
}

/* ---- 2. the nav keeps its contrast from the glyphs, not from a veil ----- */
/* This is what pays for halving the header gradient. A shadow on the glyph
   darkens the ~2px around each letter, which is the only area that decides
   whether the letter is legible; a background veil darkens 216px of
   photograph to achieve the same thing. Same principle the hero headline uses
   directly above. */
@media (min-width: 900px) {
  body:has(.hero--photo) .site-head:not(.is-stuck) .nav__links a,
  body:has(.hero--photo) .site-head:not(.is-stuck) .nav__phone {
    text-shadow:
      0 1px 2px rgba(10, 14, 18, 0.62),
      0 2px 14px rgba(10, 14, 18, 0.45);
  }
}

/* ---- 3. the rule under the nav: NOTHING ADDED HERE, ON PURPOSE ---------- */
/* Worth writing down, because it looked like a defect twice and is not one.
   The 2px rule under the masthead is built from
   `linear-gradient(90deg, var(--mark) 0 ..., rgba(242,239,231,0.18) ...)`, and
   that cream is indeed a leftover from a palette this site no longer has. But
   it has never been on screen: `--mark` was shadowed to `58px` inside the
   masthead (see the note on `.site-head .nav`), which made the gradient
   invalid, and the desktop block above sets the pseudo-element to
   `display: none` anyway, because the deliberate decision here is that AT THE
   TOP OF A PHOTOGRAPH THERE IS NO BAR AT ALL. That decision is right and is
   the reason this first screen does not look like a template. Drawing a tick
   under the nav would put a header mark back over the picture to fix a line
   that was never drawn.
   What the --mark repair DOES fix is the same rule on the stuck bar and on
   every interior page, where it is supposed to paint and never has. */

/* ---- 4. the masthead footing: TRIED, BROKE THE HEADER, REVERTED --------- */
/* `align-items: end` with an interpolated bottom padding was proposed here,
   on the argument that five 13px links centred in a 216px bar read as a 70px
   nav somebody stretched. The argument is fine and the arithmetic is fatal:
   the roundel is 196px tall in a 216px bar, so bottom-aligning the row puts
   the LOGO'S TOP EDGE AT y=0 — jammed flush against the top of the screen
   with no air above it at all — and drags the links and the call-to-action
   down to the bottom rail at the same time. Measured: logo top 0, bottom 196,
   links and CTA bottom 196, against a centred layout's 10 and 206.
   `align-items: center` is not a leftover here. When one child is 91% of the
   bar's height, centring IS the footing — there is no slack to distribute.
   Left as it was, and written down so it is not re-proposed. */

/* ==========================================================================
   THE PHOTOGRAPH BANDS — a shape that is chosen, and a caption that is not
   painted over the picture

   Three things were wrong here and the second was hiding the first.
   ========================================================================== */

/* ---- 1. every band had a hole in the bottom of it ---------------------- */
/* MEASURED, not inferred: picture box 552.2px, image 552.2px, image
   `margin-top: -60.7px`. The image is pulled up out of its own frame and the
   bottom 56px of every band — a tenth of the photograph — is empty, painting
   the page ground.

   It happened because two rules half-fought. A later top-level rule set the
   image height back to the box height, beating the `calc(--band-h * 1.22)`
   that made the overflow work (that one lives inside an @supports nest, which
   adds no specificity) — but the matching negative margin-top was NOT
   overridden, so the offset survived without the extra height it offsets.

   Nobody saw it because the caption's gradient reaches rgba(10,8,7,0.94) at
   exactly that edge and paints the empty strip black. A bug concealed by a
   second bug, and fixing the caption first would have exposed it. */

/* ---- 2. a band's shape stopped depending on the window's height -------- */
/* The band's width is fixed — min(100% - 2*gutter, 1280px) — and its height
   was 68vh. So the ASPECT RATIO of every photograph on this page was a
   function of the reader's window chrome: 2.09:1 on a 900px-tall display,
   1.64:1 on a 1200px one. You cannot art-direct a picture whose shape the
   browser toolbar decides. Height now derives from width, so the crop is the
   same composition for everyone: 16:9 for the standard band, 3:2 for the tall
   one — deliberately off-meter, as the original intent recorded, but now as a
   ratio rather than as an accident. The one viewport term left is the 86vh
   cap, which only engages on a short window.

   --band-h is also declared out here rather than only inside the @supports
   nest, where it was invisible to Safari-without-view() and to anyone with
   reduced motion — both of whom were reading `height: var(--band-h)` as
   nothing at all. */
@media (min-width: 701px) {
  /* LETTERBOX, NOT 16:9. The first version of this rule used 0.5625 — a 16:9
     frame of a 1280px band, which is 720px tall, and with the caption plate
     under it each band came to 826px. Three of those in a row is 2,478px of
     near-identical full-width slab, and it was TALLER than the 612px it
     replaced. Fixing the geometry is not the same as choosing a shape.

     2.4:1 is the choice. A finished yard is a wide subject — lawn, paving and
     a planting line running across the frame — and a letterbox crop says the
     photograph was framed for that, where a 16:9 box says the camera was
     pointed at it. It also brings the band to 532px, below the original, so
     three of them stop dominating the scroll. The crop biases slightly down
     (52%) because what is being sold is the ground, not the sky.

     The tall variant goes to 2:1. Wider than the standard band would be a
     contradiction, so it earns its difference by being 107px taller rather
     than by being a different shape. */
  .band--photo {
    --band-w: min(100vw - 2 * var(--gutter), var(--max));
    --band-h: calc(var(--band-w) / 2.4);
  }
  .band--photo--tall { --band-h: min(calc(var(--band-w) / 2), 78vh); }
  .band--photo img { object-position: 50% 52%; }
  .band--photo picture { display: block; overflow: hidden; height: var(--band-h); }
  .band--photo img { width: 100%; height: var(--band-h); object-fit: cover; margin-top: 0; }

  /* Only where the drift actually runs does the image get the extra height
     and the offset that pays for it — together, or not at all. */
  @supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
      .band--photo img {
        height: calc(var(--band-h) * 1.22);
        margin-top: calc(var(--band-h) * -0.11);
      }
    }
  }
}

/* ---- 3. the caption comes out of the frame ----------------------------- */
/* The caption was absolutely positioned at the bottom of the photograph with
   `padding-top: clamp(4rem, 12vw, 9rem)` over a FOUR-STOP gradient ending at
   0.94 black — measured at 144px of padding, and covering roughly 42% of the
   band on a desktop and nearer two thirds on a phone. On top of that the
   image underneath was being run through brightness(0.82) saturate(0.86).

   A four-stop gradient is what you reach for when type is fighting a picture,
   and the answer to that fight is to stop having it. The caption moves onto a
   solid plate below the frame: the photograph is uncovered end to end, and
   the contrast stops being "against the worst pixel of whatever foliage is
   under the word" and becomes a number — 15.2:1.

   THE TYPE IS UNCHANGED. The eyebrow setting on the second line — uppercase,
   0.14em, steel, on its own line — is the one thing in this band that already
   looked commissioned. Only the box moves. */
/* NO FRAME, NO PLATE. The first version put a 1px border and a --paper-2 fill
   around the whole thing, which turns a photograph into a card: the eye reads
   the box first and the picture second, and three boxed slabs down one page is
   a component library, not a photo essay. A photograph on a dark page needs no
   frame — the page IS the mount. The caption is then just a caption: text sat
   under a picture, the way a plate is captioned in print.

   ONE LINE, NOT A BLOCK. It was a 104px full-width grid — eyebrow on its own
   row, statement under it, both ranged across 1278px. At that width the two
   lines have no relationship to each other; they read as two separate
   headings. Set on one baseline with a rule between them, the eyebrow becomes
   what it actually is: the label on the statement. It wraps to two lines below
   760px, where the measure runs out. */
.band--photo { background: none; border: 0; }
.band--photo figcaption {
  position: static;
  background: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.55rem 1rem;
  padding: clamp(0.85rem, 1.5vw, 1.15rem) 0 0;
  color: var(--ink);
  font-size: clamp(1.05rem, 1.7vw, 1.45rem);
  line-height: 1.2;
}
/* The eyebrow keeps its own type — uppercase, tracked, steel — and picks up a
   hairline so the two halves are visibly one caption rather than two phrases
   that happen to be adjacent. */
.band--photo figcaption span {
  margin-bottom: 0;
  padding-right: 1rem;
  border-right: 1px solid var(--line-2);
}
@media (max-width: 760px) {
  .band--photo figcaption span { border-right: 0; padding-right: 0; flex-basis: 100%; }
}
/* The image no longer needs to be dimmed to carry type it is no longer
   carrying. */
.band--photo img { filter: none; }

/* ==========================================================================
   THE CATALOGUE

   Four titles and nineteen words spread over ~3,600px of scroll is a
   brochure. A catalogue is indexed, and it has specifications.
   ========================================================================== */

/* ---- 1. the gutter was 8.8px ------------------------------------------- */
/* Measured at 1440: an 823px photograph, an 8.8px gap, then a 336px column
   holding a 44.8px heading. That is not a two-column layout, it is a
   collision. The cause is a specificity fight nobody won on purpose —
   `#build .card { gap: 0.55rem }` carries an ID, so it beats the intended
   `.bp-grid .card { gap: clamp(1.5rem, 3.5vw, 3.5rem) }` at every width. The
   file's own comment further down measured the symptom (the photo reaching
   823px) without naming the cause.
   Matched on the id rather than beaten with !important, and the gap SPLITS:
   the single clamp was also serving as the row gap between a heading and its
   own sentence, which is why the stacked layout had 56px between the two. */
#build.bp-grid .card {
  gap: 0;
  column-gap: clamp(2.25rem, 4.4vw, 4.5rem);
  row-gap: 0.75rem;
  /* !important, and only because it has to be. `.card, .feature, .stat,
     .compare__col { border-top: 1px ... !important; padding-top: 1.35rem
     !important }` sets both, and its comment explains itself honestly: "a
     rule above each block rather than a box around it: in a grid that reads
     as a column rule and a row rule at once, which is what organises a set."
     That is true OF A GRID. `#build` is no longer a grid — `.bp-grid .grid-3`
     is `display: block`, so these are four full-width rows — and a 1168px
     hairline above each photograph does not organise a set, it draws a table.
     Scoped to #build so every other .card, .feature and .stat on the site
     keeps the rule it was written for. */
  border: 0 !important;
  padding: 0 !important;
}
/* The steel flare on hover taught the reader that a row with no link in it is
   clickable. */
#build.bp-grid .card:hover { border-top-color: transparent; }

/* ---- 2. the split engaged 300px too early ------------------------------ */
/* The two-column rule started at 900px, where it produced a 257px copy column
   against a 30.6px heading — the worst case was the entry point. It now waits
   until there is room for it. */
/* ONE GRID ITEM PER COLUMN, WHICH IS THE ONLY VERSION OF THIS THAT IS SAFE.

   The copy used to be four loose children — index, heading, paragraph, tag
   row — placed into the second column by hand, with the picture told to span
   the rows beside them via `grid-row: 1 / -1`. That is broken, and it broke
   visibly: **`-1` resolves against the EXPLICIT grid**, and this card has no
   `grid-template-rows`, so there is no explicit row line to count back from.
   The span collapsed to a single row. The picture sat in row 1, the index was
   auto-placed into row 1 of the second column — a row as tall as the whole
   photograph — and the heading, paragraph and tags were pushed into rows 2, 3
   and 4, which is to say BELOW the image. Reported exactly that way: "all
   section text is being squished down below its images."

   Hand-placing children into grid cells only works while nobody adds a child.
   Two children now, one per column, and the copy lays itself out as a normal
   flow. Nothing to keep in sync, and a fifth element can be added tomorrow
   without any of this being revisited. DOM order is unchanged — picture, then
   copy — so a screen reader still reads every row the same way. */
#build.bp-grid .card__copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
}
@media (min-width: 900px) and (max-width: 1179.98px) {
  /* Below 1180 the two-column split gives a 257px copy column against a 30px
     heading — the worst case was the entry point — so it stacks instead. */
  #build.bp-grid .card { grid-template-columns: minmax(0, 1fr); }
  /* AND THE ROW GAP HAS TO GROW WHEN IT STACKS. `row-gap: 0.75rem` is right
     between a photograph and a copy column standing BESIDE it, where it never
     applies. Once they stack, that same 12px is the entire distance between
     the bottom of a photograph and the heading under it, which reads as the
     text falling out of the picture. */
  #build.bp-grid .card { row-gap: clamp(1.4rem, 3vw, 2.25rem); }
  #build.bp-grid .card .card__shot,
  #build.bp-grid .card .card__media,
  #build.bp-grid .card .card__copy { grid-column: 1; grid-row: auto; }
  #build.bp-grid .card p { max-width: 58ch; }
}
@media (min-width: 1180px) {
  #build.bp-grid .card                   { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); align-items: stretch; }
  #build.bp-grid .card:nth-of-type(even) { grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr); }
  /* Explicit placement for two items, so the alternating rows swap sides
     without `order` and the DOM stays picture-then-copy. */
  #build.bp-grid .card .card__shot,
  #build.bp-grid .card .card__media      { grid-column: 1; grid-row: 1; }
  #build.bp-grid .card .card__copy       { grid-column: 2; grid-row: 1; }
  #build.bp-grid .card:nth-of-type(even) .card__shot,
  #build.bp-grid .card:nth-of-type(even) .card__media { grid-column: 2; }
  #build.bp-grid .card:nth-of-type(even) .card__copy  { grid-column: 1; grid-row: 1; }
  /* 40ch is a real constraint now. The existing 34ch never applied, because
     the column it was constraining was 336px wide. */
  #build.bp-grid .card p                 { max-width: 40ch; }
}

/* ---- 3. the index and the specifications ------------------------------- */
/* The paragraphs and tag rows are copied VERBATIM from services.html, where
   the same four categories already carry approved copy. Two pages describing
   the same trade in different words is how a company ends up contradicting
   itself; this way the homepage cannot drift from the services page. */
/* Takes the label tokens rather than its own values — this rule carries an
   ID, so it outranks the label system and would otherwise be the one place
   that quietly opted out of it. */
#build.bp-grid .card .cap-idx {
  font-family: var(--body);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--mark);
  margin-bottom: 0.65rem;
}
#build.bp-grid .card .cap-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 1.1rem;
}

/* ---- 4. one crop, chosen ----------------------------------------------- */
/* 4:3 is what the camera was set to, not a decision — and four identical 4:3
   crops at identical width, alternating sides, is a template applied four
   times. 3:2 takes 11% off the vertical, and the 45% bias spends that on sky
   rather than on the built ground, which is the part the photograph is of. It
   also sits between the bands' 16:9 and a square, so the page reads as one
   photographic system rather than as two. */
#build.bp-grid .card__shot,
#build.bp-grid .card__shot img {
  aspect-ratio: 3 / 2;
  object-position: 50% 45%;
}

/* ==========================================================================
   SECTION MOTION — the moves the page already had, made to work

   Everything below is scoped at or under the statement band. The hero and the
   masthead are deliberately untouched.
   ========================================================================== */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* ---- 1. the curtain is the wrong colour ---------------------------- */
    /* The panel that wipes off each photograph is painted `var(--mark)` —
       #9EA9B3, the steel from the logo's ring. On a #161A1D page that is a
       near-white rectangle sitting over an 823x617 photograph and sliding
       away, which does not read as a curtain being drawn: it reads as an
       image placeholder, the grey box a page shows before a picture has
       downloaded. The whole point of the move is that something physically
       UNCOVERS the work — so the panel has to be the surface it is uncovering
       from, which is the ground each element sits on. Then there is no
       rectangle at any point, only a photograph appearing from under the page.

       The catalogue sits on --paper; the bands, since their caption moved out
       of the frame, now sit on their own --paper-2 plate. */
    .bp-grid .card__shot::after  { background: var(--paper); }
    .band--photo picture::after  { background: var(--paper-2); }

    /* A one-pixel bleed top and bottom. At a fractional device pixel ratio the
       panel and the frame round differently and a hairline of the photograph
       shows above the curtain before it moves. */
    .bp-grid .card__shot::after,
    .band--photo picture::after { inset: -1px 0; }

    /* ---- 2. a tall band cannot use an `entry` range -------------------- */
    /* `entry` measures from the element's top edge crossing the bottom of the
       viewport to the element's BOTTOM edge crossing it — so for anything
       taller than the viewport the phase does not complete until the element
       is entirely through, and `entry 58%` lands well below the fold. The
       bands are 16:9 and 3:2 of a 1280px frame: 720px and 853px. The reader
       reaches the band, sees a solid plate, and has to keep scrolling past it
       before it opens. `cover` measures the whole passage instead, so the
       photograph is uncovered while it is being looked at. */
    .band--photo picture::after { animation-range: cover 2% cover 30%; }

    /* ---- 3. stop animating a filter on a 1920px image ------------------ */
    /* `band-light` ramps brightness and saturation across the entry of every
       band, and its own comment says the quiet part out loud: "It is not
       visible as an effect." It is, however, a filter on a full-bleed
       photograph recomputed every frame across 700px of scroll — the most
       expensive move on the page, by admission invisible, and it also fought
       the `filter: none` the bands now need after the caption came out of the
       frame. The drift stays; the filter goes. */
    .band--photo img {
      animation: band-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }

    /* (The eyebrow rule already draws itself before its heading opens, a few
       hundred lines above. Not repeated here — a second copy at a different
       range is how two rules end up fighting over one pseudo-element.) */

    /* ---- 4. the catalogue's copy arrives with its photograph ----------- */
    /* The index, the specification row and the paragraph are new to this
       layout and had no move at all, so they appeared instantly beside a
       photograph that was still being uncovered. Same ladder as the heading
       above them, one rung apart, so a row reads as one object arriving. */
    .bp-grid .card .cap-idx   { animation: card-in linear both; animation-timeline: view(); animation-range: entry 10% entry 50%; }
    .bp-grid .card .cap-tags  { animation: card-in linear both; animation-timeline: view(); animation-range: entry 16% entry 56%; }
  }
}

/* ==========================================================================
   THE LAST TWO SCREENS
   ========================================================================== */

/* ---- the footer stops being the same slab as the CTA band -------------- */
/* `.cta-band` and `.site-foot` are both --slate-deep (#091A12), adjacent in
   the DOM, with the same radial wash — so the bottom two screens of the page
   are one undifferentiated block and the reader cannot tell the close from
   the colophon. The page already owns the instrument for this: a topographic
   contour seam, used at the top of the statement band and the top of the CTA.
   Telling detail — a `transition: none` rule further up already NAMES
   `.site-foot::after`, so somebody intended this seam and it was never
   defined. Now it is, by extending the three existing selectors rather than
   pasting a fourth copy of a 6KB inline SVG. The footer already carries
   `position: relative; isolation: isolate` and enough top padding that a 60px
   seam sits entirely inside it. */

/* ---- the footer's phone number is the footer's job --------------------- */
/* (0,2,0) and it MUST land after `.site-foot a { color: var(--bone-mute) }`,
   or the colour is lost to it. No hover rule needed — the footer's existing
   `a:hover:not(.btn)` already covers it. */
.site-foot .foot-call {
  display: inline-block;
  margin-bottom: 0.85rem;
  font-family: var(--display);
  font-size: clamp(1.15rem, 2.2vw, 1.5rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--bone);
}

/* Below 900 the card was never two columns, so the same stacked gap applies —
   and it is the width where the photograph and its heading are closest
   together on screen. */
@media (max-width: 899.98px) {
  #build.bp-grid .card { row-gap: clamp(1.25rem, 4vw, 2rem); }
}

/* ==========================================================================
   THE PROCESS BECOMES A SCHEDULE, AND THE OBJECTION BLOCK STOPS LEAVING A HOLE
   ========================================================================== */

/* ---- 1. the steps are columns under a rule again ----------------------- */
/* Restating §37's decision now that the sweep above no longer contradicts it.
   Text on the page ground rather than on a mid-tone fill also buys contrast
   for free: --stone goes 5.7:1 -> 7.1:1 and --mark 4.5:1 -> 7.3:1. */
.steps-strip li {
  background: none;
  border: 0;
  border-top: 1px solid var(--line-2);
  padding: 1.1rem 0 0;
}

/* ---- 2. five equal columns become a dated rail ------------------------- */
/* Five columns of equal weight is a marketing ladder: it ASSERTS a process
   without showing one. A schedule shows one — a single rail across the
   section with a milestone marked on it where each stage begins, which is how
   a construction programme is actually drawn.
   STEEL, NOT GREEN, and this is measured rather than taste: --mark on
   --line-2 over --paper is 3.3:1 and reads as progress along a track, while
   --volt on the same track is 1.3:1 and would be invisible. The rule this
   file already states — an action is green, a mark is steel — and a milestone
   is a mark.
   Only above 1040px. Below that the strip wraps to two columns and a
   horizontal rail across it would mean nothing, so the hairline stands. */
@media (min-width: 1040px) {
  .steps-strip { position: relative; padding-top: 2.6rem; }
  .steps-strip::before,
  .steps-strip::after {
    content: '';
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 2px;
  }
  .steps-strip::before { background: var(--line-2); }
  /* Default scaleX(1) — fully drawn. So a reduced-motion reader and a browser
     without scroll timelines get a finished rail rather than an empty track
     waiting for an animation that will never run. */
  .steps-strip::after {
    background: var(--mark);
    transform: scaleX(1);
    transform-origin: 0 50%;
  }
  .steps-strip li { border-top: 0; padding: 0; }
  .steps-strip__num {
    position: relative;
    padding-left: 1.05rem;
    font-variant-numeric: tabular-nums;
  }
  /* The milestone. The ring is the page ground, so the dot sits ON the rail
     with a clean bite out of it rather than beside it. */
  .steps-strip__num::before {
    content: '';
    position: absolute;
    left: 0; top: calc(-2.6rem - 3px);
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--mark);
    box-shadow: 0 0 0 4px var(--paper);
  }
}
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    @media (min-width: 1040px) {
      .steps-strip::after {
        animation: draw-rail cubic-bezier(0.33, 0.96, 0.42, 1) both;
        animation-timeline: view();
        animation-range: entry 30% cover 50%;
      }
      @keyframes draw-rail { from { transform: scaleX(0); } to { transform: scaleX(1); } }
    }
  }
}

/* ---- 3. the objection block ran three different measures --------------- */
/* Inside a 1280px wrap the heading stopped at 736px and the list at 960px,
   leaving a 320px dead gutter down the right of the whole block and a heading
   that ended 224px before the list under it. Three measures in one block is
   not a rhythm, it is three decisions taken separately. The list goes wide,
   the heading follows it at a slightly shorter line so it still reads as a
   heading, and the lede keeps its reading measure — a paragraph is the one
   thing that must not go wide. */
.objection__intro { max-width: 52rem; }
.objection__list  { max-width: 72rem; }

/* ---- 4. and it was the one section that never moved -------------------- */
/* Every section reveal keys off `.section-head`, `.statement` or `.cta-band`.
   This block's heading lives in `.objection__intro`, which is in none of those
   lists, and the selector that WAS listed — `.objection__item` — was replaced
   by the checklist on 2026-09-15 and now exists only in a frozen snapshot. So
   the page animated its way down and then went inert for one full section. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .objection__intro h2 {
      animation: unmask linear both;
      animation-timeline: view();
      animation-range: entry 6% entry 46%;
    }
    .objection__intro .eyebrow::before {
      transform-origin: left center;
      animation: draw-rule linear both;
      animation-timeline: view();
      animation-range: entry 6% entry 40%;
    }
    .objection__list li {
      animation: card-in linear both;
      animation-timeline: view();
      animation-range: entry 8% entry 58%;
    }
    .objection__list li:nth-child(2) { animation-range: entry 13% entry 63%; }
    .objection__list li:nth-child(3) { animation-range: entry 18% entry 68%; }
    .objection__list li:nth-child(4) { animation-range: entry 23% entry 73%; }
  }
}

/* ==========================================================================
   THE REVIEWS BAND

   Three columns of unboxed grey prose with nothing marking where one review
   ends and the next begins. The band carrying the strongest asset this
   business has — real five-star reviews with names on them — was the flattest
   thing on the page.

   It is NOT boxed to fix that. The page now has one organising device, used
   by the catalogue and by the process schedule: a column under a rule. The
   reviews take the same device, so the third use of it reads as a system
   rather than as a third idea.
   ========================================================================== */

/* !important on both, and it is the existing pattern rather than a shortcut.
   The de-frosting block a few thousand lines up strips glass off
   `.card, .feature, .stat, .compare__col, .quote, .quotebox__body` with
   `border: 0 !important; padding-inline: 0 !important`, and then the very next
   rule hands the rule-above BACK to `.card, .feature, .stat, .compare__col`
   with `border-top: ... !important` — everything in that list except `.quote`.
   Reviews were the one set left without the device the rest of the page
   organises itself with. This is that same rule, for the one selector it
   missed. (A separate `.quote { padding: 0 !important }` is why the top
   padding needs it too.) */
.quote-grid > .quote {
  border-top: 1px solid var(--line-2) !important;
  padding-top: 1.25rem !important;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

/* The stars stop being a 16.8px afterthought. They are the rating, they are
   the first thing the eye should find in the column, and they sit directly
   under the rule where the index sits in every other column on this page. */
.quote-grid .quote__stars {
  font-size: 1.35rem;
  letter-spacing: 0.14em;
  color: var(--mark);
  line-height: 1;
}

/* The quotation is the content of the column, so it gets the weight. It was
   set at the same size and colour as the paragraph in every other section,
   which is how a testimonial ends up reading as body copy. */
.quote-grid .quote blockquote {
  font-size: clamp(1.02rem, 1.25vw, 1.18rem);
  line-height: 1.55;
  color: var(--ink);
  text-wrap: pretty;
}

/* The attribution is the proof, and it goes last and quiet — a name, an
   initial and a source. `margin-top: auto` pins it to the bottom of its
   column so three reviews of different lengths still line their names up. */
.quote-grid .quote figcaption {
  margin-top: auto;
  padding-top: 0.9rem;
}

/* ==========================================================================
   THE FOOTER COLUMNS

   Measured at 1440: `268px 87px 87px 92px 87px 186px` with a 72px gap. Six
   columns means five gutters, so 360px of the 1280px wrap was gap and another
   268px was the roundel — leaving 652px to be shared by five columns of
   links, at 87px each. Eight entries wrapped: "Full Backyard Builds" ran to
   three lines, "Hardscape & Masonry", "Commercial & HOA", "Free Estimate",
   "Build Standards", "Safety & Compliance", "El Dorado Hills" and "Citrus
   Heights" to two. A footer is scanned, and a column of two- and three-line
   entries cannot be scanned — the eye has to parse where each link ends.

   The gap was doing the work of a margin. 72px between columns that are
   themselves 87px wide is backwards: the gutter should be smaller than the
   thing it separates. At 2.5rem the same six columns get 164px each and the
   contact column 222px, which fits the longest link and the email address on
   one line apiece.

   The roundel comes down with it. At 268px it was the largest single element
   on the page after the hero, in the footer, where its job is to sign off
   rather than to announce. 180px still reads as the mark at arm's length.
   ========================================================================== */
@media (min-width: 900px) {
  /* The first column is 235px, not 180px, even though the roundel in it is
     180px. The column also carries a line of prose and a badge, and sizing a
     column to its widest IMAGE rather than to its widest CONTENT is how
     "Outdoor general contractor. Roseville, California." ends up broken over
     three lines next to a picture that fits fine. */
  /* THE LINK COLUMNS SIZE TO THEIR CONTENT, not to an equal share. Equal
     fractions is the wrong model here: these five columns hold four, five,
     three and eight entries of wildly different lengths, so an even split
     either wraps the long ones ("Hardscape & Masonry", "Safety & Compliance")
     or wastes a third of the short ones ("Careers", "Folsom"). `auto` tracks
     resolve to max-content when there is room and shrink toward min-content
     when there is not, which is exactly the behaviour a footer needs — and
     min-content here is a single word, so it cannot overflow. The contact
     column takes whatever is left. Tuned by measuring what actually wrapped
     rather than by picking fractions. */
  .site-foot .foot-grid {
    grid-template-columns: 235px repeat(4, auto) minmax(0, 1fr);
    gap: clamp(1.25rem, 2.3vw, 2.25rem);
  }
  .site-foot .foot-grid > :first-child img { max-width: 180px; height: auto; }
}

/* ==========================================================================
   THE ASSISTANT PANEL

   Owner, alongside the redesign brief: "this means the AI chat design can be
   improved too seems odd still sinc last edit." The "odd" is measurable.
   ========================================================================== */

/* ---- 1. five corner radii in one 384px panel --------------------------- */
/* Measured, all in the same widget, on a site where --r-sm, --r and --r-lg
   are every one of them 0px:
       .ai__panel        0px      .ai__contact-btn  12px
       .ai__input        0px      .ai__chip        999px
       .ai__send         0px      .ai__cb-go       999px
       .ai__clear      999px      .ai__msg--you    14px 14px 4px 14px
   Three separate radius vocabularies inside one pane is the single loudest
   reason it reads as bolted on: the panel obeys the site's geometry and the
   controls inside it obey two other designs.
   .ai__fab is the deliberate exception and KEEPS its 999px — a floating
   action button must never be mistakable for a panel. */
.ai__chip,
.ai__cb-go,
.ai__clear,
.ai__msg--you { border-radius: 0 !important; }

/* ---- 2. the input had zero fill contrast ------------------------------- */
/* Measured: the input's background and the panel's background were the same
   value. 1.00:1. The field was marked only by a 0.24 hairline, with a
   detached 42px green send block floating half a rem away from it — two
   outlined rectangles and a green square, which is a 2014 support widget.
   One field, with the send inside it: the input takes the mid-tone so it
   reads as a well you type into, and the button butts against it.
   outline-offset is NEGATIVE on focus, deliberately — the field now abuts
   the button and the panel is `overflow: hidden`, so an outset ring would be
   clipped along the edge that matters. The ring itself stays. */
.ai__form {
  gap: 0;
  align-items: stretch;
}
.ai__input {
  background: var(--paper-3);
  border-right: 0;
}
.ai__input:focus,
.ai__input:focus-visible {
  outline: 2px solid var(--mark);
  outline-offset: -2px;
}
.ai__send { border: 1px solid var(--volt); }

/* ---- 3. the chips had no working hover --------------------------------- */
/* rgba(9,94,65,0.05) over the panel is a colour change of under one percent,
   and because it carried !important it also killed the live dark hover
   written for it later. The chips are <button>s, so they also need a focus
   ring that is not the UA default over a translucent border. */
.ai__chip:hover { background: var(--paper-3) !important; }
.ai__chip:focus-visible { outline: 2px solid var(--mark); outline-offset: 2px; }

/* ---- 4. the panel teleported ------------------------------------------- */
/* Not one of the ten .ai__panel rules contained a transition or an
   animation: `[hidden] { display: none }` was the entire state change, so
   the pane appeared and vanished between one frame and the next while the
   launcher disappeared in the same frame. `display` and `overlay` are
   animatable with `allow-discrete`, and @starting-style supplies the
   entry state, so this needs no JavaScript at all — and on an engine
   without @starting-style the panel behaves exactly as it does today.
   WIDTH AND HEIGHT ARE NOT ANIMATED: the panel is height:auto against a
   computed cap and would fight the message-grow behaviour. */
/* THE FIRST VERSION OF THIS TRANSITIONED `display` WITH allow-discrete, AND
   IT BROKE THE LAUNCHER. Measured after closing the panel: the panel kept
   `display: flex` while `[hidden]` was set, so it went on occupying layout
   inside `.ai` and shoved the FAB from (1215, 745) to (1036, 228) — 179px
   left and 517px UP — where it stayed. Reported exactly that way: "hover
   glitches 3 inches up to left whenever i click exit ai chat".

   The mistake was reaching for `display` at all. `.ai__panel[hidden] {
   display: none }` is set by a different rule several thousand lines above,
   and `[hidden]` also carries a UA style; adding a discrete transition across
   that stack is fragile, and when it goes wrong it goes wrong as a LAYOUT
   bug, which is far worse than the missing animation it was buying.

   An entry animation instead. It runs on open, it cannot affect the closed
   state because `[hidden] { display: none }` is untouched, and the close is
   instant — which is the right trade: nobody watches a panel they have just
   dismissed, and a dismissal that is instant reads as responsive rather than
   as unfinished. */
@media (prefers-reduced-motion: no-preference) {
  .ai.is-open .ai__panel {
    transform-origin: 100% 100%;
    animation: ai-panel-in 0.24s var(--ease) both;
  }
  @keyframes ai-panel-in {
    from { opacity: 0; transform: translateY(10px) scale(0.985); }
    to   { opacity: 1; transform: none; }
  }
}

/* ==========================================================================
   ONE LABEL, ONE SCALE

   Measured on the live page: 48 distinct type treatments below the ticker,
   of which ELEVEN are "a small uppercase label" — sizes from 10.08px to
   14.08px, two weights, tracking from 0.64px to 2.04px, in four different
   colours:

     10.88 / 600 / 1.74 / steel    .eyebrow
     11.36 / 700 / 2.04 / bone-50  .foot-col h3
     12.80 / 700 / 1.79 / steel    .band--photo figcaption span
     11.20 / 700 / 1.79 / stone    .steps__label
     11.20 / 600 / 1.57 / stone    .label
     10.72 / 600 / 0.64 / stone    .tag
     10.08 / 600 / 1.01 / steel    .badge
     12.48 / 700 / 2.24 / steel    .cap-idx
     ...

   None of that variation carries meaning. It is eleven people's worth of
   "about this big, about this letter-spaced", and it is precisely what reads
   as amateur: not too much design, but no system behind it. A professional
   page uses a small number of values consistently, and the consistency is
   the thing you actually perceive as quality.

   TWO SIZES, ONE WEIGHT, TWO TRACKS, TWO ROLES:
     LABEL     names a section or a column. Steel, because a mark is steel.
     META      annotates a thing — a tag, a caption's second half. Stone.
   Everything above collapses into one of the two.
   ========================================================================== */

:root {
  --label-size:    0.7rem;
  --label-size-sm: 0.66rem;
  --label-weight:  600;
  --label-track:   0.16em;
  --label-track-sm: 0.11em;
}

.eyebrow,
.steps__label,
.site-foot .foot-col h3,
.band--photo figcaption span,
.bp-grid .card .cap-idx {
  font-family: var(--body);
  font-size: var(--label-size);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
  color: var(--mark);
}

.tag,
.badge {
  font-family: var(--body);
  font-size: var(--label-size-sm);
  font-weight: var(--label-weight);
  letter-spacing: var(--label-track-sm);
  text-transform: uppercase;
  color: var(--stone);
}

/* The footer's column heads were bone at 50% — a fifth colour for the same
   role. Steel, like every other column head on the page. */
.site-foot .foot-col h3 { color: var(--mark); }

/* ==========================================================================
   THE PHOTOGRAPHS — one system for how every picture on this page is
   mounted, revealed and touched.

   Until now each image treatment was invented where it was needed: the
   catalogue got a curtain and a 1.09 settle, the bands got a curtain and a
   drift, and neither had a frame, a hover, or any relationship to the other.
   That is the difference between a page with image effects on it and a page
   with an image system: the system is what the eye reads as craft, because
   the second photograph confirms what the first one taught it.

   FOUR BEHAVIOURS, applied to both, in this order:

     1. THE MOUNT   a hairline inset inside the frame — the picture is
                    mounted on the page rather than cut into it
     2. THE PLACE   the picture lands: scale settles to 1 as it is uncovered,
                    so it arrives rather than appears
     3. THE PASS    it drifts against the scroll, so a photograph has depth
                    relative to the words beside it
     4. THE TOUCH   hover lifts it fractionally and lights the mount

   Every one is scoped so it degrades to nothing: the mount is a plain
   hairline without motion, the place and pass need scroll timelines, the
   touch needs a hover-capable pointer.
   ========================================================================== */

/* ---- 1. THE MOUNT ------------------------------------------------------ */
/* A hairline set one pixel INSIDE the picture rather than a border around it.
   A border sits between the photograph and the page and reads as a frame
   bolted on; an inset line reads as the edge of the print itself — the same
   reason a mounted photograph has a bevel rather than a box. It also survives
   the curtain: the panel that wipes the picture off is a sibling, so the
   mount is drawn before the photograph is under it, and the frame is set
   before the picture is placed in it. */
.bp-grid .card__shot::before,
.band--photo picture::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.10);
  transition: box-shadow 0.45s var(--ease);
}

/* ---- 4. THE TOUCH ------------------------------------------------------ */
/* Only where a pointer can actually hover — on a touch screen this fires on
   tap and the thing you pressed moves under your finger, which this file
   already records as a mistake it made once. */
@media (hover: hover) and (pointer: fine) {
  .bp-grid .card__shot img,
  .band--photo picture img {
    transition: transform 0.7s var(--ease), filter 0.7s var(--ease);
  }
  .bp-grid .card__shot:hover img { transform: scale(1.028); }
  .bp-grid .card__shot:hover::before,
  .band--photo picture:hover::before {
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22);
  }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* ---- 2. THE PLACE ------------------------------------------------- */
    /* 1.09 was too much — at 663px wide that is 60px of travel, which reads
       as a zoom effect rather than as a picture being set down. 1.035 is
       felt and not seen, which is the whole point. The range is tied to the
       curtain's so the picture finishes landing exactly as it finishes being
       uncovered, instead of continuing to move after the reveal is over. */
    .bp-grid .card__shot img {
      animation: shot-place cubic-bezier(0.33, 0.96, 0.42, 1) both;
      animation-timeline: view();
      animation-range: entry 6% entry 58%;
    }
    @keyframes shot-place {
      from { transform: scale(1.035); }
      to   { transform: scale(1); }
    }

    /* ---- 3. THE PASS -------------------------------------------------- */
    /* The bands already drift. The catalogue did not, so a photograph and
       the paragraph beside it moved as one flat card. A small opposing
       drift is what gives a row depth — and it is the same move the bands
       make, which is what makes the two read as one system rather than as
       two treatments. The picture is scaled slightly in its box so there is
       something to drift WITH; without that the edges would show. */
    .bp-grid .card__shot { overflow: hidden; }
    .bp-grid .card__shot img {
      animation:
        shot-place cubic-bezier(0.33, 0.96, 0.42, 1) both,
        shot-pass linear both;
      animation-timeline: view(), view();
      animation-range: entry 6% entry 58%, cover 0% cover 100%;
    }
    @keyframes shot-pass {
      from { translate: 0 -2.2%; }
      to   { translate: 0 2.2%; }
    }
    /* `translate` rather than a transform, deliberately: `shot-place` owns
       `transform` on this element, and two animations cannot both write it.
       The independent transform properties compose instead of fighting. */
    .bp-grid .card__shot img { scale: 1.05; }

    /* The mount comes up with the curtain rather than being there first —
       the frame is drawn as the picture is uncovered, not stamped on after. */
    .bp-grid .card__shot::before,
    .band--photo picture::before {
      animation: mount-in linear both;
      animation-timeline: view();
      animation-range: entry 8% entry 46%;
    }
    @keyframes mount-in {
      from { opacity: 0; }
      to   { opacity: 1; }
    }
  }
}

/* ==========================================================================
   ONE VERTICAL RHYTHM

   Measured at 1440, top/bottom padding per band:

     process, reviews, estimate ....... 130 / 130
     objection, catalogue .............  96 / 130
     THE STATEMENT ....................  72 /  72
     CTA band ......................... 164 /  68
     footer ........................... 128 /  56

   Five rhythms. The statement band runs at 55% of the page's own spacing,
   which is exactly why it reads as squished — and it is worse than the
   number suggests, because that band carries a 60px topographic seam across
   its top edge, so only about 12px of its 72px is actually clear of the
   drawing. The eyebrow very nearly touches it.

   ONE VALUE, plus an explicit allowance where a seam is drawn. The seam is a
   real object occupying the top of a band, so a band that has one needs the
   rhythm PLUS the seam, not the rhythm minus it. That is why the CTA band had
   164 at the top: somebody had already discovered this and solved it in one
   place with a magic number. Now it is the rule.

   The inner wrappers give their padding up to the band itself, so there is
   one place the spacing is set rather than two that add together.
   ========================================================================== */

:root {
  /* The page's own established rhythm, lifted from the .section rule that was
     already the most-used of the five — 130px at 1440. My first attempt at
     this used 7vw, which caps at 101px and made every section SMALLER than
     the thing it was meant to regularise. */
  --band-pad: clamp(4.5rem, 9vw, 9rem);   /* 130px at 1440 */
  --seam-h: 60px;
}
@media (max-width: 1024px) { :root { --seam-h: 50px; } }
@media (max-width: 600px)  { :root { --seam-h: 42px; } }

.section { padding-block: var(--band-pad); }

/* Bands with a contour seam across the top get the rhythm plus the seam.

   The `.band--photo + ...` halves of these selectors are not redundant. There
   is a rule for "a section that follows a full-bleed photo band gets a
   tighter top", written as `.band--photo + section` — a type selector plus a
   class, so (0,1,1), which outranks a bare `.cta-band` at (0,1,0). The CTA
   band happens to follow a photo band, so it was silently being handed the
   96px tighter top: with a 60px seam drawn across it, that left 36px clear,
   which is why the closing band looked as cramped as the statement did.
   Measured, not guessed. */
.statement,
.cta-band,
.band--photo + .statement,
.band--photo + .cta-band {
  padding-top: calc(var(--band-pad) + var(--seam-h));
  padding-bottom: var(--band-pad);
}

/* AND THE TIGHTER-TOP EXCEPTION GOES. It dates from when a band's caption was
   printed INSIDE the photograph, so the band ended on an image edge and a
   full gap after it read as a hole. The caption now sits on its own line
   below the frame, so the band already ends in type and the exception has
   nothing left to correct — it was just a sixth rhythm. One value everywhere. */
.band--photo + .section,
.band--photo + section { padding-top: var(--band-pad); }
.site-foot { padding-top: calc(var(--band-pad) + var(--seam-h)); }

/* The inner wrappers stop contributing, so the number above is the number. */
.statement__inner,
.cta-band__inner { padding-block: 0; }

/* ==========================================================================
   THE SMS DISCLOSURE — SMALLER, NOT SHORTER

   Owner, on step 3: "disclaimer feels too long? and too big text or no?"

   Too big: yes, and fixed below. Too long: it cannot be. Every clause in that
   block is one of the seven things the carrier looks for — who is texting,
   what about, how often, that rates may apply, how to stop, how to get help,
   and that agreeing is not a condition of purchase — plus the two links. The
   A2P 10DLC campaign was rejected three times over this exact paragraph
   (30924 MESSAGE_FLOW, and twice for unverifiable terms/privacy links). Cut a
   sentence and the number stops being able to text customers at all. So not
   one word moves; only the type does.

   Measured before: 13px, 20.15px line-height, 141px tall, 89 words — the same
   size as the body copy on the rest of the page, under a two-field form,
   which is why it reads as the loudest thing on the step rather than the
   quietest.

   11.5px at 1.45, which is ordinary legal microcopy, and the colour goes UP
   the scale rather than down to pay for the smaller size: --stone instead of
   --faint takes it from 5.38:1 to 6.8:1. Smaller and easier to read at once,
   which is the only version of "quieter" that is safe here — the carrier's
   standard is "clear and conspicuous", and a grey whisper would fail it just
   as surely as a missing sentence.

   THE TWO LINKS BECOME VISIBLE LINKS. They were the same colour as the
   surrounding text with no underline, so the one part of this block the
   reviewer is specifically told to follow looked like prose. Underlined and
   in bone now. This is a compliance improvement, not a style preference. */
.form-note {
  font-size: 0.72rem;
  line-height: 1.45;
  color: var(--stone);
}
.form-note a,
.form-note .inline-link {
  color: var(--bone);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.form-note a:hover { color: var(--ink); }

/* THE STEP METER HAS NOTHING LEFT TO COUNT ONCE THE FORM IS SENT.
   .form-done lives inside .quotebox__body, and the meter — "Step 3 of 3"
   with three filled dots — sits in .quotebox__head, a sibling of the body.
   Leaving it above a "Got it." is the small kind of sloppiness that reads as
   unfinished: a progress indicator for a process that is over.

   :has() is what makes this reachable at all. .form-done cannot select its
   parent's sibling any other way, and the alternative is a line of JS in two
   separate submit handlers. Supported everywhere this site is served; a
   browser without it simply keeps the old behaviour, which is the meter
   showing — a cosmetic miss, never a broken confirmation.

   !important, and NOT because specificity failed. `.quotebox__head` at ~5299
   sets `display: grid !important` to beat the `display: flex` this file
   declares for the same selector 4,000 lines earlier. An !important on a bare
   class outranks any weight a normal declaration can reach, so the only way
   out of that band is to match it. Measured, not assumed: without this the
   computed display stayed `grid`. */
.quotebox:has(.form-done.is-on) .quotebox__head {
  display: none !important;
}
