
/* ============================================================
   Sentinel Homes — Homepage
   Design tokens extracted from Figma (Sentinel Homes CQ)
   Build slice: Foundation + Navigation + Hero
   ============================================================ */
/* Brand signature font (from The Colab) */
@font-face{
  font-family:"Signerica Fat";
  src:url("home/fonts/Signerica_Fat.ttf") format("truetype");
  font-weight:400;font-style:normal;font-display:swap;
}

:root{
  /* Colour */
  --white:#ffffff;
  --cream:#f7f3ea;
  --off-white:#fdfbf9;
  --charcoal:#424040;
  --mid-grey:rgba(66,64,64,.8);
  --amber:#e8b639;
  --slate-green:#637468;
  --sage:#7a917f;
  --sage-dark:#698070;
  --warm-sand:#efe8d6;
  --terracotta:#7b4a39;

  /* Spacing scale (4px base) */
  --space-1:4px; --space-2:8px; --space-3:12px; --space-4:16px;
  --space-5:20px; --space-6:24px; --space-8:32px; --space-12:48px;
  --space-16:64px; --space-24:96px;
  --global-padding:48px;      /* desktop */
  --section-spacing:96px;
  --maxw:1440px; /* Figma 1440 grid (incl. 48px gutters); widens to 1600 on very large screens */

  /* Shadows */
  --shadow-sm:0 1px 4px rgba(0,0,0,.15);
  --shadow-lg:0 12px 48px rgba(0,0,0,.2);
  /* Build Options card hover (client round 26 item 6). The old --shadow-lg
     was one hard layer of 20% black reaching 60px below the card — too heavy
     to read as "soft and feathered", and far too tall for the clipped card
     window. Three low-alpha charcoal layers give a properly feathered falloff
     in the design system's own colour, and the negative spreads keep the
     horizontal reach to ~8px so the leftmost card loses nothing meaningful to
     the window's left clip (which must stay exactly on the content edge). */
  --shadow-card-hover:
    0 2px 4px rgba(66,64,64,.06),
    0 8px 16px -6px rgba(66,64,64,.10),
    0 18px 40px -12px rgba(66,64,64,.16);

  /* Motion (client button-studies reference — shared sitewide) */
  --ease-fluid:cubic-bezier(.16,1,.3,1);
  --ease-soft:cubic-bezier(.4,0,.2,1);
  --ease-spring:cubic-bezier(.34,1.56,.64,1);
  /* Build Options card hover (client round 25). The client asked for
     "Normal -> Hover: smooth ease-out" and "Hover -> Normal: smooth ease-in",
     so the two directions get their own curves rather than one shared one.
     Neither overshoots — no bounce, no spring, per the brief. */
  --ease-card-open:cubic-bezier(.22,.61,.36,1);
  --ease-card-close:cubic-bezier(.45,.05,.55,.95);
  /* FAQ open/close — slow, refined, no overshoot (client round 25). */
  --ease-faq:cubic-bezier(.33,1,.68,1);
  /* Local Office card switch — unhurried, no overshoot (client round 25). */
  --ease-office:cubic-bezier(.32,.72,.28,1);
  /* ONE curve for every entrance fade on the page (client round 26 items 4
     and 14: "Smooth, Subtle, Premium, Controlled" — one visual language, no
     abrupt fades, no sudden position changes). easeOutCubic: quick to become
     legible, unhurried as it settles, and no overshoot anywhere. */
  --ease-fade:cubic-bezier(.33,1,.68,1);
  /* Button lift shadows (reference: light grounds = charcoal-tinted,
     dark grounds / imagery = black-tinted) */
  --lift-shadow:0 1px 2px rgba(66,64,64,.10),0 3px 8px rgba(66,64,64,.08),0 8px 18px rgba(66,64,64,.06);
  --lift-shadow-dark:0 1px 2px rgba(0,0,0,.18),0 3px 8px rgba(0,0,0,.13),0 10px 22px rgba(0,0,0,.15);
  --lift-shadow-active:0 1px 2px rgba(66,64,64,.08);
  --underline-base:rgba(226,217,195,.5);
  --underline-base-dark:rgba(253,251,249,.22);

  /* Type families */
  --serif:"Playfair Display",Georgia,serif;
  --sans:"Montserrat",-apple-system,"Segoe UI",sans-serif;
}

/* Very large screens: content grid widens (per AJ 2026-08-13) */
@media (min-width:1700px){
  :root{--maxw:1600px}
}

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
  font-family:var(--sans);
  color:var(--charcoal);
  background:var(--off-white);
  font-size:16px;
  line-height:1.5;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  text-rendering:optimizeLegibility;
  overflow-x:hidden;
}
img{display:block;max-width:100%}
/* Scroll reveal — client round 26 items 4 and 14. This used to fade AND rise
   28px. With the section-level fade also running, that read as two separate
   effects layered on one another, and the rise is exactly the "sudden
   position change" the client asked to remove. It is now a pure opacity fade
   on the shared --ease-fade curve, so every entrance on the page — sections,
   Plan, Build Options, headings, copy, buttons — speaks one language.
   Dropping the transform also retires a long-standing conflict: the reveal's
   `transform:none` used to outrank hover transforms until it was shed. */
.js .reveal{opacity:0;transition:opacity .75s var(--ease-fade)}
/* FAQ content answers faster (client round 13 item 15) — same fade, quicker
   duration; the per-item stagger is also tightened in home.js.
   Feedback 31 Aug item 2: still too slow, so the duration drops again from
   .55s to .38s. Same fade, same --ease-fade curve, same styling - only the
   duration moves, and only for .faq. */
.js .faq .reveal{transition-duration:.38s}
.js .reveal.is-visible{opacity:1}
/* Once revealed, drop any compositor hint so text rasterizes crisply */
.js .reveal.is-visible{will-change:auto}

/* ============================================================
   SECTION ENTRANCE CHOREOGRAPHY (client round 18, items 4+5).
   `.sh-seq` = armed (pre-states below), `.is-in` = play. home.js
   adds the classes only with JS + motion allowed, and sheds them
   once the sequence has played, so the resting DOM is untouched.

   Floor Plans & Style Ideas:  background wipes up → both images
   reveal bottom-to-top (short stagger) → heading → copy → button.
   Build Options:              background wipes up → heading →
   copy → button → cards rise in.
   ============================================================ */
/* --- armed states --- */
/* Sitewide section fade (client round 25) — see home.js. A soft opacity lift
   on first entry, never a movement and never a scroll behaviour. It starts at
   0.28 rather than 0 so the section is readable the instant it appears and the
   fade cannot make the user wait; the classes are shed once played so no
   stacking context survives to affect the sticky pins or hover transforms. */
.js .sh-fade{opacity:.28}
.js .sh-fade.is-shown{opacity:1;transition:opacity .7s var(--ease-fade)}
@media (prefers-reduced-motion: reduce){.js .sh-fade,.js .sh-fade.is-shown{opacity:1;transition:none}}

/* Plan section entrance — REBUILT in client round 25. The round-18 sequence
   (background clip-path wipe, then images wiping up 24px, then text rising
   26px) has been removed entirely: the client asked for a luxurious, elegant
   fade with no movement at all. What is left is pure opacity, staggered in
   the order the client specified — section ground, plan image, heading,
   description, then the link — with no transform, clip-path, scale or slide
   anywhere in the sequence, so nothing can shift the layout or fight a
   scroll. Delays are deliberately short (the last element starts at 0.44s)
   so the reader never waits for the copy to become legible. */
.js .ideas.sh-seq::before,
.js .ideas.sh-seq .ideas__col--plans::before{opacity:0}
.js .ideas.sh-seq .ideas__media,
.js .ideas.sh-seq .ideas__plan-m{opacity:0}
.js .ideas.sh-seq .ideas__text h2,
.js .ideas.sh-seq .ideas__text p,
.js .ideas.sh-seq .ideas__text .link-btn{opacity:0}
/* Build Options entrance — REBUILT in client round 26 item 5. The round-18
   sequence wiped the background up with clip-path, then rose the copy 26px,
   then the cards: the client says that staged background → content → cards
   choreography is not wanted. What is left is the same subtle fade the rest
   of the page uses, with only enough stagger to read as one considered
   arrival rather than a list of separate effects. No wipe, no rise, no
   background swap — so nothing can shift the layout or fight the horizontal
   card scrub that lives in this section. */
.js .build.sh-seq .build__intro .h1,
.js .build.sh-seq .build__intro p,
.js .build.sh-seq .build__head .btn,
.js .build.sh-seq .build__trackrow,
.js .build.sh-seq .build__card{opacity:0}
/* --- play (transitions live on the target state) --- */
.js .ideas.sh-seq.is-in::before{opacity:1;transition:opacity .8s var(--ease-fade)}
.js .ideas.sh-seq.is-in .ideas__col--plans::before{opacity:1;transition:opacity .8s var(--ease-fade) .04s}
.js .ideas.sh-seq.is-in .ideas__media,
.js .ideas.sh-seq.is-in .ideas__plan-m{opacity:1;transition:opacity .8s var(--ease-fade) .08s}
.js .ideas.sh-seq.is-in .ideas__col--style .ideas__media{transition-delay:.14s}
.js .ideas.sh-seq.is-in .ideas__text h2{opacity:1;transition:opacity .8s var(--ease-fade) .18s}
.js .ideas.sh-seq.is-in .ideas__text p{opacity:1;transition:opacity .8s var(--ease-fade) .26s}
.js .ideas.sh-seq.is-in .ideas__text .link-btn{opacity:1;transition:opacity .8s var(--ease-fade) .34s}
.js .build.sh-seq.is-in .build__intro .h1{opacity:1;transition:opacity .8s var(--ease-fade)}
.js .build.sh-seq.is-in .build__intro p{opacity:1;transition:opacity .8s var(--ease-fade) .08s}
.js .build.sh-seq.is-in .build__head .btn{opacity:1;transition:opacity .8s var(--ease-fade) .16s}
.js .build.sh-seq.is-in .build__card{opacity:1;transition:opacity .8s var(--ease-fade) .20s}
.js .build.sh-seq.is-in .build__card:nth-child(2){transition-delay:.25s}
.js .build.sh-seq.is-in .build__card:nth-child(3){transition-delay:.30s}
.js .build.sh-seq.is-in .build__card:nth-child(n+4){transition-delay:.34s}
.js .build.sh-seq.is-in .build__trackrow{opacity:1;transition:opacity .8s var(--ease-fade) .34s}
@media (prefers-reduced-motion: reduce){
  .js .reveal{opacity:1!important;transition:none!important}
}
a{color:inherit;text-decoration:none}
ul{list-style:none}

/* ---------- Typography helpers ---------- */
.h1{font-family:var(--serif);font-weight:400;font-size:59px;line-height:1.08;letter-spacing:-.59px} /* Desktop/H1 token: -1% of 59px */
.h2{font-family:var(--serif);font-weight:400;font-size:48px;line-height:1.17}
.h3{font-family:var(--sans);font-weight:500;font-size:27px;line-height:1.27}
.h4{font-family:var(--sans);font-weight:500;font-size:18px;line-height:1.4}
.body-lg{font-size:20px;line-height:1.5}
.body{font-size:16px;line-height:1.5}
.label{font-weight:600;font-size:12px;line-height:1.2;letter-spacing:1.44px;text-transform:uppercase}
.label-lg{font-weight:600;font-size:14px;line-height:1.2;letter-spacing:1.68px;text-transform:uppercase}

/* ---------- Layout ---------- */
.wrap{max-width:var(--maxw);margin:0 auto;position:relative}

/* ---------- Buttons ----------
   Interaction ported 1:1 from the client button-studies reference
   (sentinel-demo-423.netlify.app/button-studies.html): lift on hover,
   circular fill sweep growing from the left, arrow advance, quick press
   scale on tap — shared by every pill; colours stay per-variant. */
.btn{
  position:relative;isolation:isolate;overflow:hidden;
  display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);
  /* 11px + 1px border = Figma's 12px inset with inside stroke → 40px total height */
  padding:11px 20px;border-radius:999px;
  font-family:var(--sans);font-weight:600;font-size:12px;line-height:16px;
  letter-spacing:1.6px;text-transform:uppercase;cursor:pointer;border:1px solid transparent;
  -webkit-tap-highlight-color:transparent;
  transition:transform .45s var(--ease-spring),box-shadow .55s var(--ease-fluid),
    background-color .45s var(--ease-soft),color .45s var(--ease-soft),border-color .45s var(--ease-soft);
  white-space:nowrap;
}
/* Fill sweep — reference .btn--sweep::before (circle scales from the left).
   Negative z-index inside the isolated context paints ABOVE the button's own
   background but below its text/arrow, so the label stays crisp and stable. */
.btn::before{
  content:"";position:absolute;top:50%;left:-10%;width:250%;aspect-ratio:1;border-radius:50%;
  background:var(--sweep,transparent);transform:translate(-50%,-50%) scale(0);
  transition:transform .55s var(--ease-fluid);z-index:-1;
}
.btn:hover::before,.btn:focus-visible::before{transform:translate(-50%,-50%) scale(1)}
.btn:hover,.btn:focus-visible{transform:translateY(-2px)}
.btn:active{transform:translateY(0) scale(.95);transition-duration:.11s}
.btn:focus-visible{outline:2px solid var(--amber);outline-offset:3px}
.btn .arrow{width:16px;height:16px;flex:0 0 16px;transition:transform .45s var(--ease-fluid)}
.btn:hover .arrow,.btn:focus-visible .arrow{transform:translateX(4px)}
@media (prefers-reduced-motion: reduce){
  /* Fill/colour feedback stays (appears instantly via the global duration
     collapse); all movement — lift, press, arrow advance — is removed. */
  .btn:hover,.btn:focus-visible,.btn:active{transform:none}
  .btn:hover .arrow,.btn:focus-visible .arrow{transform:none}
}

.btn--primary{background-color:var(--cream);--sweep:var(--warm-sand);color:var(--charcoal);box-shadow:0 1px 2px rgba(0,0,0,.15)}
.btn--primary:hover,.btn--primary:focus-visible{box-shadow:var(--lift-shadow-dark)}
.btn--primary:active{box-shadow:var(--lift-shadow-active)}

.btn--outline-light{border-color:var(--white);color:var(--white);background-color:transparent;--sweep:rgba(255,255,255,.14)}
.btn--outline-light:hover,.btn--outline-light:focus-visible{box-shadow:0 3px 10px rgba(0,0,0,.12)}

.btn--outline-dark{border-color:var(--charcoal);color:var(--charcoal);background-color:transparent;--sweep:var(--charcoal)}
.btn--outline-dark:hover{color:var(--cream)}
.btn--outline-dark:hover,.btn--outline-dark:focus-visible{box-shadow:var(--lift-shadow)}
.btn--white{background-color:#fff;--sweep:var(--cream);color:var(--charcoal);box-shadow:0 1px 2px rgba(0,0,0,.15)}
.btn--white:hover,.btn--white:focus-visible{box-shadow:var(--lift-shadow)}
.btn--white:active{box-shadow:var(--lift-shadow-active)}

/* Text link with amber underline (Figma "Sentinel / Button" link variant).
   Hover per reference "slide underline": the faint base rule sits underneath
   (the border), gold fills over it from the left at a constant weight. */
.link-btn{
  position:relative;
  display:inline-flex;align-items:center;gap:var(--space-2);
  border-bottom:1px solid var(--amber);padding:8px 0;
  font-weight:600;font-size:12px;line-height:16px;letter-spacing:1.6px;
  text-transform:uppercase;color:var(--charcoal);
  align-self:flex-start;white-space:nowrap;
  transition:color .2s var(--ease-soft);
}
.link-btn{border-bottom-color:var(--underline-base)}
.link-btn::after{
  content:"";position:absolute;left:0;right:0;bottom:-1px;height:1px;background:var(--amber);
  transform:scaleX(0);transform-origin:left;transition:transform .5s var(--ease-fluid);
}
.link-btn:hover::after,.link-btn:focus-visible::after{transform:scaleX(1)}
.link-btn:active{transform:scale(.97)}
.link-btn:focus-visible{outline:2px solid var(--amber);outline-offset:3px}
.link-btn .arrow{width:16px;height:16px;transition:transform .45s var(--ease-fluid)}
.link-btn:hover .arrow,.link-btn:focus-visible .arrow{transform:translateX(4px)}
.link-btn--light{color:var(--off-white);border-bottom-color:var(--underline-base-dark)}
@media (prefers-reduced-motion: reduce){
  .link-btn:active{transform:none}
  .link-btn:hover .arrow,.link-btn:focus-visible .arrow{transform:none}
}
/* Touch devices (client round 13 item 11): a tap must never leave a button
   stuck in its hover state — hover motion/fill is neutralised where hover
   isn't a real capability; the quick :active press remains as tap feedback. */
@media (hover:none){
  .btn:hover{transform:none}
  .btn:hover::before{transform:translate(-50%,-50%) scale(0)}
  .btn:hover .arrow{transform:none}
  .link-btn:hover::after{transform:scaleX(0)}
  .link-btn:hover .arrow{transform:none}
  .step__cta:hover{background:transparent;border-color:transparent;border-bottom-color:var(--amber);border-radius:0;padding:8px 2px;margin-bottom:0;box-shadow:none;transform:none}
  .step__cta:hover .arrow{transform:none}
}

/* ============================================================
   NAVIGATION  (Figma: Desktop Nav 330:1140)
   Transparent, overlaid on hero. 48px x / 32px y padding.
   ============================================================ */
.nav{position:fixed;top:0;left:0;right:0;z-index:50}
/* WordPress admin bar (logged-in users): offset every fixed chrome element by
   the bar's own height via WP's core class + CSS variable. No effect when the
   admin bar is absent (body.admin-bar missing -> rules never apply). */
body.admin-bar .nav,
body.admin-bar .nav__sheet,
body.admin-bar .mnav{top:var(--wp-admin--admin-bar--height,32px)}
@media screen and (max-width:782px){
  body.admin-bar .nav,
  body.admin-bar .nav__sheet,
  body.admin-bar .mnav{top:var(--wp-admin--admin-bar--height,46px)}
}
/* Bar background surface — its own layer so the scrim never darkens the bar
   (demo stacking: scrim < bar background < sheet < bar content). */
.nav::before{content:"";position:absolute;inset:0;z-index:1;background:transparent;
  transition:background-color .5s cubic-bezier(.4,0,.2,1),box-shadow .5s cubic-bezier(.4,0,.2,1)}
.nav__inner{
  max-width:var(--maxw);margin:0 auto;
  display:flex;align-items:center;justify-content:space-between;
  padding:32px var(--global-padding); /* Figma: 32 / 48 → 119.76px bar */
}
.nav__logo{display:inline-flex;align-items:center;line-height:0;transition:opacity .3s ease}
.nav__logo img{height:55.76px;width:auto;display:block} /* Figma logo 242.06 x 55.76 */
.nav__links{display:flex;align-items:center;gap:40px;transition:opacity .3s ease}
.nav__links a{color:var(--off-white);font-family:var(--sans);font-weight:600;font-size:12px;line-height:1.2;letter-spacing:1.44px;text-transform:uppercase;position:relative;padding-bottom:2px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transition:color .2s ease}
/* Shared sliding underline (demo .nav-underline): one 1px bar that glides
   between items on hover/open. */
.nav__underline{position:absolute;bottom:-6px;left:0;height:1px;width:0;background:currentColor;opacity:0;
  transition:transform .45s cubic-bezier(.16,1,.3,1),width .45s cubic-bezier(.16,1,.3,1),opacity .25s cubic-bezier(.4,0,.2,1);pointer-events:none}
.nav__underline.is-on{opacity:1}
.nav__links{position:relative;color:var(--off-white)}
.nav.scrolled .nav__links,.nav.panel-open .nav__links{color:var(--charcoal)}
/* Demo hover/open state: links dim to 72% (plus the sliding underline). */
.nav__links a{transition:color .2s ease,opacity .25s cubic-bezier(.4,0,.2,1)}
.nav__links a:hover,.nav__links a:focus-visible,.nav__links a.is-open{opacity:.72}
.nav__links a:focus-visible{outline:2px solid var(--amber);outline-offset:3px;border-radius:2px}

/* Right cluster: saved-plans button + contact (Figma: gap 16) */
.nav__actions{display:flex;align-items:center;gap:var(--space-4)}
.nav__save{
  position:relative;display:inline-flex;align-items:center;justify-content:center;
  width:40px;height:40px;border-radius:999px;
  background:transparent;border:1px solid var(--off-white);color:var(--off-white);
  font-weight:600;font-size:12px;letter-spacing:1.44px;text-transform:uppercase;line-height:1.2;
  transition:background-color .3s ease,color .3s ease,border-color .3s ease;
}
.nav__save-count{
  position:absolute;top:-5px;left:23px;
  display:inline-flex;align-items:center;justify-content:center;
  min-width:16px;padding:3px;border-radius:999px;
  background:var(--amber);color:var(--off-white);
  font-weight:600;font-size:10px;line-height:1.2;letter-spacing:0;
}
.nav__contact{color:var(--off-white);letter-spacing:1.44px;line-height:1.2;transition:background-color .3s ease,color .3s ease,border-color .3s ease}
.nav__toggle{display:none;color:var(--off-white)}

/* Scrolled state (per client demo): solid cream bar, charcoal recolour, and —
   like the demo (--nav-h 120px → --nav-h-scrolled 92px) — the bar COMPACTS so
   it covers less of the section underneath. The desktop compact rules used to
   sit inside the mobile media query by mistake, which left a full 120px bar
   parked over every snapped section top (the "section under the header"
   overlap AJ flagged 19-Aug). */
.nav__logo img{transition:filter .3s ease,height .35s cubic-bezier(.4,0,.2,1)}
.nav__inner{transition:padding .35s cubic-bezier(.4,0,.2,1)}
.nav.scrolled::before{background:var(--cream);box-shadow:0 4px 24px rgba(66,64,64,.08)}
.nav.scrolled .nav__logo img{filter:brightness(0) invert(.26)}
@media (min-width:1024px){
  /* 26 + 40 (actions row) + 26 = 92px — the demo's scrolled bar height. */
  .nav.scrolled .nav__inner{padding-top:26px;padding-bottom:26px}
  .nav.scrolled .nav__logo img{height:34px}
}
.nav.scrolled .nav__links a{color:var(--charcoal)}
.nav.scrolled .nav__save{background:var(--white);border-color:transparent;color:var(--charcoal);box-shadow:var(--shadow-sm)}
.nav.scrolled .nav__contact{background-color:var(--sage);--sweep:rgba(45,55,47,.28);border-color:transparent;color:var(--off-white)}
.nav.scrolled .nav__toggle{color:var(--charcoal)}

/* Menu-open state: bar goes solid cream, seamless with the sheet */
.nav.panel-open::before{background:var(--cream);box-shadow:none}
.nav.panel-open .nav__logo img{filter:brightness(0) invert(.26)}
.nav.panel-open .nav__links a{color:var(--charcoal)}
.nav.panel-open .nav__save{background:var(--white);border-color:transparent;color:var(--charcoal);box-shadow:var(--shadow-sm)}
.nav.panel-open .nav__contact{background-color:var(--sage);--sweep:rgba(45,55,47,.28);border-color:transparent;color:var(--off-white)}

/* ============================================================
   HERO  (Figma: Hero Full Bleed 271:785) — 1440 x 881
   ============================================================ */
.hero{position:relative;height:881px;overflow:hidden}
.hero__bg{position:absolute;inset:0;z-index:0}
.hero__bg img{width:100%;height:100%;object-fit:cover;object-position:center 30%}
/* graceful fallback before real photo is added */
.hero__bg{background:linear-gradient(120deg,#c9c3b4,#e7e0d2)}
.hero__overlay{
  position:absolute;inset:0;z-index:1;mix-blend-mode:multiply;
  background:linear-gradient(180deg,rgba(66,64,64,.4) 0%,rgba(66,64,64,0) 60%,rgba(53,53,53,.8) 100%);
}
/* Logo/header top shade (client round 25). The approved Figma frame carries a
   dark tinge across the very top of the hero photo that the image asset does
   not contain, so the logo and nav always sit on enough contrast. Measured off
   the Figma frame against this build at 1440x881: the darkening is neutral
   (equal R/G/B), uniform across the full width, ~31% at the top edge and gone
   by ~90px — precisely the band the logo occupies.

   It is a fixed-height band in px, not a percentage, because it belongs to the
   nav rather than to the hero's height: the hero is 100vh on desktop, so a
   percentage would grow with the viewport and drift off the logo.

   Deliberately a sibling layer of the photo rather than anything inside
   .hero__bg: the hero's slow zoom transforms the IMAGE, and a shade painted
   over the top of the section can never move, scale or jump with it. It sits
   under the nav (z-index 50) and under the hero copy (z-index 2), takes no
   pointer events, and is inset:0-anchored inside .hero{overflow:hidden}, so it
   adds no width and cannot change the section's dimensions. */
.hero::before{
  content:"";position:absolute;left:0;right:0;top:0;height:96px;z-index:1;pointer-events:none;
  background:linear-gradient(180deg,
    rgba(0,0,0,.31) 0,
    rgba(0,0,0,.25) 24px,
    rgba(0,0,0,.215) 46px,
    rgba(0,0,0,.145) 60px,
    rgba(0,0,0,.105) 72px,
    rgba(0,0,0,.025) 84px,
    rgba(0,0,0,0) 96px);
}
/* Hero background VIDEO (client round 25) — same box, same framing rules as
   the photo, no controls, no zoom. */
.hero__video{width:100%;height:100%;object-fit:cover;object-position:center 30%;display:block;pointer-events:none}
.hero__inner{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;height:100%}
/* Recognition + live chat overlay layer — same max-width geometry as
   .hero__inner; kept separate so mobile can reflow them below the photo. */
.hero__meta{position:absolute;inset:0;z-index:2;max-width:var(--maxw);margin:0 auto;pointer-events:none}
.hero__meta>*{pointer-events:auto}

.hero__copy{position:absolute;left:48px;top:531px;width:620px;display:flex;flex-direction:column;gap:32px}
.hero__headline{display:flex;flex-direction:column;gap:20px}
.hero__title{
  font-family:var(--serif);font-weight:400;color:var(--white);
  font-size:80px;line-height:86px;letter-spacing:-1px;
  text-shadow:0 2px 12px rgba(0,0,0,.25);
}
.hero__title .em{font-style:italic;display:block}
.hero__sub{
  font-size:20px;line-height:32px;color:var(--cream);
  text-shadow:0 1px 6px rgba(0,0,0,.2);
}
.hero__actions{display:flex;gap:16px;align-items:center}

.hero__recognition{position:absolute;right:48px;top:781px;width:256px;display:flex;flex-direction:column;align-items:flex-end;gap:6px;text-align:right}
.hero__recognition .rec-title{font-weight:600;font-size:10px;letter-spacing:1.6px;text-transform:uppercase;color:var(--amber)}
.hero__recognition .rec-rule{width:91px;height:2px;background:var(--amber)}
.hero__recognition .rec-desc{font-size:13px;line-height:20px;color:var(--cream)}

/* Round 20 item 1: the chat bubble is FIXED to the viewport (it used to be
   absolute inside the hero, so it scrolled away with the banner). Same
   Figma offsets (48 right / 117 up = its designed resting spot over the
   hero), visible through the whole page scroll on every device; below the
   nav layers (50/80) so open menus still cover it, and it still hides when
   the real respond.io widget is configured (rule in functions.php). */
/* Round 22 item 1: the bubble is anchored to the MAIN CONTENT CONTAINER's
   right edge — the same edge the Recognition line uses — not to the viewport.
   `100%` on a fixed element resolves against the viewport's content box (the
   scrollbar excluded), which is exactly what the centred container is centred
   in, so the two edges line up to the pixel at every width. Vertically it
   sits 40px above the Recognition text (Recognition rests 51px up and is 26px
   tall → 51+26+40 = 117... measured live: 140px). Because it is fixed in BOTH
   states there is no transition to jump: it starts in its Hero position and
   simply stays there as the page scrolls. */
.hero__livechat{
  position:fixed;right:max(48px,calc((100% - var(--maxw))/2 + 48px));bottom:140px;width:61px;height:61px;border-radius:50%;z-index:40;
  background:var(--slate-green);display:flex;align-items:center;justify-content:center;
  border:none;box-shadow:none;cursor:pointer;transition:transform .25s ease;
}
.hero__livechat:hover{transform:scale(1.06)}
.hero__livechat svg{width:35.24px;height:27.79px;color:var(--cream)}

/* ---- Full-viewport hero (client feedback #8/#9) — desktop only.
   The hero fills the viewport (Stanley St reference) and its overlay content
   anchors to the BOTTOM edge (offsets = Figma 1440×881 geometry: copy bottom
   54, recognition bottom 51, chat bottom 117), so nothing crops at 1280×800
   and the composition scales naturally to 1920+. The hero scrolls naturally
   with the page (AJ 19-Aug: the sticky "cover" pinning was reverted). */
@media (min-width:1024px){
  .hero{height:100vh;height:100svh;min-height:660px}
  .hero__copy{top:auto;bottom:54px}
  .hero__recognition{top:auto;bottom:51px}
  /* (chat bubble is viewport-fixed since round 20 — no per-hero override) */
  /* Client feedback #4: award line reads as white; long names clip cleanly
     inside the fixed frame instead of spilling out of it. */
  .hero__recognition .rec-desc{color:var(--white)}
  .rec-desc.is-multi .rec-award{max-width:100%;overflow:hidden;text-overflow:ellipsis}
}

/* ---- Hero entrance (on load — above the fold; Figma-consistent choreography:
        copy rises in sequence, recognition fades, chat pops, image fades in
        at its resting framing - opacity only, the zoom is hover-driven, see
        the hover zoom-out block below).
        fill-mode:backwards → end state = natural styles, so hovers still work. */
@keyframes hero-rise{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:none}}
@keyframes hero-fade{from{opacity:0}to{opacity:1}}
@keyframes hero-pop{from{opacity:0;transform:scale(.8)}to{opacity:1;transform:none}}
.hero__bg img{animation:hero-fade 1.2s cubic-bezier(.16,1,.3,1) backwards}
.hero__title{animation:hero-rise .7s cubic-bezier(.16,1,.3,1) .1s backwards}
.hero__sub{animation:hero-rise .7s cubic-bezier(.16,1,.3,1) .24s backwards}
.hero__actions{animation:hero-rise .7s cubic-bezier(.16,1,.3,1) .38s backwards}
.hero__recognition{animation:hero-fade .8s cubic-bezier(.16,1,.3,1) .6s backwards}
.hero__livechat{animation:hero-pop .5s cubic-bezier(.16,1,.3,1) .85s backwards}
@media (prefers-reduced-motion: reduce){
  .hero__bg img,.hero__title,.hero__sub,.hero__actions,.hero__recognition,.hero__livechat{animation:none}
}

/* ---- Hero hover zoom-OUT (client request 20-Aug, direction swapped on a
   follow-up request the same day). Interaction-driven, never ambient: at
   rest the image is STATIC at its default framing (scale 1.12 - slightly
   over-filled so a zoom-out never exposes the frame edges; this IS the
   normal resting scale, nothing animates until the pointer arrives). On
   hover it eases DOWN toward 1 on a long cinematic curve (slow zoom-out);
   when the pointer leaves, the transition retargets from whatever scale it
   is mid-flight back to the resting 1.12 just as gradually - CSS
   transitions reverse from the current value, so there is no reset jump,
   snap or animation break. Scrolling away clears :hover, so returning to
   the hero always starts from the resting framing. The effect applies ONLY
   to the background image (copy, nav, buttons untouched) and stays inside
   .hero{overflow:hidden} with the same object-position, so no layout shift
   or horizontal scroll. Gated to hover-capable fine pointers (touch
   devices keep the static image) and disabled under prefers-reduced-motion.
   Tunables: resting 1.12 / hover-out 14s / return 7s. No libraries. */
/* Client round 17: the hero behaviour above is now ONE reusable utility —
   `sh-zoom` on the section + `sh-zoom-media` on its full-width background
   wrapper. Applied to the hero (image backgrounds only), Build With
   Confidence, FAQ and the Take the First Step banner. Beautifully Built and
   Free Site Check were removed from the list in client round 25 — the client
   asked for those two images to stay completely static. It targets only
   images inside a .sh-zoom-media wrapper, so card/split-layout/content
   images can never inherit it. The wrapper clips its own image, layout and
   object-position are untouched, and the whole thing stays hover-capable
   fine-pointer + reduced-motion-off only (touch keeps static images). */
.sh-zoom-media{overflow:hidden}
@media (hover:hover) and (pointer:fine) and (prefers-reduced-motion: no-preference){
  .sh-zoom .sh-zoom-media img{transform:scale(1.12);transition:transform 7s cubic-bezier(.25,.46,.45,.94);will-change:transform}
  .sh-zoom:hover .sh-zoom-media img{transform:scale(1);transition-duration:14s;transition-timing-function:cubic-bezier(.22,.61,.24,1)}
}

/* ============================================================
   IDEAS & PLANS  (Figma 261:957) — 1440 x 853, two columns
   Left: Floor Plans (off-white) · Right: Style Ideas (cream)
   ============================================================ */
/* ---------- Ideas & Plans — Figma 261:957 (1440×853) ----------
   Left: Floor Plans on an off-white panel that bleeds to the viewport's left
   edge and ends at content x694. Right: Style Ideas on cream, image bleeding
   to the right edge. CTAs of both columns bottom-align at y≈770 by design. */
.ideas{position:relative;display:grid;grid-template-columns:694fr 746fr;align-items:stretch;max-width:var(--maxw);margin:0 auto;min-height:853px}
/* Cream ground for the whole band (body is off-white), then the off-white
   panel over the plans column — both bleed to the viewport edges. */
.ideas::before{content:"";position:absolute;top:0;bottom:0;left:calc(50% - 50vw);width:100vw;background:var(--cream);z-index:0}
.ideas__col{position:relative;display:flex;flex-direction:column;align-items:flex-start}
.ideas__col--plans::before{content:"";position:absolute;top:0;bottom:0;right:0;width:100vw;background:var(--off-white);z-index:0}
.ideas__media,.ideas__text{position:relative;z-index:1}
/* Plan drawing: Figma ink 499×425 at (101,76) = the bundled asset at 0.4995
   scale, whitespace included → 500px-wide box at (101,75). */
/* multiply = Figma's own technique on this node: the drawing's white canvas
   disappears into the off-white panel, only the ink stays. The blend lives on
   the wrapper — its z-index makes it a stacking context, which would isolate
   a blend set on the img inside. */
.ideas__col--plans .ideas__media{margin:75px 48px 0 min(101px,7.014vw);mix-blend-mode:multiply}
.ideas__col--plans .ideas__media img{display:block;width:min(500px,34.723vw);height:auto}
/* Materials photo: 693×570 at content (747,54), bleeds right. The exposed
   top/bottom/left edges fade into the background so no hard boundary shows;
   the right edge runs off the viewport and stays solid. */
.ideas__col--style .ideas__media{align-self:stretch;margin:54px calc(-1 * max(0px,(100vw - var(--maxw)) / 2)) 0 min(53px,3.681vw)}
.ideas__col--style .ideas__media img{display:block;width:100%;height:min(570px,39.584vw);object-fit:cover;border-radius:4px;
  -webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 64px,#000 calc(100% - 64px),transparent 100%),linear-gradient(to right,transparent 0,#000 64px);
  -webkit-mask-composite:source-in;
  mask-image:linear-gradient(to bottom,transparent 0,#000 64px,#000 calc(100% - 64px),transparent 100%),linear-gradient(to right,transparent 0,#000 64px);
  mask-composite:intersect}
.ideas__text{display:flex;flex-direction:column;gap:24px}
/* Floor Plans copy at x50: title top 568 (media box ends 535 + 33). */
.ideas__col--plans .ideas__text{padding:33px 48px 0 50px}
/* Style Ideas copy at x758: title top 597 — 27px over the photo's quiet
   lower edge, exactly as composed in Figma. */
.ideas__col--style .ideas__text{margin-top:-27px;padding:0 64px 0}
.ideas__text .h1{color:var(--charcoal)}
.ideas__text p{max-width:365px;color:var(--charcoal)}
.ideas__text .link-btn{align-self:flex-start}
/* Mobile-only plan graphic (hidden on desktop). */
.ideas__plan-m{display:none}
.ideas__d{display:inline}
.ideas__m{display:none}

/* ============================================================
   BUILD OPTIONS  (Figma 261:997) — 1440 x 1122, warm-sand
   Heading + intro + ALL BUILD OPTIONS pill, then card carousel
   ============================================================ */
.build{position:relative;background:var(--warm-sand);overflow:hidden}
/* the ::before carries the warm-sand ground as well as the pattern so the
   round-18 entrance can wipe the whole background up in one layer */
.build::before{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;background:var(--warm-sand) url("home/graphics.png") no-repeat center top;background-size:cover}
.build__inner{position:relative;z-index:1;max-width:var(--maxw);margin:0 auto;padding:96px 0 129.6px}
.build__head{display:flex;justify-content:space-between;align-items:center;gap:24px;padding:0 48px}
.build__intro{max-width:460px}
.build__intro .h1{color:var(--charcoal)}
.build__intro p{margin-top:16px;font-size:16px;line-height:1.5;color:var(--mid-grey)}
.build__cards{display:flex;gap:32px;padding:56px 0 0;margin:0 48px;overflow-x:auto;scrollbar-width:none;scroll-snap-type:x mandatory}
.build__cards::-webkit-scrollbar{display:none}
.build__card{flex:0 0 358px;height:493px;background:#fff;border-radius:14.4px;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 1px 4px rgba(0,0,0,.06);scroll-snap-align:start}
.build__card img{width:100%;flex:1;min-height:0;object-fit:cover}
.build__card-body{padding:24px 24px 30.6px;display:flex;flex-direction:column;gap:0}
.build__card-title{font-family:var(--sans);font-weight:500;font-size:27px;line-height:1.27;color:var(--charcoal)}
.build__card-desc{font-size:16px;line-height:1.5;color:var(--mid-grey)}
/* Build Options card hover — REBUILT in client round 25.

   The old expand animated `max-height: 0 -> 198px`. Two things were wrong
   with that. It is a guess: 198px is smaller than the taller descriptions,
   so on those cards the bottom of the link was permanently cut off — and
   because the sweep underline (.link-btn::after) is drawn 1px BELOW the
   link's own box, that edge is the very first thing an overflow:hidden
   ancestor eats. That is the broken arrow/underline the client reported.
   It also never looked smooth: easing toward a max-height larger than the
   content means the visible motion finishes early and then stalls.

   Now the panel is a grid row easing 0fr -> 1fr, so it animates to the
   description's real height on every card, whatever its length — no magic
   number, no stall, nothing clipped. The clipped child carries 2px of
   bottom padding so the underline's 1px overhang always has room, and the
   clip is released the moment the card is open, so the arrow stays whole
   for the entire transition and after it.

   Easing follows the client's brief exactly: opening is ease-OUT (the base
   rule below carries the collapse, the :hover rule carries the open), and
   closing is a gentle ease-IN. Card lift, shadow, panel and typography all
   run on the same clock so the whole card moves as one piece. */
.build__card-reveal{
  display:grid;grid-template-rows:0fr;margin-top:0;opacity:0;
  transition:grid-template-rows .8s var(--ease-card-close),opacity .55s var(--ease-card-close),margin-top .8s var(--ease-card-close);
}
.build__card-reveal-in{overflow:hidden;min-height:0;display:flex;flex-direction:column;gap:12px;padding-bottom:2px}
.build__card:hover .build__card-reveal,.build__card:focus-within .build__card-reveal{
  grid-template-rows:1fr;opacity:1;margin-top:12.6px;
  transition:grid-template-rows .9s var(--ease-card-open),opacity .68s var(--ease-card-open) .06s,margin-top .9s var(--ease-card-open);
}
/* Once open, stop clipping entirely — belt and braces for the underline. */
.build__card:hover .build__card-reveal-in,.build__card:focus-within .build__card-reveal-in{overflow:visible}
.build__card .link-btn{align-self:flex-start;margin-top:2px}
.build__card:hover,.build__card:focus-within{box-shadow:var(--shadow-card-hover)}
/* Card hover motion (client round 13 item 1): a subtle lift on a smooth curve
   with a small intent delay, hover-capable fine pointers only, reduced-motion
   off. transform-only, so surrounding cards never move; the Figma "Large
   Shadow" above completes the lift. */
@media (hover:hover) and (pointer:fine) and (prefers-reduced-motion: no-preference){
  .build__card{transition:transform .8s var(--ease-card-close),box-shadow .8s var(--ease-card-close)}
  .build__card:hover,.build__card:focus-within{transform:translateY(-6px);transition:transform .9s var(--ease-card-open),box-shadow .9s var(--ease-card-open)}
}
/* Progress row (Figma "scrollbar" 648:4825): track + the white prev/next
   arrows on its right (client round 18 item 5). */
.build__trackrow{display:flex;align-items:center;gap:16px;margin:68.4px 48px 0;max-width:calc(var(--maxw) - 96px)}
/* round 22 item 2.2: exact client colours — track #FDFBF9, active #424040 */
.build__track{flex:1;min-width:0;height:3px;background:#fdfbf9;position:relative;border-radius:2px;cursor:pointer}
.build__track-fill{position:absolute;left:0;top:0;height:100%;width:25%;background:#424040;border-radius:2px}
.build__nav{display:flex;align-items:center;gap:10px}
/* Round 20 item 2.1: CLEAN WHITE ARROWS per the original Figma — no circle,
   no fill, no border, no shadow. The 40px hit area stays for usability;
   only the visible glyph is the 24px white chevron. */
.build__arrow{width:40px;height:40px;flex:0 0 40px;border:0;border-radius:50%;background:transparent;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:none;transition:opacity .25s ease,transform .25s var(--ease-spring)}
.build__arrow svg{width:24px;height:24px}
.build__arrow:hover,.build__arrow:focus-visible{opacity:.7}
.build__arrow:active{transform:scale(.9);transition-duration:.11s}
.build__arrow:focus-visible{outline:2px solid var(--amber);outline-offset:3px;opacity:1}
.build__arrow[disabled]{opacity:.5;pointer-events:none}
@media (prefers-reduced-motion: reduce){.build__arrow:active{transform:none}}
@media (max-width:1023px){
  .build__nav{display:none}
  /* Below 1024 the section is not pinned, so .build__cards is itself the
     horizontal scroller and clips the hover shadow at its own bottom edge.
     Same trick as the desktop window: grow the clip box, cancel it with an
     equal negative margin so the layout does not move. (Kept out of the base
     rule because the pinned-mode override resets the cards' margin, which
     would leave the padding uncompensated and add 48px to the section.) */
  .build__cards{padding-bottom:48px;margin-bottom:-48px}
}
/* Round 22: the chat launcher is fixed at the content container's right edge
   (item 1), which is exactly where these arrows sat — it covered the "next"
   arrow for the whole Build Options section. The arrows step inboard by the
   launcher's footprint so both stay usable; they remain on the right of the
   progress bar as designed, and the track simply extends to meet them. This
   also keeps them clear of the real respond.io widget, which docks in the
   same corner once its ID is configured. */
@media (min-width:1024px){.build__nav{margin-right:77px}}

/* ============================================================
   FOUNDER  (Figma 261:1038) — 1440 x 800, charcoal band
   Photo left (897) fading to charcoal · story text right
   ============================================================ */
.founder{background:var(--charcoal)}
.founder__inner{display:flex;align-items:stretch;min-height:800px}
.founder__media{position:relative;flex:0 0 52%;overflow:hidden}
.founder__media img{width:100%;height:100%;object-fit:cover}
/* Round 26 item 7: the text now starts over the photo's faded edge, and the
   client's reference point is a WOODEN PANEL — so the fade must not have
   swallowed the timber by then. It reaches full charcoal at the photo's own
   right edge, as before, only easing in a little sooner so the copy has a
   settled ground without hiding the panels behind it. */
.founder__media::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(66,64,64,0) 62%,rgba(66,64,64,.72) 84%,var(--charcoal) 100%)}
/* Round 26 item 7: the client asked for the right-hand content to move
   ~130px left so it begins around the second vertical wooden panel, just
   past the man's elbow — the block now overlaps the photo's faded edge
   rather than starting where the image ends. Width, vertical centring,
   typography and the rest of the composition are unchanged; only the
   starting position moves. Desktop only — below 1024 the section stacks. */
.founder__text{flex:1;display:flex;flex-direction:column;justify-content:center;gap:24px;padding:0 64.8px;max-width:594px}
@media (min-width:1024px){
  .founder__text{margin-left:-130px;position:relative;z-index:1}
}
.founder__label{font-weight:600;font-size:11px;letter-spacing:2px;text-transform:uppercase;color:var(--amber)}
.founder__title{font-family:var(--serif);font-weight:400;font-size:52px;line-height:58px;color:var(--white)}
.founder__body p{font-size:16px;line-height:1.5;color:var(--cream);margin-bottom:10px}
.founder__body .founder__sign{font-family:"Signerica Fat","Dancing Script",cursive;font-weight:400;font-size:32.4px;color:var(--amber);line-height:1;margin:36px 0 5.4px;white-space:nowrap}
.founder__name{font-size:16px;line-height:1.5;color:var(--cream);margin:0}
.founder__name strong{font-weight:700}

/* ============================================================
   REVIEWS  (Figma 261:1055) — 1440 x 913
   Entry photo right, charcoal gradient left, 3 Google Review cards
   ============================================================ */
.reviews{position:relative;min-height:820.8px;background:var(--cream);overflow:hidden}
.reviews__bg{position:absolute;inset:0;z-index:0;overflow:hidden}
.reviews__bg img{width:100%;height:100%;object-fit:cover;object-position:72% center}
/* Client round 18 item 6: on wide desktops the band gets much wider than the
   photo's aspect, so a centred crop pushed the homeowner's head to the top
   edge. Anchoring the visible band higher up the photo keeps the intended
   composition (doorway, homeowner, dog, planting) at every large width. */
@media (min-width:1560px){.reviews__bg img{object-position:72% 35%}}
@media (min-width:1800px){.reviews__bg img{object-position:72% 30%}}
/* Round 22 item 4.1: this section's band is much wider than the photo's own
   3:2 shape, so `cover` already has to crop it vertically — and the shared
   zoom utility's RESTING scale(1.12) was cropping a further 12% on top of
   that, which is what still read as "cropped incorrectly" on desktop. The
   zoom-out interaction is kept exactly as designed, just started from a much
   gentler 1.04, so ~8% more of the composition is on screen at rest at every
   desktop size. Aspect ratio and focal point are untouched. */
@media (hover:hover) and (pointer:fine) and (prefers-reduced-motion: no-preference){
  /* Client round 25: Beautifully Built no longer zooms — the section dropped
     its sh-zoom hooks, so this override is retired. The photo now renders at
     its natural cover framing and stays completely still while the
     testimonial column scrolls. */
}
.reviews__overlay{position:absolute;inset:0;z-index:1;background:linear-gradient(to left,rgba(66,64,64,0) 48%,#424040 67%)}
.reviews__tagline{position:absolute;z-index:1;bottom:60px;left:max(0px,calc(50vw - 192px));right:0;text-align:center;margin:0;font-family:var(--serif);font-style:italic;font-weight:400;font-size:101.68px;line-height:1;color:var(--amber);white-space:nowrap;pointer-events:none}
.reviews__inner{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;padding:60px 48px;display:flex}
.reviews__col{display:flex;flex-direction:column;gap:32px;width:480px}
.review-card{width:480px;background:var(--off-white);border-radius:4px;overflow:hidden}
.review-card__media{position:relative;padding:12px}
.review-card__media img{width:100%;aspect-ratio:332/172;object-fit:cover;border-radius:2px;display:block}
.review-card__tag{position:absolute;top:20px;right:17px;background:var(--warm-sand);color:var(--charcoal);font-weight:600;font-size:12px;letter-spacing:0.72px;text-transform:uppercase;line-height:1.2;padding:4px 8px;border-radius:999px}
.review-card__body{padding:12px 16px;display:flex;flex-direction:column;gap:10px}
.review-card__meta{display:flex;align-items:center;justify-content:space-between}
.review-card__stars{display:flex;gap:2px;color:var(--amber)}
.review-card__stars svg{width:15px;height:15px}
.review-card__date{font-size:14px;color:var(--mid-grey);letter-spacing:0.28px}
.review-card__quote{font-size:18px;line-height:1.4;color:var(--charcoal);letter-spacing:0.36px}
.review-card__name{font-weight:700;font-size:14px;color:var(--charcoal);letter-spacing:0.28px}
/* (client round 13 item: the "--sm" small-type card variant was removed —
   every testimonial uses the same Figma "Google Review" base typography:
   18px/1.4 quote, 14px bold name.) */

/* ============================================================
   FEATURE / CHECKLIST  (Figma 307:1529) — 1440 x 929, cream
   Living-room image left, guide-book download text + check list right
   ============================================================ */
.feature{background:var(--cream);position:relative;z-index:0;display:flex;align-items:stretch;max-width:var(--maxw);margin:0 auto;min-height:929px}
.feature::before{content:"";position:absolute;top:0;bottom:0;left:calc(50% - 50vw);width:100vw;background:#F7F3EA;z-index:-1;pointer-events:none}
.feature__media{flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:96px 96px 96px 48px}
.feature__media img{width:526px;height:679px;object-fit:cover;display:block}
.feature__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:24px;padding:128px 64px}
.feature__label{font-weight:600;font-size:12px;letter-spacing:1.6px;text-transform:uppercase;color:var(--charcoal)}
.feature__title{font-family:var(--serif);font-weight:400;font-size:48px;line-height:1.17;color:var(--charcoal)}
.feature__list{list-style:none;display:flex;flex-direction:column;gap:6px}
.feature__list li{display:flex;align-items:center;gap:12px;font-size:20px;line-height:1.5;color:#000}
.feature__list svg{flex:0 0 24px;width:24px;height:24px}
.feature__text .btn{align-self:flex-start;margin-top:8px}

/* ============================================================
   IMAGE SECTION  (Figma 309:1556) — 1440 x 929, terracotta
   "Build vs buy" text left, tall image right
   ============================================================ */
.image-section{background:var(--terracotta);display:flex;align-items:stretch;min-height:929px}
.image-section__text{flex:0 0 50%;box-sizing:border-box;min-width:0;display:flex;flex-direction:column;justify-content:center;padding:128px 48px 128px max(48px,calc(50vw - var(--maxw)/2 + 48px))}
.image-section__inner{max-width:640px;display:flex;flex-direction:column;gap:24px}
.image-section__label{font-weight:600;font-size:12px;letter-spacing:1.6px;text-transform:uppercase;color:var(--white)}
.image-section__title{font-family:var(--serif);font-weight:400;font-size:59px;line-height:1.08;letter-spacing:-0.59px;color:var(--off-white)}
.image-section__body{font-size:20px;line-height:1.5;color:var(--off-white)}
.image-section__actions{display:flex;gap:24px;flex-wrap:wrap}
.image-section__media{flex:0 0 50%;box-sizing:border-box;min-width:0;overflow:hidden}
.image-section__media img{width:100%;height:100%;object-fit:cover;display:block}

/* ============================================================
   SECTION IMAGE  (Figma 309:1585) — 1440 x 929, full-bleed
   "Build with confidence" centered over builder photo + MB badge
   ============================================================ */
.section-image{position:relative;min-height:929px;background:var(--terracotta);overflow:hidden;display:flex;align-items:flex-start;justify-content:center}
/* Round 21 item 1: the section keeps the Figma frame's own proportions
   (1440×929) as the viewport widens, so the bottom-anchored photo shows the
   IDENTICAL composition at every width — the sky band, and the text's place
   inside it, scale together instead of the sky collapsing under a fixed
   height. Below 1440 the 929px minimum simply shows MORE sky (safer). */
@media (min-width:1024px){
  /* width:100% keeps the box stretch-sized — otherwise, when min-height
     (the 100svh floor) beats the aspect height, the aspect-ratio would
     transfer the forced height back into a WIDER box (horizontal overflow) */
  /* Client feedback, 25 Aug item 3: past 1440 the aspect ratio kept growing
     the section — 1239px tall at 1920 — so it read as far too tall. The frame
     height is now capped at the Figma frame's own 929px, so the section stops
     growing once it reaches the design's height and the bottom-anchored photo
     simply shows less sky above the house. Composition, text position and the
     colour treatment are untouched. */
  /* Client feedback, 25 Aug item 3 — the section was too tall. The cause was
     the FLOOR below, not this rule: min-height was max(929px,100svh), so on a
     tall window the section was stretched to the whole viewport — 1080px on a
     1280x1080 screen, where the design wants 929 — and simply showed a band of
     extra sky. That floor is now a flat 929px, so the section is exactly the
     Figma proportion and never more.
       A hard 929px cap was tried and rejected on sight: the photograph is
     square, so a 1920-wide box only 929 tall crops nearly all the sky away and
     the heading ends up sitting on the mountains. Capping to the viewport has
     the same effect on a wide, short window. Past 1440 the section therefore
     stays proportional — that is the design's own composition, and shortening
     it further needs a wider crop of the photo, not CSS. */
  .section-image{width:100%;height:auto;aspect-ratio:1440/929}
}
.section-image__bg{position:absolute;inset:0;z-index:0}
/* round 19: the new approved photo (timber frame + lake) anchors to the
   bottom like the Figma frame — the sky takes the crop, never the house */
/* Round 26 item 9 — image treatment. The photo was pulled from Figma at
   native resolution in round 19, but Figma applies its adjustments to the
   image FILL, not to the exported file, so the export came across without
   them: on the page it reads far more saturated than the frame does.
   Measured off the client's own side-by-side in the feedback document
   (same screenshot, same rendering, matched bands): their reference is
   0.75x the saturation and about 1.13x the luminance of ours. Saturation
   and brightness only — no hue or temperature shift, so the colour balance
   and composition are untouched, exactly as the brief asks. */
.section-image__bg img{width:100%;height:100%;object-fit:cover;object-position:center bottom;filter:saturate(.75) brightness(1.09)}
.section-image__text{position:relative;z-index:2;width:838px;max-width:100%;display:flex;flex-direction:column;align-items:center;text-align:center;gap:24px;padding:128px 96px 128px 48px}
.section-image__label{font-weight:600;font-size:12px;letter-spacing:1.6px;text-transform:uppercase;color:var(--white)}
.section-image__title{font-family:var(--serif);font-weight:400;font-size:59px;line-height:1.08;letter-spacing:-0.59px;color:var(--off-white)}
.section-image__body{font-size:20px;line-height:1.5;color:var(--off-white)}
/* round 22 item 5.1: the Master Builders logo tracks the MAIN CONTENT
   CONTAINER's right edge at every width (the old `50% - 672px` assumed a
   fixed 1344 container, so it fell outside at 1920 and inside at 1280). */
.section-image__mb{position:absolute;top:0;right:max(48px,calc((100% - var(--maxw))/2 + 48px));width:75px;height:103px;z-index:3}
.section-image__mb img{width:100%;height:100%;object-fit:contain;display:block}

/* ============================================================
   LOCAL OFFICE + MAP  (Figma 309:1668) — 1440 x 929
   NZ map (left) + Find your local office panel (right)
   ============================================================ */
/* round 19: ONE background across the whole section — #AECCD5, the exact
   colour baked into the map artwork, so map and content halves read as one */
.local{position:relative;background:#aeccd5;display:flex;min-height:929px}
.local__map{flex:0 0 50%;position:relative;overflow:hidden;background:#aeccd5}
/* The artwork paints the stretched map pane absolutely, so its intrinsic
   height can never inflate the section (round 21) — the map is always exactly
   the height of the right-side content, top to bottom edge.
   Mobile keeps the designed 300px cropped band. */
.local__mapfit{position:absolute;inset:0}
.local__mapbox{position:absolute;inset:0}
.local__map img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
/* Client feedback, 25 Aug item 6: the gap under the office list was too deep.
   Bottom padding is exactly 50px now (was 96px); the top stays at 96px so the
   heading keeps its position. */
.local__panel{flex:1;min-width:0;padding:96px max(48px,calc(50vw - var(--maxw)/2 + 48px)) 50px 48px;display:flex;flex-direction:column;gap:24px}
.local__title{font-family:var(--serif);font-weight:400;font-size:48px;line-height:1.17;color:var(--charcoal)}
/* round 19: the search is a compact 352px control (Figma "map search"
   309:1779 — narrower than the address card), borderless with the small
   shadow; the functionality is untouched. */
/* round 21 item 2.5: a little more room than the Figma 352 — still well
   narrower than the address card */
.local__search{display:flex;height:48px;width:420px;max-width:100%;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.15)}
.local__search input{flex:1;min-width:0;border:none;border-radius:4px 0 0 4px;padding:0 16px;font-family:var(--sans);font-size:16px;color:var(--charcoal);background:#fff}
.local__search input::placeholder{color:var(--mid-grey)}
.local__search button{width:53px;flex:0 0 53px;background:var(--sage);border:none;border-radius:0 4px 4px 0;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center}
/* Postcode search "no match" note (client round 13 item 8) — quiet, in the
   section's existing type style; hidden until a search finds nothing. */
.local__search-msg{margin-top:8px;font-size:14px;line-height:1.4;color:var(--mid-grey)}
.local__search-msg[hidden]{display:none}
.local__search button svg{width:20px;height:20px}
.local__markers{position:absolute;inset:0;z-index:2;pointer-events:none}
/* round 21 item 2.4: solid marker dots — no white stroke/outline */
.map-marker{position:absolute;width:30px;height:30px;border-radius:50%;transform:translate(-50%,-50%);background:var(--sage);border:none;box-shadow:0 2px 6px rgba(0,0,0,.28);cursor:pointer;pointer-events:auto;padding:0;transition:background .25s ease,transform .2s ease}
.map-marker:hover{transform:translate(-50%,-50%) scale(1.14)}
.map-marker:focus-visible{outline:2px solid var(--amber);outline-offset:2px}
.map-marker.is-active{background:var(--amber);z-index:3}
.local__list{display:flex;flex-direction:column;gap:8px}
/* Round 21 item 2: the round-19 fixed-height/inner-scroll experiment is
   gone — the client wants NO nested scrollbar and natural free-flow
   scrolling. The section grows to its content (929px minimum per Figma),
   every office is simply on the page, and the map — a stretched flex
   sibling — always runs the full height of the right-side content down to
   the same bottom edge. */
@media (min-width:1024px){
  .local__panel{padding-bottom:0} /* the last card IS the bottom edge, map matches */
  /* Round 22 item 6: on desktop the map is FITTED, never cropped.
     · The fit layer starts at the panel's own 96px top padding, so the map's
       top edge and the right-hand content's top edge begin on exactly the
       same line (item 6.1).
     · The box is sized to min(pane width, available height × the artwork's
       aspect), i.e. true "contain" — so the right-hand side of the map stops
       being cut at 1440/1366/1280 and the "New Zealand" label stays on screen
       at every desktop width (items 6.2 / 6.3). Aspect ratio is preserved
       exactly; nothing is stretched.
     · The markers live inside this same box, so they stay locked to the map
       instead of drifting as the crop changes. The pane keeps the section's
       #AECCD5, so the fitted artwork reads as sitting on the section's field. */
  .local__mapfit{top:96px;bottom:0;container-type:size;display:flex;justify-content:center;align-items:flex-start}
  .local__mapbox{position:relative;inset:auto;width:100%;width:min(100%,calc(100cqh * 1061 / 1483));aspect-ratio:1061/1483;flex:0 0 auto}
  .local__mapbox img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;object-position:center top}
}
/* Office list scrolling (client round 25). The client asked for a vertical
   scroll "if there are more locations than can be displayed at once", so the
   list is capped and scrolls only when its content actually exceeds that cap —
   with few offices it still simply flows with the page, exactly as before.

   Deliberately NOT a scroll trap: overscroll-behavior is left at auto, so
   reaching either end of the list hands the gesture straight back to the page
   and the user is never held inside the panel. That was the specific problem
   with the round-19 version the client rejected. The scrollbar is a slim,
   quiet one in the section's own palette rather than the browser default. */
@media (min-width:1024px){
  .local__list{
    /* Feedback 31 Aug item 4: the panel already carried the 50px bottom
       padding, but the gap under the last card measured 131px. The cap was
       derived from the VIEWPORT (100svh - 320px = 550px at an 870px window)
       while the panel's height comes from the section's own 929px Figma
       frame - so the list under-filled its column by 81px and that slack fell
       below it, on top of the 50px padding. The cap now comes from the panel
       itself: the list flexes to fill the column, which leaves exactly the
       50px padding beneath it at any window size, and it still scrolls only
       when the cards genuinely exceed the space (the round-26 intent), still
       with no scroll trap and no visible scrollbar.
       Flex basis is 0, not auto: with `auto` the list asks for its full
       content height (751px for seven offices) and the panel is pushed to
       1049px, growing the section past its own 929px Figma frame. At basis 0
       the list only ever takes the space the panel has left, so the section
       keeps its designed height, the gap is the 50px padding, and the list
       goes back to scrolling internally exactly as round 26 intended. */
    flex:1 1 0;min-height:0;max-height:none;
    overflow-y:auto;overscroll-behavior:auto;scroll-behavior:smooth;
    /* Round 26 item 10: the scrollbar itself is hidden — the client does not
       want it visible — while the list still scrolls with wheel, trackpad,
       keyboard and touch exactly as before, and still never traps the page. */
    scrollbar-width:none;-ms-overflow-style:none;
  }
  /* Flex items shrink by default. In a height-capped column that means the
     OPEN office card gets squashed to fit and then clipped by .office's own
     overflow:hidden — the address and the Email/Phone/Find-out-more row simply
     disappear. Every row must keep its natural height and let the list scroll
     instead. (Same trap as the round-13 scroll lists.) */
  .local__list>.office{flex:0 0 auto}
  .local__list::-webkit-scrollbar{width:0;height:0;display:none}
  /* the list now owns the bottom edge, so the panel keeps its own padding.
     Client feedback, 25 Aug item 6: 96px left too deep a gap under the last
     office card — exactly 50px now. */
  .local__panel{padding-bottom:50px}
}
/* Office card — client round 26 item 10.
   · The white stroke round 25 added is gone: the client explicitly does not
     want a visible border around the card.
   · The stark #fff is replaced by the design system's off-white, so the card
     no longer reads as a "plain/stock white" block against the section.
   · The content area is inset by 15px (the client's 14-16px), so the photo
     and the address sit inside the card rather than bleeding to its edges. */
.office{background:var(--off-white);border-radius:6px;overflow:hidden;transition:background-color .45s var(--ease-office),box-shadow .45s var(--ease-office)}
.office[open]{box-shadow:0 2px 10px rgba(66,64,64,.10)}
.office__card{padding:15px; background: var(--cream); }
.office__card img{border-radius:4px}
.office__body{border-radius:4px}
/* The expand/collapse height is animated from home.js — a <details> cannot be
   transitioned from CSS in every browser, and the native exclusive group was
   snapping the outgoing card shut. The panel is the clipped, height-animated
   wrapper; the card inside it fades so the two read as one movement. */
.office__panel{overflow:hidden;height:0;opacity:0}
.js .office__panel{transition:height .5s var(--ease-office),opacity .38s var(--ease-office)}
.office.is-open>.office__panel{opacity:1}
html:not(.js) .office[open]>.office__panel{height:auto;opacity:1}
@media (prefers-reduced-motion: reduce){
  .office{transition:none}
  .js .office__panel{transition:none}
}
.office__summary{list-style:none;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:15px 20px;cursor:pointer}
.office__summary::-webkit-details-marker{display:none}
.office__summary span{font-size:16px;color:var(--charcoal);font-weight:500}
.office__chev{width:16px;height:16px;color:var(--charcoal);flex:0 0 auto;transition:transform .25s ease}
.office__summary:hover .office__chev{transform:translateY(2px)}
.office[open]>.office__summary{display:none}
.office__card img{width:100%;height:172px;object-fit:cover;display:block}
.office__body{background:var(--cream);padding:18px 2px 20px;display:flex;flex-direction:column;gap:12px}
.office__name{font-weight:600;font-size:20px;line-height:1.4;color:var(--charcoal)}
.office__meta{font-size:14px;line-height:1.6;color:var(--mid-grey)}
.office__actions{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-top:4px}
.office__contact{display:flex;gap:12px}
/* round 19: solid WHITE pills per Figma (309:1795/1796) — never transparent */
/* Client feedback, 25 Aug item 6: these two set a white background but never
   set --sweep, so .btn::before painted "transparent" and the circular fill —
   the signature of the button-studies reference — did nothing on exactly these
   buttons. The markup now carries btn--white, which supplies both the white
   background and its cream sweep, so all that is left here is dropping the
   border. Same root cause as the footer's "Get in Touch" button. */
.office__contact .btn{border:none}
.office__contact .btn svg{width:14px;height:14px}
/* Feedback 31 Aug item 3: this override forced the rest rule back to amber, so
   the standard .link-btn amber sweep filled gold over gold and the Find out
   more buttons looked static. Dropping the colour restores the reference
   interaction (faint var(--underline-base) rule, amber sweeping from the left
   over .5s var(--ease-fluid), arrow advancing over .45s) with no other change:
   the charcoal text colour this rule also sets is kept. */
.office__body .link-btn{color:var(--charcoal)}

/* ============================================================
   SITE CHECK  (Figma 435:3976) — 1440 x 929, full-bleed photo
   Centered "Site Check & Ballpark Pricing"
   ============================================================ */
.site-check{position:relative;min-height:929px;overflow:hidden;display:flex;justify-content:center}
.site-check__bg{position:absolute;inset:0;z-index:0;overflow:hidden}
.site-check__bg img{width:100%;height:100%;object-fit:cover;object-position:center}
.site-check__text{position:relative;z-index:2;text-align:center;display:flex;flex-direction:column;align-items:center;gap:24px;padding-top:112px;width:660px;max-width:90%}
.site-check__label{font-weight:600;font-size:12px;letter-spacing:1.6px;text-transform:uppercase;color:var(--charcoal)}
.site-check__title{font-family:var(--serif);font-weight:400;font-size:59px;line-height:1.08;letter-spacing:-0.59px;color:var(--charcoal)}
.site-check__body{font-size:20px;line-height:1.5;color:var(--charcoal)}

/* ============================================================
   PROCESS STEPS  (Figma 310:2037) — "Our Process", 3 steps
   ============================================================ */
.process{background:#F7F3EA;padding:96px 0}
.process__header{display:flex;flex-direction:column;align-items:center;gap:24px}
.process__header h2{font-family:var(--serif);font-weight:400;font-size:59px;line-height:1.08;letter-spacing:-0.59px;color:var(--charcoal)}
.process__rule{width:251px;height:2px;background:var(--amber)}
.process__steps{display:flex;padding:96px 48px 0;max-width:var(--maxw);margin:0 auto}
.step{flex:1;min-width:0;display:flex;flex-direction:column;position:relative}
.step__media{height:340px;display:flex;align-items:flex-end;justify-content:center;padding:0 24px}
.step__media img{max-width:100%;max-height:100%;object-fit:contain}
.step__body{display:flex;align-items:flex-start;gap:34px;padding:0 24px 0 0;position:relative}
.step__num{font-family:var(--serif);font-weight:400;font-size:200px;line-height:.7;color:var(--sage);flex:0 0 auto;position:relative;z-index:0;margin-top:-45px}
.step__text{display:flex;flex-direction:column;gap:12px;padding-top:30px;min-width:0;position:relative;z-index:1}
.step__label{font-weight:600;font-size:12px;letter-spacing:1.6px;text-transform:uppercase;color:var(--charcoal)}
.step__title{font-family:var(--serif);font-weight:400;font-size:36px;line-height:1.17;color:var(--charcoal)}
.step__desc{font-size:14px;line-height:1.4;letter-spacing:0.28px;color:#000}
.step__text .link-btn,.step__text .btn{align-self:flex-start;margin-top:4px}
.step__cta{display:inline-flex;align-items:center;gap:8px;align-self:flex-start;margin-top:8px;font-family:inherit;font-weight:600;font-size:12px;letter-spacing:1.44px;text-transform:uppercase;color:var(--charcoal);text-decoration:none;background:transparent;border:1px solid transparent;border-bottom-color:var(--amber);border-radius:0;padding:8px 2px;transition:background .3s ease,border-color .3s ease,border-radius .3s ease,padding .3s ease,margin .3s ease,box-shadow .3s ease,color .3s ease,transform .45s var(--ease-spring)}
.step__cta .arrow{width:14px;height:14px;flex:0 0 auto;transition:transform .45s var(--ease-fluid)}
/* Client round 13 items 12+14: the morph fires ONLY when the button itself is
   hovered/focused (it previously reacted to hovering anywhere on the step,
   which read as the button animating by itself) — with the button-studies
   lift, arrow advance and press. */
.step__cta:hover,.step__cta:focus-visible{background:#ffffff;border-color:rgba(0,0,0,.08);border-radius:999px;padding:13px 24px;margin-bottom:-10px;box-shadow:0 4px 14px rgba(0,0,0,.10);transform:translateY(-2px)}
.step__cta:hover .arrow,.step__cta:focus-visible .arrow{transform:translateX(4px)}
.step__cta:active{transform:translateY(0) scale(.96);transition-duration:.11s}
.step__cta:focus-visible{outline:2px solid var(--amber);outline-offset:3px}
@media (prefers-reduced-motion: reduce){
  .step__cta:hover,.step__cta:focus-visible,.step__cta:active{transform:none}
  .step__cta:hover .arrow,.step__cta:focus-visible .arrow{transform:none}
}
.step__num::before{content:"";position:absolute;width:279px;height:279px;border-radius:50%;background:#ffffff;left:50%;top:50%;z-index:-1;opacity:0;transform:translate(-50%,-38%) scale(.8);transform-origin:center;transition:opacity .35s ease,transform .35s ease;pointer-events:none}
.step:hover .step__num::before{opacity:1;transform:translate(-50%,-38%) scale(1)}

/* ============================================================
   BLOG CARDS  (Figma 310:2097) — slate-green band, featured + 3 cards
   ============================================================ */
.blog{position:relative}
.blog__band{position:absolute;top:0;left:0;right:0;height:929px;background:var(--slate-green);z-index:0}
.blog__inner{position:relative;z-index:1;display:flex;align-items:flex-start}
.blog__media{flex:1;min-width:0;height:929px;overflow:hidden}
.blog__media img{width:100%;height:100%;object-fit:cover;display:block}
.blog__right{flex:1;min-width:0;display:flex;flex-direction:column;padding-bottom:64px}
.blog__feature{padding:128px max(48px,calc(50vw - var(--maxw)/2 + 48px)) 40px 64px;display:flex;flex-direction:column;gap:24px}
.blog__label{font-weight:600;font-size:12px;letter-spacing:1.6px;text-transform:uppercase;color:var(--off-white)}
.blog__title{font-family:var(--serif);font-weight:400;font-size:59px;line-height:1.08;letter-spacing:-0.59px;color:var(--off-white)}
.blog__desc{font-size:20px;line-height:1.5;color:var(--off-white)}
.blog__cards{padding:0 max(48px,calc(50vw - var(--maxw)/2 + 48px)) 0 64px;display:flex;flex-direction:column;gap:24px}
.blog__slider{overflow:hidden;width:100%}
.blog__track{display:flex;transition:transform .55s cubic-bezier(.4,0,.2,1)}
.blog__track .blog-card{flex:0 0 100%;min-width:0}
/* Wrap-around duplicates exist only for the desktop scroll-driven loop —
   the tablet/mobile slider and reduced-motion layouts never show them. */
.blog__track .blog-card--wrap{display:none}
.blog__dots{display:flex;gap:10px;align-items:center;justify-content:flex-start}
.blog__dots button{width:10px;height:10px;border-radius:50%;border:none;padding:0;background:rgba(255,255,255,.4);cursor:pointer;transition:background .3s ease,width .3s ease}
.blog__dots button[aria-selected="true"]{background:var(--off-white);width:28px;border-radius:5px}
.blog-card{background:#fff;border-radius:4px;overflow:hidden;display:flex;align-items:stretch}
.blog-card__img{flex:1;min-width:0;padding:12px}
.blog-card__img .frame{height:226px;border-radius:2px;overflow:hidden}
.blog-card__img img{width:100%;height:100%;object-fit:cover;display:block}
.blog-card__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:8px;padding:12px 16px 16px}
.blog-card__date{font-size:14px;color:var(--mid-grey);letter-spacing:0.28px;line-height:1.4}
.blog-card__ct{font-family:var(--sans);font-weight:500;font-size:18px;line-height:1.4;color:var(--charcoal)}
.blog-card__cd{font-size:16px;line-height:1.5;color:var(--mid-grey)}

/* ============================================================
   FAQ  (Figma 426:4548) — 1440 x 857, cream + faded photo right
   ============================================================ */
.faq{position:relative;background:var(--cream);overflow:hidden;min-height:857px}
.faq__bg{position:absolute;inset:0;z-index:0}
.faq__bg img{width:100%;height:100%;object-fit:cover;object-position:bottom;mix-blend-mode:multiply}
.faq__overlay{position:absolute;inset:0;z-index:1;background:linear-gradient(269.5deg,rgba(247,243,234,0) 40.7%,var(--cream) 74.2%)}
.faq__inner{position:relative;z-index:2;width:min(100%,calc(859px + max(48px,calc(50vw - var(--maxw)/2 + 48px))));padding:96px 48px 96px max(48px,calc(50vw - var(--maxw)/2 + 48px));display:flex;flex-direction:column;gap:24px}
.faq__title{font-family:var(--serif);font-weight:400;font-size:59px;line-height:1.08;letter-spacing:-0.59px;color:var(--charcoal)}
.faq__sub{font-size:20px;line-height:1.5;color:var(--charcoal);margin-top:20px}
.faq__list{width:674px;max-width:100%;display:flex;flex-direction:column;gap:12px}
.faq-item{background:#fff;border-radius:4px;box-shadow:0 1px 2px rgba(0,0,0,.15);padding:16px}
.faq-item>summary{list-style:none;display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer}
.faq-item>summary::-webkit-details-marker{display:none}
.faq-item>summary p{font-family:var(--sans);font-weight:500;font-size:18px;line-height:1.4;color:var(--charcoal);margin:0}
/* FAQ expand/collapse — client round 25.

   The chevron and the answer now run on ONE curve and ONE duration, so the
   arrow finishes turning exactly as the answer finishes arriving. The panel's
   height is driven from home.js (a <details> element cannot be transitioned
   from CSS in every browser we support), and the arrow keys off `.is-open`
   rather than `[open]` — on the way closed the `open` attribute survives
   until the animation ends, so keying off it would snap the arrow back early
   and break the synchronisation the client asked for.

   Nothing here changes the content, type, colours, padding or spacing: the
   answer keeps its 12px offset, now carried inside the panel so the collapsed
   state measures exactly zero. */
.faq-item>summary svg{width:24px;height:24px;flex:0 0 24px;color:var(--charcoal);transition:transform .55s var(--ease-faq)}
.faq-item.is-open>summary svg{transform:rotate(180deg)}
.faq-item__panel{overflow:hidden;height:0;opacity:0;transform:translateY(-6px);will-change:height}
.js .faq-item__panel{transition:height .55s var(--ease-faq),opacity .42s var(--ease-faq),transform .55s var(--ease-faq)}
.faq-item.is-open>.faq-item__panel{opacity:1;transform:none}
/* No JS, or the browser rendering before home.js runs: a plain open accordion. */
html:not(.js) .faq-item[open]>.faq-item__panel{height:auto;opacity:1;transform:none}
.faq-item__a{font-size:14px;line-height:1.4;letter-spacing:0.28px;color:var(--mid-grey);margin-top:12px}
@media (prefers-reduced-motion: reduce){
  .faq-item>summary svg,.js .faq-item__panel{transition:none}
  .faq-item__panel{transform:none}
}

/* ============================================================
   FOOTER CTA  (Figma 426:4449) — 1440 x 845, full-bleed photo
   ============================================================ */
.footer-cta{position:relative;min-height:845px;overflow:hidden;display:flex}
.footer-cta__bg{position:absolute;inset:0;z-index:0}
.footer-cta__bg img{width:100%;height:100%;object-fit:cover;display:block}
.footer-cta__overlay{position:absolute;inset:0;z-index:1;mix-blend-mode:multiply;background:linear-gradient(-85.9deg,rgba(118,118,118,0) 31%,rgb(128,144,160) 98%)}
.footer-cta__text{position:relative;z-index:2;width:min(100%,calc(548px + max(48px,calc(50vw - var(--maxw)/2 + 48px))));display:flex;flex-direction:column;justify-content:center;gap:24px;padding:180px 80px 180px max(48px,calc(50vw - var(--maxw)/2 + 48px))}
.footer-cta__label{font-weight:600;font-size:11px;letter-spacing:2px;text-transform:uppercase;color:var(--off-white);opacity:.7;margin-bottom:4px}
.footer-cta__title{font-family:var(--serif);font-weight:400;font-size:60px;line-height:66px;color:var(--off-white)}
.footer-cta__body{font-family:var(--sans);font-weight:500;font-size:18px;line-height:28px;color:var(--off-white)}
.footer-cta__actions{display:flex;flex-direction:column;gap:16px;align-items:flex-start}

/* ============================================================
   FOOTER  (Figma 322:2201) — charcoal
   ============================================================ */
.footer{background:var(--charcoal)}
.footer__inner{max-width:var(--maxw);margin:0 auto;padding:80px 48px;display:flex;flex-direction:column;gap:56px;container-type:inline-size}
/* Round 22 item 9: the heading is sized from the CONTAINER, not the viewport,
   so its proportion holds at every width instead of being capped by a
   viewport clamp. Round 25b: the client asked for it bigger still — filling
   the full width of the content container rather than the 94% the Figma
   frame measures (1265px of line in a 1344px box, which is where 8.185cqi
   came from). Measured across 1920/1600/1440/1280, the line sat at exactly
   94.0% at every width, and 8.705cqi is the factor that would make it 100%.
   8.64cqi lands it at ~99.3% — visually flush with the container edges, with
   just under 10px of slack at 1440 so sub-pixel font-metric variance can
   never tip the line into a wrap or a clip. Still one line, still the Figma
   spacing between "Your life.", "Beautifully" and "built.", and still a
   fraction OF the container, so it cannot outgrow it. clamp() keeps mobile at
   44px; the upper bound is inert on supported widths (the container tops out
   at 1504, i.e. ~130px) and exists only as a backstop. */
.footer__brand-title{font-family:var(--serif);font-weight:400;font-size:clamp(44px,8.64cqi,138px);line-height:1.1;color:var(--warm-sand)}
.footer__brand-title em{font-style:italic}
.footer__brand-text{font-size:16px;line-height:1.4;letter-spacing:0.32px;color:var(--cream);margin-top:24px;max-width:960px}
.footer__contact{display:flex;gap:48px;align-items:center;flex-wrap:wrap}
/* Round 25b: the Get in Touch pill now carries .btn--outline-light in the
   markup instead of this bespoke rule. The bespoke version set the border,
   colour and transparent background but never set --sweep, so .btn::before
   painted with `transparent` and the circular fill — the "grow" half of the
   reference interaction — did nothing on this one button. The lift, the arrow
   advance and the easing were already reference-exact site-wide; adopting the
   shared variant restores the fill and keeps this button identical to every
   other CTA. Nothing bespoke is needed here now. */
.footer__contact-item{display:flex;gap:12px;align-items:center;color:var(--cream)}
.footer__contact-item .lbl{font-weight:600;font-size:14px;letter-spacing:1.68px;text-transform:uppercase}
.footer__contact-item .val{font-size:16px;letter-spacing:0.32px}
.footer__nav{display:flex;gap:32px}
.footer__col{flex:1;min-width:0;display:flex;flex-direction:column;gap:12px}
.footer__col h4{font-weight:600;font-size:14px;letter-spacing:1.68px;text-transform:uppercase;color:var(--cream)}
.footer__col ul{display:flex;flex-direction:column;gap:8px}
.footer__col a{font-size:16px;line-height:1.4;letter-spacing:0.32px;color:var(--cream);transition:opacity .2s}
.footer__col a:hover{opacity:.7}
.footer__midrow{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;flex-wrap:wrap}
.footer__news h4{font-weight:600;font-size:14px;letter-spacing:1.68px;text-transform:uppercase;color:var(--cream);margin-bottom:12px}
.footer__news-row{display:flex}
.footer__news-row input{width:245px;max-width:100%;border:none;background:var(--cream);border-radius:4px 0 0 4px;padding:12px 16px;font-family:var(--sans);font-size:16px;color:var(--charcoal)}
.footer__news-row input::placeholder{color:var(--mid-grey)}
.footer__news-row button{display:inline-flex;align-items:center;gap:8px;background:#fff;color:var(--charcoal);border:none;border-radius:0 4px 4px 0;padding:12px 16px 12px 20px;font-family:var(--sans);font-weight:600;font-size:12px;letter-spacing:1.6px;text-transform:uppercase;cursor:pointer}
.footer__news-row button svg{width:16px;height:16px}
/* round 19: icons use the exact Figma glyphs (Social Links 329:1706) — all
   three are a true 20px tall on one baseline, widths follow each glyph. */
.footer__social{display:flex;gap:16px;align-items:center}
.footer__social a{color:var(--cream);display:flex;align-items:center}
.footer__social svg{height:20px;width:auto;display:block}
.footer__crumbs{border-top:1px solid rgba(255,255,255,.2);padding-top:12px;display:flex;gap:10px;align-items:center}
.footer__crumbs span{font-weight:600;font-size:14px;letter-spacing:1.68px;text-transform:uppercase;color:var(--cream)}
.footer__crumbs svg{width:16px;height:16px;color:var(--cream)}
.footer__bottom{display:flex;flex-direction:column;gap:12px;font-size:14px;letter-spacing:0.28px;color:var(--cream)}
.footer__bottom .links{display:flex;align-items:center;justify-content:space-between}
.footer__bottom .links .left{display:flex;gap:16px}
.footer__bottom a{color:var(--cream)}

/* ============================================================
   TAGLINE  (Figma 305:1505) — decorative amber statement
   ============================================================ */
.tagline{background:var(--off-white)}
.tagline__inner{max-width:var(--maxw);margin:0 auto;position:relative;height:230px}
.tagline__text{position:absolute;left:630px;top:58px;margin:0;font-family:var(--serif);font-style:italic;font-weight:400;font-size:101.68px;line-height:114.39px;color:var(--amber);white-space:nowrap}

/* ============================================================
   RESPONSIVE — scale desktop down; mobile layout added in next slice
   (Home • Mobile Figma frame)
   ============================================================ */
/* ---------- Tablet & down: relax fixed/absolute desktop layouts ---------- */
@media (max-width:1439px){
  .nav__links{gap:28px}
  .founder__text{padding:0 64px}
  .tagline__text{left:auto;right:48px}
}
@media (max-width:1200px){
  /* Desktop nav persists to 1024 (agreed tablet rule): compact the bar so
     logo + 6 links + actions fit without collision. */
  .nav__inner{padding-left:32px;padding-right:32px}
  .nav__logo img{height:40px}
  .nav__links{gap:16px}
  .nav__actions{gap:12px}
}

/* ---------- Hero, stacked layout (Figma Home • Mobile) ----------
   Applies below the desktop-nav breakpoint: cream page, charcoal copy,
   buttons overlapping the photo edge, charcoal recognition band below,
   floating chat bubble. Tablet (769–1023) refines the same structure. */
@media (max-width:1023px){
  .hero{height:auto;min-height:0;display:flex;flex-direction:column;background:var(--cream)}
  .hero__overlay{display:none}
  .hero::before{display:none}
  .hero__inner,.hero__meta{display:contents}
  /* Top padding = fixed mobile bar (~63px) + 65px gap to the title (Figma). */
  .hero__copy{position:static;order:1;width:auto;padding:128px 24px 0;gap:0}
  .hero__headline{gap:12px}
  .hero__title{font-size:36px;line-height:1.08;letter-spacing:-.36px;color:var(--charcoal);text-shadow:none}
  .hero__sub{font-family:var(--sans);font-weight:500;font-size:18px;line-height:1.4;color:var(--charcoal);text-shadow:none}
  /* Buttons straddle the photo's top edge (Figma: 21px above, 71px over). */
  .hero__actions{position:relative;z-index:2;flex-direction:column;align-items:flex-start;gap:12px;margin:23px 0 -71px;padding:0 24px}
  .hero__actions .btn--outline-light{background-color:var(--cream);border-color:transparent;color:var(--charcoal);--sweep:var(--warm-sand);box-shadow:0 1px 2px rgba(0,0,0,.15)}
  .hero__bg{position:static;order:2;width:100%;height:auto}
  /* Ratio lives on the replaced element — a ratio on the wrapper gets beaten
     by the img's content-based minimum height once the real photo loads. */
  .hero__bg img,.hero__video{width:100%;height:auto;aspect-ratio:402/393;object-fit:cover;object-position:center 30%;display:block}
  /* Recognition — full-width charcoal band (Figma 1026:26794). */
  .hero__recognition{position:static;order:3;width:100%;background:var(--charcoal);padding:24px;align-items:flex-start;text-align:left;gap:6px;animation:none}
  .hero__recognition .rec-title{font-size:12px;letter-spacing:1.44px;color:var(--off-white)}
  .hero__recognition .rec-rule{width:75px}
  .hero__recognition .rec-desc{font-size:14px;line-height:1.4;letter-spacing:.28px;font-weight:700;min-height:0;max-width:22ch}
  /* Rotating awards in the band: keep a one-line clip frame (absolute children
     collapse height otherwise) and anchor left to match the band's alignment. */
  .hero__recognition .rec-desc.is-multi{min-height:20px;max-width:none;width:100%}
  .rec-desc.is-multi .rec-award{left:0;right:auto;max-width:100%;overflow:hidden;text-overflow:ellipsis}
  /* Chat bubble floats like the real widget will (demo behaviour). */
  .hero__livechat{position:fixed;top:auto;right:24px;bottom:24px;z-index:40}

  /* ---------- Ideas & Plans, stacked (Figma Home•Mobile 1026:26813/26789) ----------
     Order flips: Ideas (style) first on cream with the materials square bleeding
     off the right edge, then Plans on an off-white band with the small plan
     drawing floating right. Mobile uses the shorter Figma copy (.ideas__m). */
  .ideas{display:flex;flex-direction:column;align-items:stretch;max-width:100%;min-height:0;overflow:hidden}
  .ideas__d{display:none}
  .ideas__m{display:inline}
  .ideas__col--plans::before{display:none}
  .ideas__col--style{order:1;background:var(--cream);min-height:348px}
  .ideas__col--plans{order:2;background:var(--off-white)}
  /* Ideas: narrow text column beside the 296px materials square (Figma x140,
     bleeding 34px past the right edge, top-aligned with the block). */
  .ideas__col--style .ideas__media{position:absolute;top:0;right:-34px;bottom:auto;left:auto;align-self:auto;margin:0;width:296px;height:296px}
  /* Mobile shows the square as a crisp photo card — no edge fade. */
  .ideas__col--style .ideas__media img{width:100%;height:100%;object-fit:cover;border-radius:4px;-webkit-mask-image:none;mask-image:none}
  .ideas__col--style .ideas__text{margin:0;padding:64px 24px;width:204px;box-sizing:border-box}
  /* Plans: off-white band, small plan drawing at (193,49), 190×224. */
  .ideas__col--plans .ideas__media{display:none}
  .ideas__plan-m{display:block;position:absolute;top:49px;right:19px;width:190px;height:224px;z-index:1;mix-blend-mode:multiply}
  .ideas__col--plans .ideas__text{padding:96px 24px;width:auto}
  .ideas__text{gap:15px}
  .ideas__text .h1{font-size:36px;line-height:1.17;letter-spacing:0}
  .ideas__text p{font-size:16px;line-height:1.5;max-width:135px}
}
/* Tablet refinements (769–1023): same structure, more generous scale. */
@media (min-width:769px) and (max-width:1023px){
  .hero__copy{padding:160px 48px 0}
  .hero__title{font-size:56px}
  .hero__sub{font-size:20px}
  .hero__actions{flex-direction:row;align-items:center;margin:32px 0 -20px;padding:0 48px}
  .hero__bg img,.hero__video{aspect-ratio:16/9}
  .hero__recognition{padding:24px 48px}
  /* Ideas & Plans: same stacked structure, 48px gutters. */
  .ideas__col--style .ideas__text{padding:64px 48px;width:280px}
  .ideas__col--plans .ideas__text{padding:96px 48px}
  .ideas__plan-m{right:48px}
  /* Our Story at tablet (client round 25, found while checking item 7 across
     breakpoints). Between 769 and 1023 the section still uses the two-column
     desktop layout, but the right-hand column could not shrink into the space
     left by the 52% image: a flex item's default min-width:auto holds it at
     its content's minimum, and the signature line is white-space:nowrap, so
     the column stayed wide and pushed the whole PAGE 130px sideways at 820.
     Letting the column shrink, trimming its gutters to the 48px used
     elsewhere at this width, and allowing the signature to wrap keeps the
     composition intact and the overflow gone. Desktop and mobile are
     untouched — this block only ever applies in the tablet band. */
  .founder__text{min-width:0;padding:0 48px}
  .founder__sign{white-space:normal}
  /* Guide Book at tablet (same round-25 breakpoint sweep). The media column is
     flex:0 0 auto around a hard 526px image plus 144px of padding, so it took
     670 of the 820 available and left the text column 150px — narrower than
     its own 128px of padding, which pushed the CTA (white-space:nowrap) clean
     off the page. Letting the image scale within the column and trimming both
     gutters to the 48px used elsewhere at this width fixes it without
     touching the desktop composition. */
  .feature__media{flex:0 1 auto;min-width:0;padding:64px 32px 64px 48px}
  .feature__media img{width:100%;max-width:360px;height:auto;aspect-ratio:526/679}
  .feature__text{padding:64px 48px 64px 0}
}

/* ---------- Mobile (Home • Mobile) ---------- */
@media (max-width:768px){
  :root{--global-padding:24px}

  /* BUILD OPTIONS */
  .build{min-height:0}
  .build__inner{padding:64px 0 0}
  .build__head{flex-direction:column;align-items:flex-start;padding:0 24px}
  .build__intro .h1{font-size:36px}
  .build__cards{padding:40px 0 0;margin:0 24px;gap:16px}
  .build__card{flex:0 0 100%;height:auto}
  .build__card img{flex:none;height:340px;max-height:400px}
  .build__card-reveal{grid-template-rows:1fr;opacity:1;margin-top:14px;transition:none}
  .build__card-reveal-in{overflow:visible}
  .build__trackrow{margin:40px 24px 0}

  /* FOUNDER */
  .founder__inner{flex-direction:column;height:auto}
  .founder__media{width:100%;flex:none;height:320px}
  .founder__media::after{background:none}
  .founder__text{padding:48px 24px}
  .founder__title{font-size:36px;line-height:1.17}

  /* REVIEWS */
  .reviews{min-height:0}
  .reviews__overlay{background:linear-gradient(180deg,rgba(66,64,64,.72),rgba(66,64,64,.85))}
  .reviews__inner{padding:48px 24px}
  .reviews__col,.review-card{width:100%}

  /* TAGLINE */
  .tagline__inner{height:auto;padding:48px 24px}
  .tagline__text{position:static;font-size:44px;line-height:50px;white-space:normal}

  /* FEATURE / CHECKLIST */
  .feature{flex-direction:column;min-height:0}
  .feature__media{padding:32px 24px 0}
  .feature__media img{width:100%;height:auto;max-height:420px}
  .feature__text{padding:32px 24px 48px}
  .feature__title{font-size:36px}
  .feature__list li{font-size:16px}

  /* IMAGE SECTION */
  .image-section{flex-direction:column;min-height:0}
  .image-section__text{padding:48px 24px}
  .image-section__title{font-size:36px}
  .image-section__media{width:100%;height:360px}
  .image-section__actions{flex-direction:column;align-items:stretch}

  /* SECTION IMAGE */
  .section-image{min-height:0}
  .section-image__text{width:auto;padding:64px 24px;gap:16px}
  .section-image__title{font-size:36px}
  .section-image__mb{left:auto;right:16px;width:56px;height:77px}

  /* LOCAL OFFICE */
  .local{flex-direction:column;min-height:0}
  .local__map{flex:none;width:100%;height:300px}
  .local__panel{padding:48px 24px}
  .local__title{font-size:36px}

  /* SITE CHECK */
  .site-check{min-height:0}
  .site-check__text{width:auto;padding:64px 24px;gap:16px}
  .site-check__title{font-size:36px}

  /* PROCESS */
  .process{padding:64px 0}
  .process__header h2{font-size:36px}
  .process__steps{flex-direction:column;padding:48px 24px 0;gap:48px}
  .step__num{font-size:120px;margin-top:0}
  .step__num::before{display:none}
  .step__body{gap:20px}
  .step__cta{margin-bottom:0}
  .step__title{font-size:28px}

  /* BLOG */
  .blog{background:var(--slate-green)}
  .blog__band{display:none}
  .blog__inner{flex-direction:column;align-items:stretch}
  .blog__media{height:300px;width:100%}
  .blog__feature{padding:48px 24px}
  .blog__title{font-size:36px}
  .blog__cards{padding:0 24px 48px;min-width:0}
  .blog__right{min-width:0}
  .blog__slider{max-width:100%}
  .blog__track .blog-card{flex:0 0 100%;width:100%;min-width:0}
  .blog-card{flex-direction:column}
  .blog-card__text{min-width:0}
  .blog-card__ct,.blog-card__cd{overflow-wrap:break-word}
  .blog-card__img .frame{height:200px}

  /* FAQ */
  .faq{min-height:0}
  .faq__bg img{opacity:.2}
  .faq__inner{width:auto;padding:48px 24px}
  .faq__title{font-size:36px}
  .faq__list{width:100%}

  /* FOOTER CTA */
  .footer-cta{min-height:0}
  .footer-cta__text{width:auto;padding:64px 24px}
  .footer-cta__title{font-size:40px;line-height:44px}
  .footer-cta__actions{align-items:stretch}

  /* FOOTER */
  .footer__inner{padding:48px 24px;gap:40px}
  .footer__brand-title{font-size:44px;line-height:1.1}
  .footer__contact{flex-direction:column;align-items:flex-start;gap:20px}
  .footer__nav{flex-wrap:wrap;gap:32px 24px}
  .footer__col{flex:0 0 42%}
  .footer__midrow{flex-direction:column;align-items:stretch;gap:24px}
  .footer__news{width:100%}
  .footer__news-row{width:100%}
  .footer__news-row input{width:auto;flex:1;min-width:0}
  .footer__news-row button{flex:0 0 auto;white-space:nowrap}
  .footer__bottom .links{flex-direction:column;gap:12px;align-items:flex-start}
}

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


/* ============================================================
   MEGA MENUS — ported from the client reference demo
   (sentinel-demo-423.netlify.app/desktop-nav.html):
   soft scrim (z below), one full-height cream sheet docked to the
   right edge, left edge anchored to the open item; sliding/resizing
   between menus; header content floats above the sheet.
   ============================================================ */
.nav__scrim{position:fixed;inset:0;background:rgba(45,44,44,.55);opacity:0;pointer-events:none;
  transition:opacity .6s cubic-bezier(.4,0,.2,1);z-index:0}
.nav.panel-open .nav__scrim{opacity:1;pointer-events:auto}
.nav__inner{position:relative;z-index:3}

.nav__sheet{
  position:fixed;top:0;bottom:0;right:0;left:auto;z-index:2;background:var(--cream);
  overflow:hidden; /* panels scroll individually (demo .menu-content) */
  transform:translateX(105%);visibility:hidden;
  transition:transform .85s cubic-bezier(.16,1,.3,1),left .7s cubic-bezier(.16,1,.3,1),visibility 0s .85s;
  will-change:transform,left;
}
.nav__sheet.is-open{transform:none;visibility:visible;transition:transform .85s cubic-bezier(.16,1,.3,1),left .7s cubic-bezier(.16,1,.3,1),visibility 0s 0s}

/* Panels: absolutely stacked inside the sheet (demo .menu-content). Menus
   cross-fade with a 6px rise while the sheet slides between anchors — the
   outgoing menu is never cut off abruptly. */
.nav__panel{position:absolute;inset:0;overflow-y:auto;overscroll-behavior:contain;
  padding:140px 40px 48px;
  opacity:0;transform:translateY(6px);visibility:hidden;pointer-events:none;
  transition:opacity .5s cubic-bezier(.4,0,.2,1),transform .55s cubic-bezier(.16,1,.3,1),visibility 0s .55s}
.nav__panel.is-current{opacity:1;transform:none;visibility:visible;pointer-events:auto;
  transition:opacity .5s cubic-bezier(.4,0,.2,1),transform .55s cubic-bezier(.16,1,.3,1),visibility 0s 0s}
.nav__panel--plans{display:flex;align-items:flex-start;gap:56px}
.nav__panel--plans .nav__panel-body{flex:0 0 auto;min-width:240px}

/* Row entrance stagger (demo itemIn: rows slide 28px in from the right,
   55ms curve, 35ms/row + 60ms head start). JS restarts the animation each
   time a panel becomes current, so every open/switch re-plays it. */
.nav__panel-list li,
.np-office-block>*,
.np-office-list__title,
.np-regions li{
  opacity:0;
  animation:sh-item-in .75s cubic-bezier(.16,1,.3,1) both;
  animation-delay:calc(var(--i,0)*45ms + 120ms);
}
@keyframes sh-item-in{from{opacity:0;transform:translateX(28px)}to{opacity:1;transform:none}}

.nav__panel-list{display:flex;flex-direction:column;gap:24px;align-items:flex-start}
.nav__panel-list>li{display:flex} /* row height = content height (demo line boxes) */
/* 18px rows (demo .heading-link): mid-grey -> charcoal, hidden arrow slides in */
.np-item a{display:inline-flex;align-items:center;gap:8px;font-weight:500;font-size:18px;line-height:1.4;color:var(--mid-grey);transition:color .25s cubic-bezier(.4,0,.2,1)}
.np-item a:hover,.np-item a:focus-visible{color:var(--charcoal)}
.np-item .np-arrow{width:20px;height:20px;flex:0 0 20px;opacity:0;transform:translateX(-8px);
  transition:opacity .25s cubic-bezier(.4,0,.2,1),transform .35s cubic-bezier(.16,1,.3,1)}
.np-item a:hover .np-arrow,.np-item a:focus-visible .np-arrow{opacity:1;transform:none}
/* 14px rows (demo .menu-sublist): charcoal -> sage, 4px nudge */
.np-sub{margin-top:-12px}
.np-sub + .np-sub{margin-top:-16px}
.np-sub a,.np-sub button{font-size:14px;line-height:1.5;font-weight:400;color:var(--charcoal);background:none;border:0;padding:0;cursor:pointer;font-family:var(--sans);
  transition:color .2s cubic-bezier(.4,0,.2,1),padding-left .25s cubic-bezier(.16,1,.3,1)}
.np-sub a:hover,.np-sub button:hover,.np-sub a:focus-visible,.np-sub button:focus-visible{color:var(--sage-dark);padding-left:4px}
/* Icon rows (demo .plan-list): 14px/400, 21px icons, same hover nudge */
.np-icon a{display:inline-flex;align-items:center;gap:10px;font-size:14px;line-height:1.5;font-weight:400;color:var(--charcoal);
  transition:color .2s cubic-bezier(.4,0,.2,1),padding-left .25s cubic-bezier(.16,1,.3,1)}
.np-icon a:hover,.np-icon a:focus-visible{color:var(--sage-dark);padding-left:4px}
.np-icon svg{width:21px;height:21px;flex:0 0 21px}
.np-icon{margin-top:-4px}
.np-icon + .np-icon{margin-top:-14px}
.np-divider{width:100%;max-width:288px;height:2px;background:var(--warm-sand)}
/* Section label (demo .menu-label: plain small-caps text, not a chip) */
.np-tag span{font-weight:600;font-size:12px;line-height:1.2;letter-spacing:1.6px;text-transform:uppercase;color:var(--charcoal)}

/* Good-to-know circle — pops in 1s after the sheet (demo noteIn: scale .8,
   .62s fluid curve; Figma comment #94). Restarted with the row stagger. */
.np-circle{
  flex:0 0 auto;width:385px;max-width:40vw;aspect-ratio:1/1;border-radius:50%;margin-top:60px;
  background:var(--off-white);display:flex;flex-direction:column;align-items:center;justify-content:center;
  padding:0 54px;text-align:center;
  opacity:0;transform-origin:center;
  animation:sh-note-in 1.05s cubic-bezier(.16,1,.3,1) 1.15s both;
}
@keyframes sh-note-in{from{opacity:0;transform:scale(.8)}to{opacity:1;transform:scale(1)}}
.np-circle__tag{display:inline-block;background:var(--warm-sand);padding:6px 12px;margin-bottom:18px;font-weight:700;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--charcoal)}
.np-circle__head{font-size:19px;line-height:1.35;font-weight:400;color:var(--charcoal);margin:0 0 18px}
.np-circle__body{font-size:13px;line-height:1.5;color:var(--mid-grey);margin:0}

/* ---- Contact panel (demo .contact-menu + client-specified CHANGE toggle) --
   Initial state: full white popup, current office only, right-aligned.
   CHANGE reveals the cream "Find your local office" section below. */
.nav__panel--contact{padding:0;background:var(--off-white);display:flex;flex-direction:column;text-align:right;overflow-x:hidden}
.np-office-block{background:var(--off-white);padding:144px 48px 30px}
.np-office-eyebrow{display:flex;justify-content:flex-end;align-items:center;gap:6px;font-weight:600;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--charcoal);margin:0 0 10px}
.np-office-change{display:inline-flex;align-items:center;gap:6px;background:none;border:0;padding:0;font-family:var(--sans);font-weight:600;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--charcoal);cursor:pointer;transition:color .2s cubic-bezier(.4,0,.2,1)}
.np-office-change:hover,.np-office-change:focus-visible{color:var(--sage-dark)}
.np-office-change .np-chev{width:9px;height:6px;flex:0 0 auto;transition:transform .3s cubic-bezier(.16,1,.3,1)}
.nav__panel--contact.is-picking .np-office-change .np-chev{transform:rotate(180deg)}
.np-office-name{font-size:18px;font-weight:500;line-height:1.15;color:var(--charcoal);margin:0 0 4px}
.np-office-addr{font-size:14px;line-height:1.4;color:var(--mid-grey);margin:0}
.np-office-hours{font-size:14px;line-height:1.4;color:var(--mid-grey);margin:0 0 16px}
.np-office-meet-row{margin:0}
/* Client feedback, 25 Aug item 1: "Meet the team" now uses the button-studies
   hover — the underline fills from the left and the arrow advances, on the
   reference's own durations and curves. The old effect widened the gap, which
   moved the arrow's resting position; the brief asks for the animation only,
   so size, type, colour and spacing are all untouched. The amber rule stays
   exactly as it was at rest — the sweep paints over it. */
/* Feedback 31 Aug item 1: the hover was already the reference sweep but it was
   INVISIBLE - the rest border was var(--amber) and the ::after sweep is amber
   too, so gold filled over gold and nothing appeared to happen. The reference
   (button-studies .btn--underline) puts a faint var(--underline-base) rule
   underneath and sweeps var(--amber) over it; that base colour is the one
   change the brief authorises ("only update the hover animation/effect and
   button bottom border color"). The sweep is also brought to the same 1px
   weight as the rest rule and as every other .link-btn on the page, so the
   underline fills rather than thickening from 1px to 2px mid-hover. Size,
   typography, padding and position are untouched. */
.np-office-meet{position:relative;display:inline-flex;align-items:center;gap:8px;font-weight:600;font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--charcoal);padding-bottom:6px;border-bottom:1px solid var(--underline-base);transition:color .45s var(--ease-soft)}
.np-office-meet::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:1px;background:var(--amber);
  transform:scaleX(0);transform-origin:left center;transition:transform .5s var(--ease-fluid)}
.np-office-meet:hover::after,.np-office-meet:focus-visible::after{transform:scaleX(1)}
.np-office-meet .np-arrow{width:16px;height:16px;flex:0 0 16px;transition:transform .45s var(--ease-fluid)}
.np-office-meet:hover .np-arrow,.np-office-meet:focus-visible .np-arrow{transform:translateX(4px)}
.np-office-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;margin-top:22px}
/* Client feedback, 25 Aug item 1: the Email and Phone pills now carry the
   button-studies interaction 1:1, same as every .btn on the page — a 2px lift
   on the spring curve, the circular fill sweeping from the left, the layered
   lift shadow, and the quick press. Cream pill, so the sweep is warm sand,
   matching .btn--primary. Geometry, type and colour at rest are unchanged. */
.np-pill{position:relative;isolation:isolate;overflow:hidden;
  display:inline-flex;align-items:center;gap:6px;white-space:nowrap;background:var(--cream);--sweep:var(--warm-sand);box-shadow:0 1px 2px rgba(0,0,0,.15);border-radius:999px;padding:10px 13px;font-weight:600;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--charcoal);
  transition:transform .45s var(--ease-spring),box-shadow .55s var(--ease-fluid),color .45s var(--ease-soft)}
.np-pill::before{content:"";position:absolute;top:50%;left:-10%;width:250%;aspect-ratio:1;border-radius:50%;
  background:var(--sweep,transparent);transform:translate(-50%,-50%) scale(0);
  transition:transform .55s var(--ease-fluid);z-index:-1}
.np-pill:hover::before,.np-pill:focus-visible::before{transform:translate(-50%,-50%) scale(1)}
.np-pill:hover,.np-pill:focus-visible{transform:translateY(-2px);box-shadow:var(--lift-shadow)}
.np-pill:active{transform:translateY(0) scale(.95);transition-duration:.11s}
.np-pill svg{width:14px;height:12px;flex:0 0 auto}
/* Collapsible office list: 0fr -> 1fr grid rows, transparent -> cream */
.np-office-list{flex:1 1 auto;display:grid;grid-template-rows:0fr;background:transparent;
  transition:grid-template-rows .5s cubic-bezier(.16,1,.3,1),background-color .4s cubic-bezier(.4,0,.2,1)}
.nav__panel--contact.is-picking .np-office-list{grid-template-rows:1fr;background:var(--cream)}
.np-office-list__inner{overflow:hidden;min-height:0;visibility:hidden;padding:0 48px;
  transition:visibility 0s .5s,padding .5s cubic-bezier(.16,1,.3,1)}
.nav__panel--contact.is-picking .np-office-list__inner{visibility:visible;padding:28px 48px 48px;transition:visibility 0s 0s,padding .5s cubic-bezier(.16,1,.3,1)}
.np-office-list__title{font-size:18px;font-weight:500;line-height:1.15;color:var(--mid-grey);margin:0 0 16px}
.np-regions{display:flex;flex-direction:column;align-items:flex-end;gap:8px;margin:0;padding:0;list-style:none}
.np-regions li{display:flex;justify-content:flex-end}
.np-regions button{background:none;border:0;padding:0;font-family:var(--sans);font-size:14px;line-height:1.5;font-weight:400;color:var(--charcoal);cursor:pointer;
  transition:color .2s cubic-bezier(.4,0,.2,1),padding-right .25s cubic-bezier(.16,1,.3,1)}
.np-regions button:hover,.np-regions button:focus-visible{color:var(--sage-dark);padding-right:4px}

/* Reduced motion: keep the menus usable, drop the theatrics */
@media (prefers-reduced-motion:reduce){
  .nav__sheet,.nav__sheet.is-open{transition:none}
  .nav__panel,.nav__panel.is-current{transition:none}
  .nav__scrim,.np-office-list{transition:none}
  .nav__panel-list li,.np-office-block>*,.np-office-list__title,.np-regions li,.np-circle{animation-duration:.01ms;animation-delay:0ms}
  /* the fill still appears (durations collapse globally); movement does not */
  .np-pill:hover,.np-pill:focus-visible,.np-pill:active{transform:none}
  .np-office-meet:hover .np-arrow,.np-office-meet:focus-visible .np-arrow{transform:none}
}

/* ============================================================
   MOBILE NAVIGATION  (<1024px; Figma Mobile Nav 1101:71207)
   ============================================================ */
@media (max-width:1023px){
  /* Solid mobile chrome bar (Figma Mobile Page Chrome 126:789) */
  .nav::before{background:var(--cream)}
  .nav__inner{padding:16px 24px}
  .nav__logo img{height:31px;filter:brightness(0) invert(.26)}
  .nav__links,.nav__contact,.nav__save{display:none}
  .nav__sheet,.nav__scrim,.nav__underline{display:none!important}
  .nav__toggle{display:flex;align-items:center;justify-content:center;width:40px;height:40px;color:var(--sage);background:none;border:none;cursor:pointer}
  .nav__toggle svg{width:26px;height:26px}
  .nav.scrolled .nav__inner{padding-top:12px;padding-bottom:12px}
  .nav.scrolled .nav__logo img{height:28px}
}
@media (min-width:1024px){
  .mnav{display:none!important}
}

.mnav{
  position:fixed;inset:0;z-index:80;color:var(--off-white);
  /* Figma Mobile Nav (1101:70985): sage ground + leafy texture (multiply,
     32%) + a sage→transparent wash under the header. */
  background:linear-gradient(180deg,var(--sage) 0%,rgba(122,145,127,0) 163px),var(--sage);
  display:flex;flex-direction:column;
  opacity:0;transition:opacity .3s ease;
}
.mnav::before{
  content:"";position:absolute;inset:0;background:url("home/leafy-overlay.png") center/cover no-repeat;
  mix-blend-mode:multiply;opacity:.32;pointer-events:none;
}
.mnav>*{position:relative;z-index:1}
.mnav[hidden]{display:none}
.mnav.is-open{opacity:1}
html.mnav-lock,body.mnav-lock{overflow:hidden}

.mnav__chrome{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;flex:0 0 auto}
.mnav__logo img{height:31px;width:auto;display:block}
.mnav__close{background:none;border:0;color:var(--off-white);width:40px;height:40px;display:flex;align-items:center;justify-content:center;cursor:pointer}
.mnav__close svg{width:22px;height:22px}

.mnav__screens{position:relative;flex:1;overflow:hidden}
.mnav__screen{
  position:absolute;inset:0;padding:16px 24px 24px;overflow-y:auto;display:flex;flex-direction:column;
  transform:translateX(100%);visibility:hidden;
  transition:transform .35s cubic-bezier(.16,1,.3,1),visibility 0s .35s;
}
.mnav__screen.is-active{transform:none;visibility:visible;transition:transform .35s cubic-bezier(.16,1,.3,1),visibility 0s 0s}
.mnav__screen.is-left{transform:translateX(-24%);visibility:hidden;transition:transform .35s cubic-bezier(.16,1,.3,1),visibility 0s .35s}

.mnav__list{display:flex;flex-direction:column;gap:24px;align-items:stretch}
.mnav__row a,.mnav__row button{
  display:flex;align-items:center;justify-content:space-between;width:100%;text-align:left;
  background:none;border:0;padding:0;cursor:pointer;color:var(--off-white);
  font-family:var(--sans);font-weight:500;font-size:18px;line-height:1.3;
}
.mnav__row svg{width:8px;height:12px;flex:0 0 8px}
.mnav__row--sub a{font-size:15px;font-weight:400;color:rgba(253,251,249,.9)}
.mnav__row--icon a{justify-content:flex-start;gap:10px;font-size:15px}
.mnav__row--icon svg{width:20px;height:20px;flex:0 0 20px}
.mnav__divider{height:2px;background:rgba(253,251,249,.28);width:100%}
.mnav__tag span{display:inline-flex;background:var(--off-white);border-radius:4px;padding:4px 8px;font-weight:600;font-size:11px;letter-spacing:1.32px;text-transform:uppercase;color:var(--charcoal)}
.mnav__tag--crumb{margin:12px 0 20px}
.mnav__back{
  display:inline-flex;align-items:center;gap:8px;background:none;border:0;padding:0;cursor:pointer;
  color:var(--off-white);font-family:var(--sans);font-weight:600;font-size:11px;letter-spacing:1.32px;text-transform:uppercase;
}
.mnav__back svg{width:8px;height:12px}

/* Drawer footer row (Figma Mobile Nav / Overlay Footer): SAVED ITEMS pill on
   the left, round live-chat launcher on the right, bottom-aligned. */
.mnav__foot{margin-top:auto;padding:32px 0 25px;display:flex;align-items:flex-end;justify-content:space-between;gap:16px}
.mnav__saved{
  display:inline-flex;align-items:center;gap:8px;border:1px solid var(--white);border-radius:999px;
  padding:12px 20px;font-weight:600;font-size:12px;line-height:16px;letter-spacing:1.6px;text-transform:uppercase;color:var(--white);
  transition:background-color .3s var(--ease-soft);
}
.mnav__saved svg{width:16px;height:14px;flex:0 0 16px}
.mnav__saved-count{background:var(--amber);color:var(--off-white);border-radius:999px;min-width:16px;padding:2px 5px;font-size:10px;line-height:1.2;text-align:center}
/* Live chat launcher inside the drawer (Figma: 40px slate-green circle,
   20×16 chat glyph — the mobile menu's missing chat icon). */
.mnav__chat{
  width:40px;height:40px;border-radius:50%;background:var(--slate-green);border:0;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;color:var(--off-white);flex:0 0 40px;
  transition:transform .25s var(--ease-fluid);
}
.mnav__chat svg{width:20px;height:16px}
.mnav__chat:active{transform:scale(.92)}

/* Office contact block (Figma Mobile Nav / Office Contact — the mobile
   menu's "Contact Us" section) on a full-width slate-green band. */
.mnav__office{background:var(--slate-green);margin:0 -24px -24px;padding:24px 24px 24px;display:flex;flex-direction:column;gap:0}
.mnav__office-label{display:flex;align-items:center;gap:8px;font-weight:600;font-size:12px;line-height:1.2;letter-spacing:1.44px;text-transform:uppercase;color:var(--white);margin-bottom:15px}
.mnav__office-label button{display:inline-flex;align-items:center;gap:4px;background:var(--sage);border:0;border-radius:4px;padding:4px 4px 4px 8px;color:var(--off-white);font-family:var(--sans);font-weight:600;font-size:12px;line-height:1.2;letter-spacing:1.44px;text-transform:uppercase;cursor:pointer}
.mnav__office-label button svg{width:7px;height:10px}
.mnav__office-name{font-weight:500;font-size:18px;line-height:1.3;margin-bottom:12px}
.mnav__office-addr{font-size:14px;line-height:1.4;letter-spacing:.28px;color:var(--off-white);margin-bottom:12px}
.mnav__office-hours{display:none} /* Figma Office Contact shows name + address only */
.mnav__office-cta{display:flex;gap:14px;margin-top:0}
.mnav__office-cta .btn{gap:8px}
.mnav__office-cta .btn svg{width:14px;height:12px;flex:0 0 14px} /* same glyph scale as the desktop contact pills */
.mnav__office-cta .btn[data-office-phone]{flex:1 1 auto}

/* ============================================================
   FULL-PAGE SECTION SCROLLING (client comment #62 — gentle
   proximity snapping on desktop; disabled for reduced motion)
   ============================================================ */
/* Client round 18 (items 1+3): CSS scroll-snap REMOVED as the root cause of
   the jerking/stuck feel — proximity snap tugged at the page while wheel
   inertia was still running and while the Section Flow module was driving,
   so the browser and the assists fought over the scroll position. Native
   scrolling is now untouched; the Section Flow module in home.js handles
   section transitions AND a gentle after-idle settle (it only ever acts once
   scrolling has fully stopped, so nothing can fight or trap the user).
   scroll-padding stays: it keeps anchor jumps landing flush under the bar.
   home.js sets --sh-snap-pad = nav offset + the 92px compact bar height. */
html{scroll-padding-top:var(--sh-snap-pad,92px)}

/* Award-line rotation (client comment #41) */
/* Award rotation (client comment #41): a vertical PUSH — the incoming award
   slides in from above while the current one is pushed down and fully out of
   the clipped line frame. Both move together (same duration + easing), no fade,
   so it reads as one connected motion. Site-standard easing. */
.hero__recognition .rec-desc{position:relative;display:block;min-height:20px;overflow:hidden}
/* With absolute children the clip frame would shrink to zero width — hold it open. */
.hero__recognition .rec-desc.is-multi{width:100%}
.rec-award{display:block}
.rec-desc.is-multi .rec-award{position:absolute;right:0;top:0;white-space:nowrap;transform:translateY(-110%);transition:transform .6s cubic-bezier(.16,1,.3,1)}
.rec-desc.is-multi .rec-award.is-current{transform:none}
.rec-desc.is-multi .rec-award.is-leaving{transform:translateY(110%)}
@media (prefers-reduced-motion: reduce){
  .rec-desc.is-multi .rec-award{transition:none}
}

/* ============================================================
   FULL-VIEWPORT BANNERS (client feedback #9 — full-height image
   sections). Desktop only; tablet/mobile keep their existing
   layouts so nothing gets cropped by a forced 100vh. NOTE: the
   sticky "cover" pinning + scroll-linked scrim that briefly
   shipped here was REVERTED (AJ 19-Aug) — every section scrolls
   naturally and its background moves with it.
   ============================================================ */
@media (min-width:1024px){
  .image-section,.site-check,.footer-cta{min-height:100vh;min-height:100svh}
  /* round 21: Build With Confidence keeps the 929px Figma floor even on
     short viewports — below it the sky band gets too shallow for the text.
     Client feedback, 25 Aug item 3: the 100svh half of this floor is gone.
     On a tall window it forced the section past the Figma frame — 1080px on a
     1080px screen — and a min-height always beats a max-height, so the
     ceiling above could never take effect. The 929px Figma floor stays. */
  .section-image{min-height:929px}
  .blog{background:var(--off-white)}
}

/* ============================================================
   SCROLL-DRIVEN SECTIONS (client backend round, items 3/4/6).
   Desktop + motion-safe only: Build Options cards travel
   horizontally, the Beautifully Built testimonial column and the
   Featured Articles step vertically, while each section pins
   inside its own scroll runner (.sh-pin-wrap) and releases when
   its content is exhausted. Tablet/mobile keep their existing
   interactions untouched.
   ============================================================ */
@media (min-width:1024px) and (prefers-reduced-motion: no-preference){
  .sh-pin-wrap{position:relative}
  .sh-pin-wrap>.sh-pinned{position:sticky}

  /* Build Options: the strip is scrubbed by page scroll — no native
     scrollbar, no snap, cards keep their exact design and size.
     Round 20 items 2.3–2.5: the cards move inside a CLIPPED WINDOW whose
     left edge is the content container's own left padding (responsive,
     centred with the 1440 grid) — so the left alignment is identical at
     the beginning, middle and end of the scroll and cards can never touch
     the viewport's left edge. The window's right side extends to the
     viewport edge so the strip keeps its designed right-hand bleed. */
  /* Round 26 item 6: the window clipped the hover shadow 54px short at the
     bottom. Padding grows the clip box 48px above and below while the equal
     negative margins keep the layout byte-identical, so the shadow is fully
     visible and the LEFT clip — which round 20 requires to sit exactly on the
     content edge — is untouched. */
  .build.sh-hscroll .build__window{overflow:hidden;margin-left:48px;margin-right:min(0px,calc((var(--maxw) - 100vw)/2));padding-top:48px;padding-bottom:48px;margin-top:-48px;margin-bottom:-48px}
  .build.sh-hscroll .build__cards{overflow:visible;scroll-snap-type:none;scrollbar-width:auto;margin:0}
  .build.sh-hscroll .build__cards{will-change:transform}

  /* Beautifully Built: fixed visible window, the column travels through it. */
  .reviews.sh-vscroll .reviews__viewport{display:block;overflow:hidden}
  .reviews.sh-vscroll .reviews__col{will-change:transform}
  /* round 18 item 6: the card window runs to the section's bottom edge */
  /* Round 25 item 8.1: no top band either — the testimonial column starts
     flush with the section's top edge so the cards reach it as they rise. */
  .reviews.sh-vscroll .reviews__inner{padding-top:0;padding-bottom:0}

  /* Featured Articles: single-card window, column steps vertically; the
     active article's text + image cross-fade slowly (premium, no flicker). */
  /* Client round 13: the card column GLIDES 1:1 with the scroll (no stepped
     transition — that stepping read as jerking), showing one full card plus a
     partial next card (Figma 310:2136: 250px card, 16px gap, ~105px peek).
     The left hero/text crossfade is quick so nothing feels like a jump. */
  /* round 19: the card window sits flush on the section's bottom edge
     (Figma 310:2097 — the cards region ends exactly at the 929px band) */
  .blog.sh-steps .blog__right{height:929px;padding-bottom:0}
  .blog.sh-steps .blog__cards{margin-top:auto}
  .blog.sh-steps .blog__slider{overflow:hidden}
  .blog.sh-steps .blog__track{flex-direction:column;width:auto;gap:16px;transition:none;will-change:transform}
  .blog.sh-steps .blog__track .blog-card{flex:0 0 auto;width:100%}
  .blog.sh-steps .blog-card--wrap{display:flex}
  /* Round 26 item 11: the left image's opacity is now written every frame
     from the scroll position (see home.js), so it must NOT also be
     transitioned here — a transition would lag behind the scrub and undo the
     whole point of tying the fade to the scroll. */
  .blog.sh-steps [data-article-media] img,
  .blog.sh-steps [data-article-title],
  .blog.sh-steps [data-article-desc],
  .blog.sh-steps [data-article-link]{transition:none;will-change:opacity}
  /* The column cards' opacity is written per frame by home.js from each
     card's position in the window, so it must not also be transitioned here —
     that would lag the scroll. */
  .blog.sh-steps .blog__track .blog-card{will-change:opacity}
}

/* The testimonial viewport is invisible to layout everywhere except the
   pinned desktop mode (JS sizes it there). */
.reviews__viewport{display:contents}

/* ============================================================
   FULL-PAGE SECTION SNAPPING (client comment #62, round 28).

   Third attempt at this feature, and deliberately unlike the two
   that were rejected. Rounds 12 and 23 both used a JS "Section
   Flow" module that listened for wheel events, called
   preventDefault() and animated the page with scrollTo() - so the
   module and the browser were both driving the scroll position at
   once, which is what produced every "jerking / stopping /
   jumping / I have to scroll several times" report. That module is
   gone and is not coming back: nothing here touches the wheel, the
   trackpad, the keyboard or touch, and no JS moves the page.

   This is native CSS scroll-snap and nothing else. The browser
   keeps sole ownership of the scroll; it simply has a set of
   positions it prefers to come to rest on. Round 18 removed CSS
   snap as well, but for a cause that no longer exists - back then
   the proximity snap was fighting the Section Flow module for the
   scroll position. With one party driving instead of two, that
   specific conflict cannot recur.

   Why a marker rail rather than scroll-snap-align on the sections:
   the three scroll-LINKED sections (Build Options, Beautifully
   Built, Featured Articles) are position:sticky inside a tall
   runner, and the page's scroll offset INSIDE that runner is their
   animation timeline. Snap points at section edges only would let
   the browser rest at progress 0 or 1 and nowhere between, which
   would make the Build strip jump between its first and last card
   and the article stepper unusable. So home.js instead lays down a
   rail of 1x1 markers at every position worth resting on: each
   plain section's top, each CARD of the Build strip, each ARTICLE
   of the Featured strip, each testimonial step. The pinned
   sections GAIN card-to-card snapping instead of losing their
   scrub.

   STRENGTH - the one decision left, and it is a real trade-off.
   Measured on this page at 1440x900, where consecutive sections
   sit 808-1056px apart and one mouse-wheel notch travels ~98px:

     mandatory  A gesture must clear ~404px (5+ notches) to reach
                the next marker. Anything shorter is NEAREST to the
                marker it started from, so the browser returns the
                page there - the gesture is undone. A trackpad
                swipe carries momentum and clears it easily; a
                discrete mouse wheel often does not. Undoing a
                small wheel gesture is exactly the "I have to
                scroll several times" report from rounds 18-22.

     proximity  Small gestures are left alone (a 3-notch scroll
                holds where it lands); once a gesture ends NEAR a
                marker the page settles onto it. Never undoes
                anything, so it cannot reproduce that report, but
                it is not an absolute one-pager - a slow reader can
                come to rest between two sections.

   There is no CSS-only third option: making a short wheel notch
   advance a whole section requires intercepting the wheel, which
   is the Section Flow module that rounds 12 and 23 built and the
   client rejected both times. Adding closer markers would only
   trade the problem for resting mid-section.

   Set below, and switchable in one line - `mandatory` for the
   strict one-pager, `proximity` for never fighting the user:
       html{--sh-snap-strength:proximity}

   scroll-snap-stop stays `normal` either way, so a fast flick can
   still cross several sections in one gesture - the brief asks for
   elegant, not restrictive.
   ============================================================ */
@media (min-width:1024px) and (prefers-reduced-motion: no-preference){
  html.sh-snap{scroll-snap-type:y var(--sh-snap-strength,mandatory)}
  /* 1px wide, transparent, non-interactive. It must stay VISIBLE -
     visibility:hidden or display:none would disqualify the markers as snap
     areas - and height:0 keeps it out of layout entirely. */
  .sh-snap-rail{position:absolute;top:0;left:0;width:1px;height:0;pointer-events:none;z-index:-1}
  .sh-snap-rail>i{position:absolute;left:0;width:1px;height:1px;display:block;background:transparent;
    scroll-snap-align:start;scroll-snap-stop:normal}
}

/* ============================================================
   ROUND 31 — MOBILE QC (Home • Mobile Figma 1026:26776)
   ============================================================
   Everything from here down is inside @media (max-width:768px).
   The desktop homepage was locked at rev-19 on 31 Aug 2026, so no rule
   below this line may be reachable from a viewport of 1024px or wider —
   see desktop-lock/DESKTOP-LOCK.md and run
     python3 desktop-lock/desktop-projection.py --check
   which proves it by hashing the desktop-reachable subset of this file.

   This block sits at the END of the stylesheet on purpose: it must win
   over the earlier mobile rules (the max-width:1023px block at ~1240 and
   the max-width:768px block at ~1330) without editing them.
   ------------------------------------------------------------ */
@media (max-width:768px){

  /* ==========================================================
     §2  NAVIGATION & HEADER
     ==========================================================
     The client's complaint was that the menu "appears and disappears"
     instead of animating, and that the burger never becomes the close
     button. Both were structural, not a matter of tuning:

     1. There were TWO separate controls — .nav__toggle (burger, in the
        bar) and .mnav__close (X, inside the drawer) — so nothing could
        morph. The drawer also sits at z-index 80 above the bar's 50, so
        even a morphing burger would have been hidden behind it.
     2. .mnav[hidden]{display:none} means the open frame changes display
        and opacity together, and a transition cannot run across a
        display change.

     The fix keeps home.js untouched. It already sets aria-expanded on
     .nav__toggle and adds html.mnav-lock, which is everything the CSS
     below needs — and .mnav-lock on <html> is the only handle CSS has on
     the bar from the drawer's state, because .mnav is a FOLLOWING
     sibling of .nav and CSS cannot select backwards.
     ---------------------------------------------------------- */

  /* Header ground — client-specified #637468. */
  .nav::before{background:#637468;box-shadow:none}
  .nav.scrolled::before{background:#637468;box-shadow:0 4px 24px rgba(66,64,64,.14)}
  /* Logo and burger read light on the dark bar at every scroll position. */
  .nav__logo img,.nav.scrolled .nav__logo img{filter:brightness(0) invert(1)}
  .nav__toggle,.nav.scrolled .nav__toggle{color:var(--off-white)}

  /* Bar height 63px = Figma Nav 1026:26964 (16 + 31 logo + 16). The 40px
     toggle stays a 40px tap target and centres inside the shorter bar
     instead of dictating its height. */
  .nav__inner{height:63px;padding:0 24px;align-items:center}
  .nav.scrolled .nav__inner{padding-top:0;padding-bottom:0}
  .nav__logo img,.nav.scrolled .nav__logo img{height:31px}

  /* ---- Burger → close, one control that morphs ----
     Three bars from two pseudo-elements: ::before paints the top bar and
     casts a solid box-shadow 7px below it, which is the middle bar.
     Opening fades that shadow to transparent (a colour IS animatable,
     unlike box-shadow:none) and rotates the two real bars into an X.
     Motion is transform-only, so it composites. */
  .nav__toggle svg{display:none}
  .nav__toggle{position:relative}
  .nav__toggle::before,.nav__toggle::after{
    content:"";position:absolute;left:50%;top:50%;
    width:22px;height:1.8px;border-radius:2px;background:currentColor;
    transition:transform .42s var(--ease-fluid),box-shadow .24s linear;
  }
  .nav__toggle::before{transform:translate(-50%,calc(-50% - 7px));box-shadow:0 7px 0 currentColor}
  .nav__toggle::after{transform:translate(-50%,calc(-50% + 7px))}
  .nav__toggle[aria-expanded="true"]::before{transform:translate(-50%,-50%) rotate(45deg);box-shadow:0 7px 0 transparent}
  .nav__toggle[aria-expanded="true"]::after{transform:translate(-50%,-50%) rotate(-45deg)}

  /* ---- The bar becomes the drawer's chrome while the drawer is open ----
     Lifting .nav above the drawer and dropping the drawer's own chrome is
     what makes the morph visible: one button, one continuous animation,
     and no second logo. The bar's own ground goes transparent so the
     drawer's sage shows through and the two read as one surface. */
  html.mnav-lock .nav{z-index:90}
  html.mnav-lock .nav::before{background:transparent;box-shadow:none}
  .mnav__chrome{display:none}
  /* padding-top = the space the fixed bar now occupies.
     overflow:hidden is not cosmetic: round 31 keeps the drawer laid out
     even when closed (see the panel block below), and its inactive child
     screens sit at translateX(100%). Un-clipped, those extend the
     document's scrollable overflow and the page reports a scrollWidth far
     wider than the viewport — which is exactly the "unexpected horizontal
     page scrolling" the QC checks for. The drawer clips its own screens. */
  .mnav{padding-top:63px;overflow:hidden;max-width:100vw}

  /* ---- Panel open/close ----
     Two things are going on here, and the second one is a real bug fix.

     (a) No display swap mid-transition. .mnav keeps its flex layout at
         all times; visibility (not display) is what takes it out of the
         a11y tree and off the hit-testing path when closed, and
         visibility is delayed on the way out so the fade stays visible.
         300ms is deliberate: home.js sets hidden=true 320ms after it
         removes .is-open, so the fade has to finish inside that window.

     (b) Every state below is keyed off :not([hidden]) — the attribute
         home.js sets SYNCHRONOUSLY — and NOT off .is-open. openDrawer()
         adds .is-open inside a nested requestAnimationFrame, and rAF does
         not always run: it is frozen in a background or hidden tab and
         throttled during scroll momentum on iOS. When that callback is
         skipped the drawer used to end up unlocked-but-invisible, and a
         drawer that flicks between "not there" and "fully there" is
         precisely the "sudden appearance/disappearance" in the report.
         Driving the animation from the attribute means the transition
         runs on its own; .is-open, when it does arrive, only reaffirms
         what is already true. No JS change, no rAF dependency. */
  .mnav[hidden]{
    display:flex!important;
    opacity:0;visibility:hidden;pointer-events:none;
    transition:opacity .3s var(--ease-fluid),visibility 0s .3s;
  }
  .mnav{
    opacity:1;visibility:visible;
    transition:opacity .3s var(--ease-fluid),visibility 0s 0s;
  }

  /* Contents lift in behind the fade rather than simply being present. */
  .mnav__screens{
    opacity:0;transform:translateY(14px);
    transition:opacity .34s ease,transform .5s var(--ease-fluid);
  }
  .mnav:not([hidden]) .mnav__screens{opacity:1;transform:none}

  /* Root rows stagger in. The delays live only in the open state, so
     closing runs them all out together instead of in slow sequence. */
  .mnav__screen[data-screen="root"] .mnav__row,
  .mnav__screen[data-screen="root"] .mnav__foot,
  .mnav__screen[data-screen="root"] .mnav__office{
    opacity:0;transform:translateY(10px);
    transition:opacity .38s ease,transform .5s var(--ease-fluid);
  }
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__row,
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__foot,
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__office{opacity:1;transform:none}
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__row:nth-child(1){transition-delay:.08s}
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__row:nth-child(2){transition-delay:.12s}
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__row:nth-child(3){transition-delay:.16s}
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__row:nth-child(4){transition-delay:.2s}
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__row:nth-child(5){transition-delay:.24s}
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__row:nth-child(6){transition-delay:.28s}
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__foot{transition-delay:.32s}
  .mnav:not([hidden]) .mnav__screen[data-screen="root"] .mnav__office{transition-delay:.34s}

  /* ==========================================================
     §3  HERO / BANNER
     ==========================================================
     Spacing was checked against Figma with layout offsets (offsetTop),
     not getBoundingClientRect: the reveal animation carries a translateY
     on these elements, which makes every rect read ~22px low and looks
     exactly like a spacing bug. Measured properly, the hero already
     lands on Figma to the pixel — title 128, sub 218, buttons 291/343,
     photo 312–705 (Figma Y minus the 50px the status bar offsets the
     mobile frame by). So only two things actually needed changing.
     ---------------------------------------------------------- */

  /* "Find your style" fill: white, not cream. */
  .hero__actions .btn--primary{background-color:var(--white);--sweep:#F1EDE3}

  /* Award line wraps to TWO lines (Figma 1026:26798 is 354×40, and its
     layer name carries the break: "2026 House of the Year  Gold Award").
     The awards are absolutely positioned inside a clipped frame so the
     rotation can push one out as the next comes in, so the frame has to
     be told to be two lines tall — it cannot get there from content. The
     22ch cap is what forces the break, and it suits all three award
     strings ("…of the Year" 22, "…Master Builders" 20, "2024 Regional"). */
  .hero__recognition .rec-desc,
  .hero__recognition .rec-desc.is-multi{min-height:40px}
  .hero__recognition .rec-desc.is-multi .rec-award{
    white-space:normal;text-overflow:clip;overflow:hidden;
    width:100%;max-width:22ch;
  }

  /* ==========================================================
     §4  PLANS — CTA no longer collides with the plan drawing
     ==========================================================
     The drawing was absolutely positioned (right:19px, width:190px) over
     a text column that was free to grow to the full width. At 402 they
     happened to miss each other by 20px; at 360 the image's left edge
     lands at 151 and the "Explore plans" link runs to 173, so it sat
     under the drawing — which is why the client saw it and why the
     report asks to "check the layout across different mobile widths".

     Tuning the numbers would only move the width at which they touch.
     Making the column a real two-track grid removes the overlap as a
     possibility at every width, and reproduces the Figma geometry
     exactly at 402 (190px drawing, 19px right gutter, 49px drop).
     ---------------------------------------------------------- */
  .ideas__col--plans{
    display:grid;
    /* Track 2 is the drawing: 190px as designed, but never wider than the
       space left once the text column keeps the 173px its "Explore plans"
       link needs (149px intrinsic + the 24px gutter). Percentages here
       resolve against the grid's content box, hence 185 rather than 204.
         402 -> 190 (Figma exactly)   390 -> 186   375 -> 171   360 -> 156
       So the drawing gives way instead of the copy, at every width. */
    grid-template-columns:minmax(173px,1fr) min(190px,calc(100% - 185px));
    column-gap:12px;align-items:start;
    padding-right:19px;      /* Figma's right gutter, on the grid not the image */
  }
  .ideas__col--plans .ideas__text{grid-column:1;grid-row:1;width:auto;padding:96px 0 96px 24px}
  .ideas__plan-m{
    position:static;grid-column:2;grid-row:1;
    width:100%;height:auto;margin:49px 0 0;
    top:auto;right:auto;
  }

  /* ==========================================================
     §5  BUILD OPTIONS — small square cards, 2 + a peek,
         scrubbed horizontally by vertical scroll
     ==========================================================
     Figma stack 1026:26802 settles the geometry that the report only
     describes: six "small card" instances at 159x184 on a 171px step,
     i.e. a 12px gutter, inside a 354px Card Section — so the row is
     1014px wide in a 354px window and shows card 1 + card 2 + 24px of
     card 3 without anything being asked to. It also shows why the
     description and the in-card link have to go: 184px cannot hold an
     image, a title, a paragraph AND a link. And it puts the "All Build
     Options" button at y=447, BELOW the cards.
     ---------------------------------------------------------- */

  /* The button is not missing from the markup — it sits inside
     .build__head, above the cards. display:contents dissolves that
     wrapper so its two children become flex items of .build__inner and
     can be ordered around the card row. The wrapper's own 24px gutter
     goes with it, so both children carry it themselves. */
  /* Figma's stack is 583px tall and that total is not a coincidence:
       96 pad + 123 intro + 22 + 184 cards + 22 + 40 button + 96 pad = 583.
     Those are the numbers below. */
  .build__inner{display:flex;flex-direction:column;padding:96px 0 96px}
  .build__head{display:contents}
  .build__intro{order:1;padding:0 24px}
  .build__head .btn--white{order:3;align-self:flex-start;margin:22px 24px 0}
  .build__window{order:2}
  .build__trackrow{order:4}

  /* Cards: 159x184, square corners, image + title only. The body is
     flex:none and the image flex:1, so the two always sum to exactly
     184 whatever the title wraps to. */
  .build__cards{gap:12px;padding:22px 0 0;margin:0 24px}
  /* "2 complete cards + a small visible portion of the 3rd" has to hold at
     every width, so the card is a fraction of the row rather than a fixed
     159px. Solving 2C + 2gap + peek = row for C, with Figma's 12px gap and
     12px peek, gives C = (row - 36) / 2 — which lands on exactly 159px at
     402 (Figma's own number) and keeps the same composition below it. A
     fixed 159 gave only ONE whole card at 360 and no peek at all.
     The 159/184 ratio carries the height, so the proportion is constant. */
  .build__card{
    flex:0 0 calc((100% - 36px) / 2);width:calc((100% - 36px) / 2);
    height:auto;aspect-ratio:159 / 184;border-radius:0;
  }
  .build__card img{flex:1;min-height:0;height:auto;max-height:none;border-radius:0}
  .build__card-body{flex:none;padding:12px;gap:0}
  .build__card-title{font-size:15px;line-height:1.25}
  .build__card-reveal{display:none}      /* description + in-card link */

  /* Figma's mobile stack has no progress track — title, copy, cards,
     button. The scrub itself is the affordance. */
  .build__trackrow{display:none}

  /* ---- Vertical scroll -> horizontal card movement ----
     .build__inner sticks under the 63px bar while the page travels the
     extra length home.js adds to the section, and home.js maps that
     travel 1:1 onto the row's scrollLeft.

     Two things here are load-bearing:
       overflow:visible — the base rule is overflow:hidden, and an
         overflow:hidden ancestor is a scroll container, so a sticky
         child sticks inside IT and never sticks to the page at all.
       scroll-snap-type:none — the row is a real scroller with
         `scroll-snap-type:x mandatory` on desktop. Left on, every
         programmatic scrollLeft would be snapped to the nearest card
         and the motion would arrive in steps. That is the "jerking"
         failure mode, and this is where it would have come from. */
  .build{overflow:visible}
  .build__inner{position:sticky;top:63px}

  /* The scrub length is added as GENERATED CONTENT, not as padding, and
     that distinction is the whole reason the first attempt did not stick.
     Chrome measures how far a sticky box may travel against its parent's
     CONTENT box, not its padding box. Proven here rather than assumed: a
     10px test div injected as .build's first child stuck for exactly 587px
     — the content height at the time — and then began to slide, while
     1224px of padding-bottom bought it nothing. With .build__inner filling
     the content box, its travel was zero, so it tracked the page and the
     cards scrolled out of view while the row was still scrubbing.
     A ::after box is real in-flow content, so it extends the content box
     and gives the inner exactly that much room to hold. home.js sets the
     variable; at 0px the section behaves as it always did. */
  .build::after{content:"";display:block;height:var(--sh-build-scrub,0px)}
  /* And the reason the sticky above did not work on the first attempt:
     the base stylesheet sets body{overflow-x:hidden}. `hidden` on either
     axis makes an element a scroll container, and a sticky descendant
     sticks to ITS scrollport — which, on body, never scrolls, because the
     viewport is what moves. So the inner tracked the page instead of
     holding. `clip` does the same visual job (no horizontal page scroll)
     without creating a scroll container, so sticky reaches the viewport
     as intended. Mobile only; the base rule is desktop-reachable and
     locked. */
  body{overflow-x:clip}
  .build__cards{scroll-snap-type:none}
  .build__card{scroll-snap-align:none}

  /* ==========================================================
     §6  OUR STORY — no hard edge between photo and background,
         smaller signature
     ==========================================================
     Figma stacks photo-lifestyle-large (2127) and then starts the text
     block at 2457, i.e. 330px of photo before the copy — which is what
     the existing 320px height was already approximating. The reported
     problem is the EDGE: the photo stops dead and charcoal starts.

     The desktop treatment for that edge already exists —
     .founder__media::after — as a 90deg wash that melts the photo's
     right-hand side into the charcoal text column. Mobile currently
     switches it off (background:none) because the columns are stacked.
     Turning the same gradient through 90 degrees does the same job for
     the stacked layout: the photo's bottom dissolves into the section
     colour instead of butting against it.
     ---------------------------------------------------------- */
  .founder__media{height:330px}
  .founder__media::after{
    background:linear-gradient(180deg,
      rgba(66,64,64,0) 46%,
      rgba(66,64,64,.55) 78%,
      var(--charcoal) 100%);
  }
  /* Signature: 32.4px is a desktop size and dominated the mobile column.
     Selector matches the base rule's specificity — the declaration lives on
     `.founder__body .founder__sign`, so a bare `.founder__sign` loses. */
  .founder__body .founder__sign{font-size:24px;margin:24px 0 4px}

  /* ==========================================================
     §8  FIRST TIME BUILDING — Text then Image, charcoal dots
     ==========================================================
     Markup order is media-then-text (it has to be: the desktop layout
     puts the guide-book photo on the left). Flipping it for mobile with
     `order` rather than moving the HTML is what keeps the desktop lock
     intact — front-page-body.php has no media query, so a markup move
     would reorder every width.
     ---------------------------------------------------------- */
  .feature__text{order:1;padding:96px 24px 32px}
  .feature__media{order:2;position:relative;padding:0;overflow:hidden}
  /* Figma's photo is 430 wide at x=-13, i.e. full-bleed with 13px over each
     edge, and 771 tall. */
  .feature__media img{
    width:calc(100% + 26px);margin:0 -13px;max-width:none;
    height:auto;max-height:none;aspect-ratio:430/771;object-fit:cover;
  }
  /* The cream above melts into the top of the photo. */
  .feature__media::before{
    content:"";position:absolute;left:0;right:0;top:0;height:150px;z-index:1;pointer-events:none;
    background:linear-gradient(180deg,#F7F3EA 0%,rgba(247,243,234,.72) 38%,rgba(247,243,234,0) 100%);
  }
  /* Bullets: the green rounded-square tick is markup (an inline <svg> with
     a #7a917f rect), so it is hidden rather than edited, and the row gets a
     small charcoal dot of its own. */
  .feature__list li{align-items:flex-start;gap:10px;font-size:16px;line-height:1.5}
  .feature__list li svg{display:none}
  .feature__list li::before{
    content:"";flex:0 0 6px;width:6px;height:6px;border-radius:50%;
    background:var(--charcoal);margin-top:9px;
  }

  /* ==========================================================
     §9  BUILD VS BUY — Image then Text
     ==========================================================
     Same technique, opposite direction: the markup is text-then-media,
     so mobile orders the media first. Figma shows 330px of photo before
     the copy starts (6020 -> 6350), matching Our Story's rhythm.
     ---------------------------------------------------------- */
  /* flex:none matters: the base rule is flex:0 0 50%, and once the section
     is a COLUMN flex container that 50% is a height basis, which wins over
     the height declaration and left the photo at 268px. */
  .image-section__media{order:1;position:relative;flex:none;width:100%;height:330px;overflow:hidden}
  .image-section__text{order:2;padding:32px 24px 96px}
  /* The photo's bottom dissolves into the terracotta the copy sits on. */
  .image-section__media::after{
    content:"";position:absolute;left:0;right:0;bottom:0;height:150px;z-index:1;pointer-events:none;
    background:linear-gradient(180deg,rgba(123,74,57,0) 0%,rgba(123,74,57,.72) 58%,var(--terracotta) 100%);
  }

  /* ==========================================================
     §10 BUILD WITH CONFIDENCE — terracotta fades into the photo
     ==========================================================
     Figma puts the Text Block (402x488) and the photo (402x789) at the
     SAME y — the copy sits in the photo's sky, exactly as it does on
     desktop — so "Text -> Image" describes what you meet reading down
     the section, not two stacked blocks. The section keeps the photo as
     its ground; what was missing is the section's height (515 against
     Figma's 789) and the blend at the top, where the terracotta band
     above met the photograph on a hard line.

     STILL OUTSTANDING: "Remove the current image. Use the new image
     provided in the Figma design." The Figma node is
     photo-house-construction-hillside (1026:26777) and I cannot export
     it — the Figma MCP hit this seat's tool-call ceiling. Everything
     else in §10 is done; the photo swap is a one-line change to
     img_build_with_confidence_png in inc/sh-content-map.php once the
     asset is in hand.
     ---------------------------------------------------------- */
  /* 789/402 as a ratio, so the Figma proportion holds at every width. */
  .section-image{min-height:calc(100vw * 789 / 402)}
  .section-image__text{
    width:100%;align-items:flex-start;text-align:left;
    padding:96px 24px 0;gap:16px;
  }
  .section-image__bg::after{
    content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
    background:linear-gradient(180deg,
      var(--terracotta) 0%,
      rgba(123,74,57,.82) 14%,
      rgba(123,74,57,.34) 34%,
      rgba(123,74,57,0) 56%);
  }
  /* Figma 1026:26828 — 75x103 at x=303, so 24px in from the right edge. */
  .section-image__mb{right:24px;width:75px;height:103px}

  /* ==========================================================
     §7  BEAUTIFULLY BUILT — photo + headline hold still while
         the testimonials scroll up over them
     ==========================================================
     Figma: photo-homeowner-at-entrance 402x798 at the section top, the
     two-line "beautifully / built" headline sitting in its lower third,
     then three review cards on a charcoal ground.

     The brief is a pin: "the picture and text should remain fixed while
     the testimonial cards scroll upward and over the section", with the
     area under the cards "fading gradually to charcoal".

     Done with position:sticky and nothing else moving. The photo block is
     sticky, so it holds under the bar; the cards are ordinary flow after
     it, so they rise over it as the page scrolls — the cards are the only
     thing that moves, which is exactly what stops this reading as two
     competing animations. home.js only supplies a number (--sh-rev-fade);
     it never positions anything.

     The headline is the awkward one: it is a SIBLING of the photo, not a
     child, so it cannot simply be placed inside the sticky block. It is
     made sticky too, with a negative top margin and a matching bottom
     margin, so it contributes exactly ZERO height to the flow while
     rendering over the photo's lower third and holding at the same time
     the photo does. The three numbers below are Figma's: the headline is
     154px tall (3654 -> 3808) and its bottom sits 57px above the photo's
     bottom edge, out of 798.
     ---------------------------------------------------------- */
  .reviews{
    /* Stage = Figma's 798/402 proportion, but never taller than the space
       under the bar, so the whole pin is visible on a short phone. */
    --sh-rev-stage:min(calc(100vw * 798 / 402),calc(100svh - 63px));
    --sh-rev-tag-h:calc(var(--sh-rev-stage) * 0.193);   /* 154/798 */
    --sh-rev-tag-gap:calc(var(--sh-rev-stage) * 0.0714);/*  57/798 */
    min-height:0;background:var(--charcoal);
    overflow:visible;   /* overflow:hidden would trap the sticky children */
  }

  /* The photo: sticky, and the only element that consumes stage height. */
  .reviews__bg{
    /* inset:auto FIRST. The base rule is inset:0, and `inset` is a
       shorthand for top/right/bottom/left — written after top:63px it
       resets it to auto and the block silently stops sticking. */
    position:sticky;inset:auto;top:63px;z-index:0;
    height:var(--sh-rev-stage);overflow:hidden;
  }
  .reviews__bg img{width:100%;height:100%;object-fit:cover;object-position:center 28%}
  /* Static blend: the photo's bottom always melts into the charcoal. */
  .reviews__bg::before{
    content:"";position:absolute;left:0;right:0;bottom:0;height:42%;z-index:1;pointer-events:none;
    background:linear-gradient(180deg,rgba(66,64,64,0) 0%,rgba(66,64,64,.55) 52%,var(--charcoal) 100%);
  }
  /* Scroll-driven wash: deepens to charcoal as the cards cover the photo.
     home.js writes --sh-rev-fade; at 0 this layer is invisible, so the
     section still looks right with JS disabled. */
  .reviews__bg::after{
    content:"";position:absolute;inset:0;z-index:2;pointer-events:none;
    background:var(--charcoal);opacity:var(--sh-rev-fade,0);
  }

  /* "Remove the current large faded background area appearing above the
     cards" — this is that area. The blending it used to do is now done by
     the two gradients above, which are tied to the photo rather than
     washing the whole section. */
  .reviews__overlay{display:none}

  /* Headline. padding-left + a negative text-indent is what reproduces
     Figma's indented second line ("beautifully" starts at 38, "built" at
     170) from a single unbroken string of markup: text-indent moves only
     the first line, so the first line starts at 170-132=38 and every line
     after it at 170. */
  .reviews__tagline{
    position:sticky;
    top:calc(63px + var(--sh-rev-stage) - var(--sh-rev-tag-h) - var(--sh-rev-tag-gap));
    margin-top:calc(-1 * (var(--sh-rev-tag-h) + var(--sh-rev-tag-gap)));
    margin-bottom:var(--sh-rev-tag-gap);
    height:var(--sh-rev-tag-h);
    left:0;right:0;bottom:auto;z-index:2;
    padding:0 0 0 170px;text-indent:-132px;text-align:left;
    white-space:normal;font-size:64px;line-height:.95;
  }

  /* Cards: ordinary flow, painted above the pin, Figma's 354/24 rhythm. */
  .reviews__inner{position:relative;z-index:3;display:block;padding:24px 24px 96px}
  .reviews__viewport{overflow:visible;height:auto;max-height:none}
  .reviews__col{width:100%;gap:24px}
  .review-card{width:100%}

  /* ==========================================================
     §11 FIND YOUR LOCAL OFFICE — one #AECCD5 ground,
         Heading -> Search -> Map -> Cards
     ==========================================================
     Figma orders it heading (7691) -> search (7746) -> map (7794) ->
     cards (8354), and paints the whole thing on ONE background fill
     402x1757 — which is the "single solid background colour" and the
     "remove the visible colour separation" asks in one.

     The order is awkward in markup terms: the map is a SIBLING of the
     panel, while the heading, search and cards are all INSIDE the panel,
     so no amount of `order` on the two top-level children can interleave
     them. display:contents dissolves the panel so all five become items
     of .local and can be ordered freely — the same trick as the Build
     Options button, and again with no markup change.
     ---------------------------------------------------------- */
  .local{
    display:flex;flex-direction:column;min-height:0;
    background:#aeccd5;gap:24px;padding:96px 24px 50px;
  }
  .local__panel{display:contents}
  .local__title{order:1;font-size:36px}
  .local__search{order:2}
  .local__search-msg{order:3}
  /* width:auto matters — an earlier mobile rule pins this to width:100%,
     which would hold the box at 354 and make the negative margins do
     nothing. Full-bleed here so the 378px map box below fits without the
     section's gutters clipping 12px off each side of New Zealand. */
  .local__map{order:4;width:auto;margin:0 -24px}
  .local__list{order:5}

  /* The seam was the artwork, not the section: nz-map.svg is a portrait
     717x974 raster and the mobile band cropped it with object-fit:cover,
     so its own edge cut across the band. Sizing the box to the artwork's
     exact aspect and using `contain` shows the whole map with no cut at
     all, keeps the % marker positions locked to it (they are positioned
     against this box), and leaves only #AECCD5 either side — which is
     the same colour as everything around it, so there is nothing to see.
     Figma's 560px-wide bleed is deliberately not reproduced: it would
     crop the map again for no gain. */
  /* The box takes the artwork's own 717:974 ratio and the section's height
     follows it, so the map is never cropped and never letterboxed — which
     is what keeps the % marker positions exactly on their cities. Capping
     at 100% matters below 402: a hard 378px box would have been clipped by
     9px on each side at 360 and taken Northland and Fiordland with it.
     513px tall at 402, scaling down from there. */
  .local__map{flex:none;height:auto;overflow:hidden;background:#aeccd5}
  .local__mapfit{position:static;inset:auto;display:flex;justify-content:center;overflow:hidden}
  .local__mapbox{
    position:relative;inset:auto;flex:none;
    width:min(calc(513px * 717 / 974),100%);aspect-ratio:717 / 974;height:auto;
  }
  .local__map img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}

  /* ==========================================================
     §12 COMPLIMENTARY — copy sits in the sky, not on the people
     ==========================================================
     Figma: photo-family-outside-home 434x772 with the Text Block over
     its TOP 446px — i.e. the copy lives in the sky band and the family
     is below it. The section was only 506px tall, which pulled the crop
     in tight and left the text sitting across the subjects.
     ---------------------------------------------------------- */
  .site-check{min-height:calc(100vw * 772 / 402)}
  /* Anchor the crop to the top of the frame so the sky is what the copy
     sits on and the people stay in the lower half. */
  .site-check__bg img{object-position:center top}
  /* Top-aligned, or the base layout centres the copy vertically and it
     lands back on the family. */
  .site-check{align-items:flex-start;justify-content:flex-start}
  .site-check__text{
    width:auto;max-width:none;flex:1;justify-content:flex-start;
    align-items:flex-start;text-align:left;padding:96px 24px 0;gap:16px;
  }
  /* Legibility scrim. This copy is CHARCOAL (.site-check__label/title/body
     all are, on desktop too), so the sky needs LIFTING, not darkening — a
     dark wash here would have made it harder to read, not easier. A soft
     off-white veil over the top of the frame only, fading out well before
     the family. */
  .site-check__bg::after{
    content:"";position:absolute;left:0;right:0;top:0;height:62%;pointer-events:none;
    background:linear-gradient(180deg,rgba(253,251,249,.55) 0%,rgba(253,251,249,.28) 54%,rgba(253,251,249,0) 100%);
  }

  /* ==========================================================
     §13 OUR PROCESS — the white circle, sliding between steps
     ==========================================================
     The circle was not styled away by accident: the existing mobile block
     carries `.step__num::before{display:none}`, so it has been switched
     off on mobile all along. That is the missing circle.

     Figma has ONE decorative-circle (279x279 at x=-124, bleeding off the
     left edge) sitting behind the numerals, and the brief asks for it to
     "move/slide down through the section as the user scrolls" while
     staying "aligned with the corresponding process step". So it is one
     circle, not three: a single generated box on .process__steps that
     home.js moves to whichever step is currently in view.

     It glides on a transition rather than tracking the scroll pixel for
     pixel — the brief wants it aligned WITH a step, so it should settle
     on one and travel to the next, not drift continuously between them.
     transform, not top/left, so the movement composites.
     ---------------------------------------------------------- */
  .process__steps{position:relative}
  .process__steps::before{
    content:"";position:absolute;left:0;top:0;z-index:0;pointer-events:none;
    width:180px;height:180px;border-radius:50%;background:var(--white);
    transform:translate(var(--sh-proc-x,0px),var(--sh-proc-y,0px));
    transition:transform .62s var(--ease-fluid);
    opacity:var(--sh-proc-on,0);
  }
  /* .step keeps z-index auto, so it creates no stacking context and the
     numerals and copy paint above the circle on document order alone. */
  .step{position:relative}
  .step__num{position:relative;z-index:1}

  /* ==========================================================
     §14 FEATURED ARTICLE — copy and photo hold, cards pass over
         and fade out
     ==========================================================
     Figma Blog Section 402x2014: slate-green band 0-344, photo 344-747,
     the text block over the top of both (0-472), then three 354x430
     cards on a 446 step from 659.

     Same pin technique as §7, with one extra wrinkle: the three things
     involved are nested as .blog__inner > (.blog__media + .blog__right >
     (.blog__feature + .blog__cards)), so the cards are siblings of the
     copy INSIDE the part that has to stay still. Two display:contents
     declarations flatten both wrappers so all four become items of .blog
     and the cards can be ordered out of the pinned pair — no markup
     change, which is what keeps the desktop lock intact.

     The copy pins at the bar, the photo pins 344px lower (Figma's own
     offset), and its -128px top margin reproduces the overlap where the
     copy's last lines sit over the top of the photograph.
     ---------------------------------------------------------- */
  .blog{
    position:relative;display:flex;flex-direction:column;
    background:var(--slate-green);overflow:visible;
  }
  .blog__inner,.blog__right{display:contents}
  .blog__band{display:none}   /* .blog's own ground replaces it */

  .blog__feature{order:1;position:sticky;top:63px;z-index:2;padding:96px 24px 24px;gap:16px}
  .blog__media{
    order:2;position:sticky;top:calc(63px + 344px);z-index:1;
    flex:none;width:100%;height:403px;margin-top:-128px;overflow:hidden;
  }
  .blog__media img{width:100%;height:100%;object-fit:cover}
  /* Both edges of the photograph dissolve into the slate green. */
  .blog__media::before,.blog__media::after{
    content:"";position:absolute;left:0;right:0;height:120px;z-index:1;pointer-events:none;
  }
  .blog__media::before{top:0;background:linear-gradient(180deg,#637468 0%,rgba(99,116,104,.6) 46%,rgba(99,116,104,0) 100%)}
  .blog__media::after{bottom:0;background:linear-gradient(0deg,#637468 0%,rgba(99,116,104,.6) 46%,rgba(99,116,104,0) 100%)}

  /* Cards: a vertical stack that rises over the pin. The desktop/tablet
     slider drives .blog__track with an inline transform, so the override
     needs !important to beat it — the track is a column here and must
     not be slid sideways. */
  .blog__cards{order:3;position:relative;z-index:3;padding:24px 24px 96px;gap:16px;min-width:0}
  .blog__slider{overflow:visible;max-width:100%}
  .blog__track{flex-direction:column;gap:16px;transform:none!important;transition:none}
  .blog__track .blog-card{flex:0 0 auto;width:100%;min-width:0}
  /* Each card's opacity is published by home.js as it leaves the frame. */
  .blog-card{opacity:var(--sh-card-o,1)}
  .blog__dots{display:none}   /* not in the Figma mobile composition */

  /* ==========================================================
     §15 READY TO TAKE THE FIRST STEP — Text then Image
     ==========================================================
     Figma: the Footer CTA frame is 403x274 of copy, and the photo
     (403x813) sits BELOW it — 274 + 813 = 1087, which is exactly the
     14316->15403 span. So the copy is on a colour, not over the
     photograph, which is also what "the top of the image should
     fade/blend into the background colour" requires: there has to be a
     background colour above the image for it to fade into.

     That also settles "reposition the text so it does not overlap
     people's faces" in the most reliable way available — the text is no
     longer over the photograph at all.

     The ground is charcoal, the colour of the footer directly below, so
     the copy block and the footer read as one dark frame with the
     photograph set between them.
     ---------------------------------------------------------- */
  .footer-cta{
    display:flex;flex-direction:column;min-height:0;
    background:var(--charcoal);overflow:hidden;
  }
  .footer-cta__text{
    order:1;position:relative;z-index:2;width:auto;
    justify-content:flex-start;gap:16px;padding:96px 24px 78px;
  }
  .footer-cta__bg{
    order:2;position:relative;inset:auto;z-index:0;
    width:100%;height:calc(100vw * 813 / 403);overflow:hidden;
  }
  /* The photograph's top edge dissolves into the charcoal above it. */
  .footer-cta__bg::after{
    content:"";position:absolute;left:0;right:0;top:0;height:170px;z-index:1;pointer-events:none;
    background:linear-gradient(180deg,var(--charcoal) 0%,rgba(66,64,64,.62) 42%,rgba(66,64,64,0) 100%);
  }
  /* The multiply wash existed to hold the copy legible ON the photo.
     With the copy on its own ground it would only mute the picture. */
  .footer-cta__overlay{display:none}
  /* Figma centres the single action (172px button at x=91.5 of 355).
     Both CTAs are kept — dropping one would remove a live link the
     client has not asked to lose — centred and stacked. */
  .footer-cta__actions{align-items:center}

  /* ==========================================================
     §16 FOOTER — logo, one-line brand line, stacked links
     ==========================================================
     Required order: logo -> "Your life. Beautifully built." -> Call Us +
     Email -> Get in Touch -> navigation links.

     Two things needed solving without touching site-footer.php (it is
     shared by every page, and markup has no media query):

     1. There is no logo element in the footer markup at all. It is drawn
        as .footer__inner::before — a real block in the flow that can be
        ordered like any other child, filling the container width as
        asked, and inverted to white because assets/home/logo.svg is the
        dark artwork (the nav does the same thing to it).

     2. Call Us, Email and Get in Touch are wrapped in .footer__contact,
        so they cannot be interleaved with their surroundings. display:
        contents dissolves it and lets all three take their own place.
        nth-of-type still resolves against .footer__contact, because
        display:contents changes boxes, not the DOM: item 1 is Email,
        item 2 is Call Us.
     ---------------------------------------------------------- */
  .footer__inner{padding:64px 24px;gap:40px}
  .footer__inner::before{
    content:"";order:0;display:block;width:100%;
    aspect-ratio:242 / 56;                       /* Figma logo 242.06x55.76 */
    background:url("home/logo.svg") no-repeat center/contain;
    filter:brightness(0) invert(1);
  }
  .footer__brand{order:1}
  /* One line, full width. The clamp's 44px FLOOR is what forced the wrap:
     8.64cqi is the factor round 25b measured as filling the container on
     one line, and at a 354px container that is ~30px. Dropping the floor
     keeps the designed proportion instead of overriding it with a fixed
     mobile size. */
  .footer__brand-title{font-size:8.64cqi;line-height:1.1}
  .footer__brand-text{margin-top:16px}
  .footer__contact{display:contents}
  .footer__contact-item:nth-of-type(2){order:2}   /* Call us */
  .footer__contact-item:nth-of-type(1){order:3}   /* Email */
  .footer__contact .btn{order:4;align-self:flex-start}
  .footer__contact-item{align-items:baseline;gap:10px}
  /* "Currently the navigation links are arranged side-by-side; this
     should be corrected." The existing mobile rule puts them in two 42%
     columns — one stacked column instead. */
  .footer__nav{order:5;flex-direction:column;flex-wrap:nowrap;gap:32px}
  .footer__col{flex:none;width:100%}
  .footer__midrow{order:6}
  .footer__crumbs{order:7}
  .footer__bottom{order:8}
}

/* The logged-in admin bar pushes the fixed bar down, so the sticky offset
   has to follow it. WP only defines this variable when the bar is present,
   and uses 46px at this width (see the max-width:782px block above). */
@media (max-width:768px){
  body.admin-bar .build__inner{top:calc(63px + var(--wp-admin--admin-bar--height,46px))}
}

/* ============================================================
   ROUND 32 — client feedback, 1 Sep 2026 (Google Doc "Sentinel
   Mobile 1"). Same contract as round 31: every rule below lives
   inside @media (max-width:768px), the desktop and tablet
   projections must not change, and no markup is edited.
   Reference for §1: https://sentinel-demo-423.netlify.app/
   (burger spring morph, circle clip-path reveal, cascading rows —
   easings and delays lifted from its stylesheet verbatim).
   ------------------------------------------------------------ */
@media (max-width:768px){

  /* ==========================================================
     R32 §1  MOBILE NAV — reference-matched animation
     ==========================================================
     The reference opens its overlay with a clip-path circle that
     grows out of the burger (0% -> 150%, .55s, cubic-bezier(.22,1,
     .36,1)) and closes by shrinking it back. Both directions are
     driven here off html.mnav-lock, which home.js adds AND removes
     synchronously in openDrawer()/closeDrawer() — unlike [hidden],
     which is only set 320ms later on close, and unlike .is-open,
     which arrives inside a nested rAF that can be skipped. So the
     close animation starts the instant the X is tapped, with no JS
     change. [hidden] keeps doing what round 31 made it do
     (display:flex!important + delayed visibility for a11y); the
     .6s visibility delay outlives the 320ms timeout + nothing
     visible after the circle hits 0%. */
  .mnav{
    opacity:1;visibility:visible;
    clip-path:circle(0% at calc(100% - 44px) 31px);
    transition:clip-path .55s cubic-bezier(.22,1,.36,1),visibility 0s 0s;
  }
  .mnav[hidden]{
    opacity:1;visibility:hidden;
    clip-path:circle(0% at calc(100% - 44px) 31px);
    transition:clip-path .55s cubic-bezier(.22,1,.36,1),visibility 0s .6s;
  }
  html.mnav-lock .mnav{clip-path:circle(150% at calc(100% - 44px) 31px)}
  /* (100% - 44px, 31px) is the burger's centre: bar 63px tall,
     40px toggle flush to the 24px gutter. The circle must grow out
     of the control that was tapped or the effect reads as a wipe. */

  /* The bar stays above the drawer at all times so the burger→X
     morph is visible over the collapsing circle on close too.
     While the drawer is open the bar ground is transparent
     (round 31), so the two still read as one sage surface; the
     ground now fades back in rather than popping. */
  .nav{z-index:90}
  .nav::before{transition:background-color .35s ease,box-shadow .35s ease}

  /* Burger: reference geometry and motion. Three 29x3 rounded bars
     (ours are two pseudo-bars + a box-shadow middle), spring curve
     cubic-bezier(.34,1.56,.64,1) over .5s, and the reference's
     exact stagger — opening: top bar 0s, middle 60ms, bottom 100ms;
     closing: 80ms / 40ms / 0s. The middle bar is a box-shadow, so
     it fades (a colour is animatable) instead of scaling away. */
  .nav__toggle::before,.nav__toggle::after{
    width:29px;height:3px;border-radius:999px;
    transition:transform .5s cubic-bezier(.34,1.56,.64,1),box-shadow .35s ease;
  }
  .nav__toggle::before{
    transform:translate(-50%,calc(-50% - 9px));
    box-shadow:0 9px 0 currentColor;
    transition-delay:.08s,.04s;
  }
  .nav__toggle::after{
    transform:translate(-50%,calc(-50% + 9px));
    transition-delay:0s,0s;
  }
  .nav__toggle[aria-expanded="true"]::before{
    transform:translate(-50%,-50%) rotate(45deg);
    box-shadow:0 9px 0 transparent;
    transition-delay:0s,.06s;
  }
  .nav__toggle[aria-expanded="true"]::after{
    transform:translate(-50%,-50%) rotate(-45deg);
    transition-delay:.1s,0s;
  }

  /* Contents: the circle is the reveal, so the screens wrapper no
     longer lifts (round 31 slid it 14px). Root rows cascade in from
     the right — translate(18px), .65s on the site's fluid curve —
     starting .5s after the tap and stepping 80ms per row; the foot
     and office cards rise 32px on the slower .75s clock. Delays
     live only in the open state so closing retracts everything at
     once, exactly like the reference. */
  .mnav__screens{opacity:1;transform:none;transition:none}
  .mnav__screen[data-screen="root"] .mnav__row{
    opacity:0;transform:translateX(18px);
    transition:opacity .65s var(--ease-fluid),transform .65s var(--ease-fluid);
  }
  .mnav__screen[data-screen="root"] .mnav__foot,
  .mnav__screen[data-screen="root"] .mnav__office{
    opacity:0;transform:translateY(32px);
    transition:opacity .75s var(--ease-fluid),transform .75s var(--ease-fluid);
  }
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__row,
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__foot,
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__office{opacity:1;transform:none}
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__row:nth-child(1){transition-delay:.5s}
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__row:nth-child(2){transition-delay:.58s}
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__row:nth-child(3){transition-delay:.66s}
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__row:nth-child(4){transition-delay:.74s}
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__row:nth-child(5){transition-delay:.82s}
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__row:nth-child(6){transition-delay:.9s}
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__foot{transition-delay:.98s}
  html.mnav-lock .mnav .mnav__screen[data-screen="root"] .mnav__office{transition-delay:1.06s}

  /* ==========================================================
     R32 §2  BANNER BUTTONS — flush with the content's left edge
     ==========================================================
     .hero__actions sits inside the 24px-padded hero copy AND
     carried its own 24px padding, so the pills rendered at x=48
     while the headline sits at 24. The container already provides
     the gutter; the actions stop double-paying it. */
  .hero__actions{padding-left:0;padding-right:0}

  /* ==========================================================
     R32 §3  IDEAS — equal top and bottom spacing
     ==========================================================
     The text column is already 64px/64px; the imbalance was the
     base min-height:348px stretching the band 19px past the
     column's natural 329px, all of it landing at the bottom. */
  .ideas__col--style{min-height:0}

  /* ==========================================================
     R32 §4  PLANS — equal top and bottom spacing
     ==========================================================
     Measured at 402: the first visible thing is the plan drawing,
     49px down (its round-31 margin); the last is its bottom edge,
     72px above the band's end. Trimming the text column's bottom
     padding 96 -> 73 puts exactly 49px under the drawing too. */
  .ideas__col--plans .ideas__text{padding:96px 0 73px 24px}

  /* ==========================================================
     R32 §5  BUILD OPTIONS
     ==========================================================
     (a) Fixed background. The topo-line artwork lived on
     .build::before, i.e. on the SECTION, which keeps scrolling
     behind the pinned .build__inner during the scrub — that is the
     vertical background movement in the report. The artwork moves
     onto the sticky inner itself, so it pins with the content and
     cannot move; the section behind stays FLAT warm-sand, so the
     only thing still travelling back there is a solid colour, which
     reads as perfectly still.
     (b) Compact: 48px pads replace the 96px ones, so the title sits
     48px under the bar at rest and the button 48px above the
     frame's edge — the two red boxes in the report. */
  .build::before{display:none}
  .build__inner{
    padding:48px 0;
    background:var(--warm-sand) url("home/graphics.png") no-repeat center top;
    background-size:cover;
  }

  /* (c) "2 complete cards + a small visible portion of the 3rd,
     extending all the way to the right edge, no unwanted gap" and
     (d) the clipped hover shadow — one geometry change covers both.
     The row was margin:0 24px, so it CLIPPED at x=378: the 12px
     peek stopped there and the remaining 24px of warm-sand read as
     the gap; the same edge (and the row's top/bottom) ate the hover
     shadow. The gutters move inside as padding — the scrollport now
     reaches the screen edge, so card 3 runs 366 -> 402 (a 36px peek
     touching the edge), while cards still rest at x=24 and the
     card width formula still resolves against the same 354px box,
     keeping Figma's 159px cards. 48px of vertical padding (pulled
     back with equal negative margins) gives the 40px-blur hover
     shadow room on every side it can reach. */
  .build__cards{margin:-26px 0;padding:48px 24px;gap:12px}

  /* (e) Card names: slightly smaller, and every title reserves two
     lines so short names get the same body height as long ones —
     the image (flex:1) absorbs the constant remainder, so all six
     cards stay pixel-identical in height and alignment. */
  .build__card-title{font-size:13px;line-height:1.3;min-height:2.6em}

  /* ==========================================================
     R32 §6  OUR STORY — the full founder, head to hands
     ==========================================================
     Figma shows the photo at its full height (643 tall on the 402
     frame) with the copy overlapping its darkened lower 313px. The
     330px strip was what cropped him. The box takes Figma's 402:643
     proportion; the raster (897x800) then fits its full height with
     only a side crop, and 43% keeps the man (his centre sits at 47%
     of the frame) centred in the window with head and hands whole.
     The copy pulls up over the photo by Figma's own 313px and reads
     off the same dark gradient, whose stops move up to be solid
     where the text begins. */
  .founder__media{height:calc(100vw * 643 / 402)}
  .founder__media img{object-position:43% 50%}
  .founder__media::after{
    background:linear-gradient(180deg,
      rgba(66,64,64,0) 26%,
      rgba(66,64,64,.62) 48%,
      var(--charcoal) 70%);
  }
  .founder__text{
    position:relative;z-index:1;
    margin-top:calc(-1 * (100vw * 313 / 402));
    padding:0 24px 64px;
  }

  /* ==========================================================
     R32 §7  BEAUTIFULLY BUILT — the lady in the middle
     ==========================================================
     Reviews.jpg is 1440x913 and the lady stands at 65% of its
     width; the 402x798 stage shows a 32% window, so centring her
     needs object-position 71% (0.65·1258 − 201 over 1258 − 402).
     28% vertical kept from round 31 for short-viewport crops. */
  .reviews__bg img{object-position:71% 28%}

  /* ==========================================================
     R32 §8  BUILD WITH CONFIDENCE — clean break, whole image
     ==========================================================
     Two problems, one cause: round 31 washed the section's top with
     terracotta so it melted into Build vs Buy above (the reported
     blending), and object-fit:cover on the square 2048 raster threw
     away half its width. Now: the wash is gone — terracotta above,
     sky below, one crisp line — and the image renders at its full
     width, bottom-anchored (no left/right crop at all). The band
     above the photograph is painted in the photograph's own sky,
     sampled from its top rows (#0b60b2, deepening slightly upward),
     so the paint and the pixels meet invisibly at 49% and the copy
     and Master Builders mark sit on deep sky. Content, layout and
     typography untouched. */
  .section-image{background:#0b60b2}
  .section-image__bg{
    display:flex;align-items:flex-end;
    background:linear-gradient(180deg,#0959a8 0%,#0b60b2 46%,#0b60b2 100%);
  }
  .section-image__bg img{height:auto;object-fit:contain;object-position:center bottom}
  /* The paint/pixels join sits at 49% of the box ((789-402)/789 at
     every width, since both scale with vw). The photograph's top row
     is not one uniform blue edge to edge, so the ::after that used to
     carry the terracotta wash now feathers the ground colour a few
     percent down INTO the sky instead — solid on the join, gone by
     56% — which dissolves the seam without touching the copy (the
     text block is a sibling stacked above this layer). */
  .section-image__bg::after{background:linear-gradient(180deg,
    rgba(11,96,178,0) 0%,
    rgba(11,96,178,0) 44%,
    #0b60b2 48.5%,
    #0b60b2 49.5%,
    rgba(11,96,178,0) 56%)}

  /* ==========================================================
     R32 §9  OUR PROCESS — photos right, numerals off the edge
     ==========================================================
     The images centred at x=48 and ran under the sliding white
     circle (which hugs the numerals on the left). Right-aligned and
     bled to the screen edge (Figma puts every step photo against or
     past the right edge) they clear the circle's reach entirely:
     the circle ends where the photos now begin. The numerals lose
     their gutter and slide 40px left, cropping a small slice off
     the screen edge exactly as the Figma frame does; the step text
     that follows them lands at x≈48, Figma's own 51. The circle JS
     needs no change — it centres on the numeral wherever it is. */
  /* overflow:clip (not hidden - no scroll container) so the
     translated images below stop at the screen edge instead of
     widening the document's scrollable overflow. */
  .step__media{justify-content:flex-end;padding:0;margin:0 -24px 0 0;overflow:clip}
  /* The three PNGs are 650x650 with the section's cream baked in as
     their ground, so even flush right the SUBJECT reads centred. Each
     image slides right by its own baked-in padding (measured per
     file: the artwork ends at ~81% / ~82% / ~92% of the canvas), so
     the subject itself sits on the right screen edge as in the Figma
     frame. transform, not margin: it never changes layout, and the
     page-level overflow-x:clip already contains the spill. */
  .step:nth-child(1) .step__media img{transform:translateX(21%)}
  .step:nth-child(2) .step__media img{transform:translateX(20%)}
  .step:nth-child(3) .step__media img{transform:translateX(10%)}
  .step__num{margin-left:-40px}

  /* ==========================================================
     R32 §10  FEATURED ARTICLE — #FDFBF9 above, #CBCAC5 below
     ==========================================================
     The pinned copy carries an off-white crown that fades into the
     section over its first 180px (it pins with the copy, so the
     blend never shifts); the photograph's bottom now dissolves into
     the card ground's grey instead of slate; and the cards bring
     their own #CBCAC5 ground with a feathered leading edge, riding
     88px up over the photo (Figma's own overlap) so grey is what
     the photo always melts into. */
  .blog__feature{background:linear-gradient(180deg,#FDFBF9 0%,rgba(253,251,249,0) 180px)}
  .blog__media::after{
    background:linear-gradient(0deg,#CBCAC5 0%,rgba(203,202,197,.6) 46%,rgba(203,202,197,0) 100%);
  }
  .blog__cards{
    margin-top:-88px;
    background:linear-gradient(180deg,rgba(203,202,197,0) 0,#CBCAC5 120px);
  }

  /* ==========================================================
     R32 §11  FAQ — solid #F7F3EA, amber rule under the heading
     ==========================================================
     The background photograph and its cream wash go; the section
     sits on the requested solid. The underline takes the site's
     75x2 divider signature in the requested #E8B639. */
  .faq{background:#F7F3EA}
  .faq__bg,.faq__overlay{display:none}
  .faq__title::after{
    content:"";display:block;width:75px;height:2px;
    background:#E8B639;margin-top:14px;
  }

  /* ==========================================================
     R32 §12  TAKE THE FIRST STEP — slate ground, centred copy
     ==========================================================
     The charcoal ground read brown against the photograph and is
     not what the frame shows: the Figma comp sits the copy on the
     site's slate green. Copy and buttons centre; the photo keeps
     its full height and crops symmetrically into the family —
     their centre sits at 72% of the 2688-wide raster, and the
     813-tall window shows 28% of it, hence 81%. The top-of-photo
     dissolve is repainted in the new ground's colour. */
  .footer-cta{background:var(--slate-green)}
  .footer-cta__text{align-items:center;text-align:center}
  .footer-cta__bg img{object-position:81% 50%}
  .footer-cta__bg::after{
    background:linear-gradient(180deg,var(--slate-green) 0%,rgba(99,116,104,.62) 42%,rgba(99,116,104,0) 100%);
  }

  /* ==========================================================
     R32 §13  FOOTER — tighter contact block, aligned values
     ==========================================================
     The dissolved contact items are spaced by .footer__inner's
     40px gap; margins pull Email to 12px under Call Us and the
     button to 24px under Email (40−28, 40−16). A fixed label
     column lines the phone number and the address up on one edge,
     as the comp shows, and the button spans the container. */
  .footer__contact-item .lbl{flex:0 0 74px}
  .footer__contact-item:nth-of-type(1){margin-top:-28px}
  .footer__contact .btn{
    margin-top:-16px;
    align-self:stretch;width:100%;justify-content:center;
  }
}

/* ============================================================
   ROUND 33 — client feedback, 1 Sep 2026 (second pass; the doc's
   screenshots show rev-22 was reviewed). Same contract: every rule
   lives inside @media (max-width:768px); desktop and tablet
   projections must not move; templates untouched. Works with the
   three round-33 home.js changes (scrub retired, card fade re-keyed,
   R33a/b/c modules).
   ------------------------------------------------------------ */
@media (max-width:768px){

  /* ==========================================================
     R33 §1  NAV — burger morph without the shadow swing
     ==========================================================
     "The icon must stay in the exact same position; no jumping."
     The only part of the r32 morph that could read as a jump is the
     middle bar: it is ::before's box-shadow, so while it fades it
     also ROTATES with its parent, swinging around the icon. Now it
     is gone almost immediately on open (.12s) and only reappears at
     the very end of the close (.18s after .32s), so no visibly
     rotated middle bar ever exists. The bars themselves already
     rotate about the icon's exact centre. */
  .nav__toggle::before{
    transition:transform .5s cubic-bezier(.34,1.56,.64,1) .08s,
               box-shadow .18s ease .32s;
  }
  .nav__toggle[aria-expanded="true"]::before{
    transition:transform .5s cubic-bezier(.34,1.56,.64,1) 0s,
               box-shadow .12s ease 0s;
  }

  /* ==========================================================
     R33 §2  NAV — About Us: folded children, chevrons, crumbs
     ==========================================================
     The R33c module builds the third-level screens and the chevron
     buttons; this block is their entire mobile appearance. The
     inline --sub rows and the group dividers fold away below 768px
     only, so the tablet drawer keeps its current inline list. */
  .mnav__screen[data-screen="about-us"] .mnav__row--foldable{display:none}
  .mnav__screen[data-screen="about-us"] .mnav__divider{display:none}

  .mnav__row--haschild{display:flex;align-items:center;gap:12px}
  /* The base .mnav__row a/button rule makes every child a 100%-wide
     space-between row - which would put the injected chevron on its own
     line. These selectors outrank it. */
  .mnav__row--haschild>a{flex:1 1 auto;width:auto;min-width:0}
  .mnav__row--haschild>.mnav__subopen{
    flex:0 0 36px;display:inline-flex;align-items:center;justify-content:center;
    width:36px;height:36px;margin:-8px -10px -8px 0;
    background:none;border:0;padding:0;cursor:pointer;color:var(--off-white);
  }
  .mnav__row--haschild>.mnav__subopen svg{width:8px;height:12px;flex:0 0 8px}

  /* Breadcrumb pills — reference: parent pill on warm sand, current
     pill on cream, 11px/600 uppercase, 4px radius, cream chevron. The
     base .mnav__tag span rule would also pill-style the separator
     (it is a span inside a .mnav__tag), so it is reset explicitly. */
  .mnav__crumbs{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
  .mnav__crumbs .mnav__crumb{
    display:inline-flex;align-items:center;border:0;border-radius:4px;padding:4px 8px;
    font-family:var(--sans);font-weight:600;font-size:11px;letter-spacing:1.32px;
    text-transform:uppercase;color:var(--charcoal);line-height:1.2;
  }
  .mnav__crumbs .mnav__crumb--parent{background:var(--warm-sand);cursor:pointer;transition:opacity .3s ease}
  .mnav__crumbs .mnav__crumb--parent:hover,
  .mnav__crumbs .mnav__crumb--parent:focus-visible{opacity:.85}
  .mnav__crumbs .mnav__crumb--current{background:var(--off-white)}
  .mnav__crumbs .mnav__crumb-sep{
    display:inline-flex;background:none;border-radius:0;padding:0;color:var(--off-white);
  }
  .mnav__crumbs .mnav__crumb-sep svg{width:8px;height:12px}

  /* ==========================================================
     R33 §3  BUILD OPTIONS — natural height, swipe-only row
     ==========================================================
     The scroll-driven scrub STAYS (retired for a few hours on 1 Sep,
     restored at AJ's direction): .build__inner keeps round 31's sticky
     pin and the ::after spacer keeps carrying --sh-build-scrub, so
     vertical scroll drives the row exactly as rev-22 shipped. Round 31's
     scroll-snap-type:none also still stands - snap would quantise the
     programmatic scrollLeft writes into steps. What this round changes:
     net spacing 22px above the row and 24px between the row and ALL
     BUILD OPTIONS (their "reduce the vertical gap"), 48px section pads
     via the inner rule in home.css's round-32 block, and 4px of extra
     interior gutter each side (padding 28 / margin -4) so the hover
     shadow's 28px horizontal reach clears the scrollport edges. */
  /* Natural height, NOT a viewport-filling frame: a min-height frame
     left its full empty tail on screen after the scrub released - the
     "why huge spacing?" gap before Our Story. The inner is content-tall
     (comp spacing: 48 above the title, 96 under the button), so when the
     pin releases the button sits exactly 96px above Our Story.

     While the section HOLDS, though, the viewport is taller than the
     frame, and whatever shows beneath the button must (a) not move -
     "keep the background completely fixed" - and (b) not read as dead
     flat space (the follow-up screenshots). So the topo ground lives on
     a ::before of the STICKY inner, stretched to viewport height: it
     rides with the pin, pixel-frozen, covering the whole hold, and Our
     Story (lifted one stacking level) slides up OVER it as the scrub
     completes. One layer, no seams, nothing scrolls behind the cards. */
  .build__inner{padding:48px 0 96px;background:none}
  .build__inner::before{
    content:"";position:absolute;left:0;right:0;top:0;z-index:-1;pointer-events:none;
    height:max(100%,calc(100svh - 63px));
    background:var(--warm-sand) url("home/graphics.png") no-repeat center top;
    background-size:cover;
  }
  .founder{position:relative;z-index:2}
  .build__cards{margin:-26px -4px -46px;padding:48px 28px}
  /* The tablet auto-carousel clones the card set for its seamless loop
     (aria-hidden copies). With auto-rotation standing down on mobile the
     copies would just repeat the deck at the end of the swipe. */
  .build__card[aria-hidden="true"]{display:none}

  /* ==========================================================
     R33 §4  OUR STORY — wider framing, no lead-in blank
     ==========================================================
     The "blank space above the background image" was the flat
     warm-sand band under the pinned Build frame; retiring the scrub
     removes it, and the founder photo is the section's first box, so
     Our Story now opens on the photograph. Framing: rev-22's 160vw
     stage showed the man head-to-hands but close up (56% of the
     raster's width). The comp shows him smaller, with wood either
     side: a 109vw stage scales the photo down (fits its full 800px
     height at scale .547) and widens the window to 82% of the
     raster, and 33% centres his 47%-of-frame position in it. The
     copy keeps Figma's proportion of overlap (213/438 = the same
     48.6% of the photo as 313/643 was). */
  .founder__media{height:calc(100vw * 438 / 402)}
  .founder__media img{object-position:33% 50%}
  .founder__text{margin-top:calc(-1 * (100vw * 213 / 402))}

  /* ==========================================================
     R33 §5  BUILD WITH CONFIDENCE — #739ac2
     ==========================================================
     Same geometry as round 32 (full-width, bottom-anchored image;
     paint above; feathered join at 49%) with the paint swapped to
     the requested #739ac2 and a longer, softer tail below the join:
     the photograph's own sky is deeper than the new paint, so the
     feather now lets go over ~20% of the box instead of 7%, reading
     as haze rather than a line. Everything else is untouched. */
  .section-image{background:#739ac2}
  .section-image__bg{background:linear-gradient(180deg,#7da3c9 0%,#739ac2 42%,#739ac2 100%)}
  .section-image__bg::after{background:linear-gradient(180deg,
    rgba(115,154,194,0) 0%,
    rgba(115,154,194,0) 36%,
    #739ac2 46%,
    #739ac2 50%,
    rgba(115,154,194,0) 69%)}

  /* ==========================================================
     R33 §6  OUR PROCESS — text left, image right, side by side
     ==========================================================
     "The images should not appear above the content." Each step
     becomes one grid cell shared by both children: the body (numeral
     + text) sits on top at z1 filling the left, the media aligns to
     the right edge with its round-32 bleed compensation. Image files,
     typography, colours and content are unchanged; the images simply
     scale to the comp's visible widths (subject ≈ 197 / 175 / 190 px
     at 402 once each PNG's baked-in cream padding is discounted). */
  .process__steps{gap:40px}
  .step{display:grid;grid-template-columns:100%}
  .step__media{grid-area:1/1;align-self:start;height:auto}
  .step__media img{max-height:none;height:auto}
  /* Column geometry straight from the comp (1 Sep, evening screenshots:
     "content should not overlap the image"). The visible subjects sit at
     x 205 / 227 / 214 of 402 with the text block ending at ~226, so the
     text column takes the comp's own 175px measure (its lines wrap the
     same way: "Find / Your Style") and each image is sized so its
     SUBJECT - not its cream canvas - starts where the text stops:
     subject widths 197 / 175 / 190 at 402, canvases scaled up by each
     PNG's padding share, right edges flush with the screen. Everything
     is vw-proportional, so wider phones keep the same two-column fit. */
  .step:nth-child(1) .step__media img{width:calc(100vw * 281/402);transform:translateX(19%)}
  .step:nth-child(2) .step__media img{width:calc(100vw * 213/402);transform:translateX(18%)}
  .step:nth-child(3) .step__media img{width:calc(100vw * 226/402);transform:translateX(9%)}
  .step__body{grid-area:1/1;position:relative;z-index:1;align-self:start;padding-top:24px}
  .step__text{flex:0 1 calc(100vw * 175/402);min-width:0}

  /* ==========================================================
     R33 §7  FEATURED ARTICLE — the green goes, the blends stay
     ==========================================================
     "No hard edge or green tint": every remaining slate-green paint
     in this section is replaced. The ground becomes the requested
     #CBCAC5; the pinned copy's crown fades #FDFBF9 over its first
     ~220px; the photo pins 120px under the bar (was 344) so the copy
     reads against the photograph the way the comp shows, and its
     lead-in fade is off-white instead of green. The photo's bottom
     fade and the cards' feathered #CBCAC5 ground are already grey
     (round 32) and stay. Card fade timing is retuned in home.js. */
  .blog{background:#CBCAC5}
  /* AJ's Figma capture (1 Sep, evening) settles what the low-res comp
     hid: the featured copy is CHARCOAL on an opaque cream panel
     (#FDFBF9 warming slightly downward) whose bottom edge alone
     dissolves into the photograph - not off-white text on a wash. So:
     dark type, 36px under the bar, an opaque panel that fades out only
     across its final 96px (all of it below READ MORE - the block's
     bottom padding grows to carry the fade), and the photo's lead-in
     veil becomes the panel's own cream. No smoke, no green, and no
     gradient band behind the eyebrow. */
  .blog__feature{
    padding-top:36px;
    padding-bottom:96px;
    background:linear-gradient(180deg,
      #FDFBF9 0%,
      #FAF6EF 55%,
      #F5EFE5 calc(100% - 96px),
      rgba(245,239,229,0) 100%);
  }
  .blog__label,.blog__title,.blog__desc{color:var(--charcoal)}
  .blog__feature .link-btn{color:var(--charcoal);border-bottom-color:var(--underline-base)}
  .blog__media{top:calc(63px + 120px);margin-top:-280px}
  .blog__media img{object-position:center 72%}
  .blog__media::before{top:0;height:120px;background:linear-gradient(180deg,
    rgba(245,239,229,.9) 0%,
    rgba(245,239,229,0) 100%)}

  /* ==========================================================
     R33 §8  READY TO TAKE THE FIRST STEP — #63A3E3, one button
     ==========================================================
     Ground and photo fade move from slate green to the requested
     #63A3E3; the eyebrow and the second button fold away; the first
     button becomes GET IN TOUCH via the R33b module (label and
     destination borrowed from the footer's own button). 88% centres
     the house rather than the family group per "the main
     subject/building is centered". */
  .footer-cta{background:#63A3E3}
  .footer-cta__bg::after{background:linear-gradient(180deg,#63A3E3 0%,rgba(99,163,227,.6) 46%,rgba(99,163,227,0) 100%)}
  .footer-cta__label{display:none}
  .footer-cta__actions .btn:nth-child(2){display:none}
  .footer-cta__bg img{object-position:88% 50%}
}

/* ============================================================
   REV-24 DEMO VARIANT — SECTION SNAPPING DISABLED SITE-WIDE.
   Temporary build for client demonstration only; never deploy as
   a production rev. The snap system (round 18+) is a marker rail
   armed by html.sh-snap — this rule makes that class inert at
   every width, leaving plain smooth scrolling. home.js runs
   unmodified; the class it toggles simply no longer snaps.
   Anchor offsets (scroll-padding-top) and the Build Options card
   carousel's own x-axis snap are unrelated to section snapping
   and are kept.
   ------------------------------------------------------------ */
html.sh-snap{scroll-snap-type:none!important}
html{scroll-snap-type:none!important}
