/* ===========================================================================
   Seymour's Window Washing — home page prototype
   Built to brand guideline V6 (2026-08-09).

   Rules enforced from page 09:
     · Radius 0 everywhere. ⚠️ Departs from V6: p03/p09 round the frosted glass
       panels (--sw-radius-glass) — squared on request 2026-08-11, so V7 has to
       drop that token or the guideline and the site disagree.
     · No shadows — separation is a hairline rule or a change of surface
     · Links underlined
     · Body copy is 400
     · Headlines ALWAYS CAPS at Light 300, +0.14em, never bold
     · Oxblood once or twice per view, and never in the same view as Bronco blue
   =========================================================================== */

/* --------------------------------------------------------------- reset --- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

/* Branded scrollbar, everywhere a track appears: the page itself, the
   lightbox panel, the mobile drawer, any future overflow box. The bare
   ::-webkit-scrollbar rule (no element in front of it) is what makes this
   apply globally in Chrome/Safari/Edge rather than one container at a time.
   Firefox gets the same two colors through scrollbar-color, which cannot
   carry a hover state or a border, so it stays visually close rather than
   identical. Square, per this file's own radius-0 rule -- no border-radius
   on the thumb. Navy-mid on hover, not oxblood: the track sits on bronco
   tint, and oxblood is never supposed to share a view with bronco blue. */
html { scrollbar-width: thin; scrollbar-color: var(--sw-navy) var(--sw-bronco-tint); }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--sw-bronco-tint); }
::-webkit-scrollbar-thumb {
  background: var(--sw-navy);
  border: 3px solid var(--sw-bronco-tint);      /* creates the thumb's inset without a border-radius */
}
::-webkit-scrollbar-thumb:hover { background: var(--sw-navy-mid); }
::-webkit-scrollbar-corner { background: var(--sw-bronco-tint); }
body {
  margin: 0;
  background: var(--sw-white);
  color: var(--sw-text);
  font-family: var(--sw-font-ui);
  font-weight: var(--sw-weight-body);
  font-size: 17px;
  line-height: var(--sw-leading-body);
  -webkit-font-smoothing: antialiased;
}
img, video { display: block; max-width: 100%; }
img { height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }
figure, blockquote, dl, dd, fieldset { margin: 0; }

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

/* ========================================================== PAGE GROUND ===
   There is no backdrop layer. The body is one flat white surface and every
   light section sits directly on it, so there is no seam anywhere to see.

   Five things were tried here and all five were removed: a field of marks
   whose arms opened on scroll, the same marks wired into a node mesh, one
   oversized etched mark per section, blurred light bands, and a tonal wash
   with mullion rules. Each one read as something applied to the page.
   README.md records why each went.

   On this brand the ground does nothing. Structure comes from space, from
   type at scale, and from the navy blocks. That is what the two references
   Aaron gave — Ralph Lauren and LVMH — actually do.
   =========================================================================== */

/* --- The ground: a monogram field ---------------------------------------
   Tiled four-point marks, LV-style: one at the center of each tile and smaller
   ones on every corner and edge midpoint. Tiled, those edge marks JOIN with
   their neighbours into whole marks — which is what makes a monogram read as a
   continuous field rather than as separate stamps sitting in boxes.

   Replaces the window-light treatment and the single revealed sparkle, both
   removed 2026-08-17.

   Bronco at 12% lands ~14.6/255 over the shaded ground: present when you look
   at it, not a pattern you read. Navy was tried and is far too heavy — even at
   5% it hits 22/255 and the page turns into wallpaper.
   ------------------------------------------------------------------------- */
.frost {
  position: fixed; inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;

  /* How present the whole ground is, and how far the white panels sit above it.
       .90  37.9/255
       .72  30.6/255   <- here
       .55  23.6/255   panels barely lifted off it */
  --frost-shade: rgba(228, 237, 244, .72);

  background-color: var(--sw-white);
  background-image:
    url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20width='200'%20height='200'%20viewBox='0%200%20200%20200'><g%20fill='none'%20stroke='%2393BDD8'%20stroke-opacity='0.32'%20stroke-width='1'><path%20d='M0%20100L100%200L200%20100L100%20200Z'/><path%20d='M22%20100L100%2022L178%20100L100%20178Z'/><path%20d='M100%200V100'/></g></svg>"),
    linear-gradient(160deg, var(--frost-shade) 0%, rgba(228,237,244,.42) 38%, var(--frost-shade) 100%);
  /* Deco lattice: nested diamonds with a stem off each apex, drawn here rather
     than licensed. The reference Aaron sent was a 3572x5000 Freepik poster with
     a baked-in gradient and edges 21-27 levels apart, so it could not tile, and
     its free license wants a visible credit on a site we hand to the client.
     The motif is ordinary Deco vocabulary; this construction is ours.

     stroke-opacity is the only knob that matters. Over the wash it measures:
       .55  41.2/255   too loud, the ground starts competing
       .32  24.2/255   <- here
       .12   9.2/255   what the old monogram measured, near invisible
     A lattice covers far more area than sparse marks did, so it reads heavier
     than the same number suggested before. Drop toward .24 if it crowds.

     Stays put while the page scrolls over it: a ground is a surface, not a
     parallax layer. */
  background-size: 200px 200px, cover;
  background-repeat: repeat, no-repeat;
  background-position: center top, center top;
}

@media (max-width: 620px) {
  /* Tighter tile on a phone or the marks read as oversized confetti. */
  .frost { background-size: 128px 128px, cover; }
}

/* --- Beams ---------------------------------------------------------------
   Brand port of the canvas "BeamsBackground" component. NAVY BLOCKS ONLY.

   The beams are additive light, so the ground decides whether they exist:
     white body ...........   0.0 / 255   <- nothing, at any settings
     bronco tint body .....  28.7
     midnight navy ........ 128.6         <- what it is drawn for
   Putting these on the light body would repeat the white-sheen failure, so the
   canvas is scoped to .proof / .section--invert / .footer and the ground stays
   marble.

   The host needs a stacking context or the canvas paints over the copy. Content
   inside these blocks already sits in .wrap, which gets lifted above it. */
.beams {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
  z-index: 0;
  pointer-events: none;
  /* One blur pass. The source ran ctx.filter blur(35px) AND a CSS blur(15px)
     over the same pixels every frame, which is two full-canvas blurs per frame
     for no visual gain the first one did not already give. */
  filter: blur(26px);
  /* The single dial for how apparent the beams are. Was .85, which read as an
     effect; at .55 the navy blocks have depth without anyone noticing why. */
  opacity: .55;
}

/* In the hero the beams light the FOOTAGE rather than sit on top of it, so they
   blend rather than overlay — a flat wash over video reads as a filter, screen
   reads as light in the scene.

   Lower again than the navy blocks: the reel is already busy and already the
   brightest thing on the page, so it needs the least help.

   They deliberately sit UNDER .hero__scrim. The scrim runs 80%+ navy across the
   left third where the copy sits, so under 10% of the beam brightness reaches
   the type — hero contrast is unchanged. Above the scrim they would eat it. */
.beams--hero {
  opacity: .34;
  mix-blend-mode: screen;
  filter: blur(34px);
}
@media (max-width: 860px) {
  /* Mobile scrim is a single vertical gradient and lighter through the middle,
     so there is less headroom before the type is affected. */
  .beams--hero { opacity: .24; }
}
.section--invert,
.footer { position: relative; isolation: isolate; }
.section--invert > .wrap,
.footer > .wrap { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  /* beams.js paints one static frame and stops; nothing to do here. */
}

/* ---------------------------------------------------------- primitives --- */
/* Full width, no centered container. Reading measure is capped on the text
   blocks themselves (--sw-measure), so a wide screen buys presence rather
   than 200-character lines. */
.wrap {
  width: 100%;
  padding-inline: var(--sw-gutter-page);
}

.eyebrow {
  font-size: 10px;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: var(--sw-track-eyebrow);
  text-transform: uppercase;
  margin: 0 0 var(--sw-space-5);
  color: var(--sw-navy);
}
.eyebrow--invert { color: var(--sw-mist); }

/* Display type: caps, Light 300, wide. Weight is never the emphasis.
   No text-indent: letter-spacing pads the RIGHT of each glyph, so the left
   edge already sits on the margin. Indenting by the tracking value pushes
   every heading 0.14em off the column its body copy aligns to. */
h1, h2, h3, h4 {
  font-family: var(--sw-font-display);
  font-weight: var(--sw-weight-display);
  text-transform: uppercase;
  letter-spacing: var(--sw-track-display);
  margin: 0;
  text-wrap: balance;
}
h1 { font-size: clamp(34px, 5.2vw, 64px); line-height: 1.10; }
h2 { font-size: clamp(26px, 3.4vw, 40px); line-height: 1.12; }
/* Card titles sit at the 17px floor where 300 goes fragile — so 400. */
h3 { font-size: clamp(17px, 1.6vw, 22px); line-height: 1.24; letter-spacing: 0.16em; font-weight: 400; }
h4 { font-size: 13px; line-height: 1.3; letter-spacing: var(--sw-track-label); font-weight: 400; }

p { margin: 0 0 var(--sw-space-5); }
p:last-child { margin-bottom: 0; }
.lede { font-size: 19px; }
.muted { color: var(--sw-text-muted); }

a { color: inherit; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.rule { height: 1px; background: var(--sw-border); border: 0; margin: 0; }

/* Light sections carry no background of their own — they are the body. Only
   the navy blocks break the white, and they do it at full bleed and full
   strength rather than as a tint, so there is never a soft band edge.

   Vertical rhythm is deliberately generous — in this aesthetic the space is
   the luxury, and it is the one thing a contractor template never buys. */
/* The hero is framed too, so it reads as a panel on the ground rather than a
   full-bleed banner — and the proof bar below it lines up to the same edges. */
/* Every block is a PANEL sitting on the ground, and that now includes the light
   ones. They used to be transparent, so their copy sat straight on the window
   light — the contrast ratio was always fine (ink 18.4:1 in the lit area,
   16.8:1 in the shade) but the number changed underneath a single paragraph and
   the pane edges cut across lines of type, which is what made it hard to read.

   A panel fixes it by giving type one constant surface. The ground still reads,
   because it shows in the frame all the way around every block and in the gap
   between them — arguably better than before, when the light was competing with
   the copy instead of framing it. */
.hero, .section, .shine, .footer {
  margin-inline: var(--sw-frame);
}
.hero { margin-top: var(--sw-frame); }

/* Blocks butt directly against each other now — --sw-gap is 0. The rule stays
   so the seam can be reopened from the token alone. */
.hero, .section, .shine { margin-bottom: var(--sw-gap); }

.section { padding-block: var(--sw-band); position: relative; }
.section, .section--alt { background: var(--sw-white); }
/* Frosted glass over the lattice, Apple-style: 70% white with a real blur, so
   the ground reads as texture beneath the panel rather than through a hole.

   Glass was tried here once before and came out grey. backdrop-filter frosts
   what is BEHIND an element, and back then the answer was a flat wash, so it
   had nothing to work with. The lattice changed that — there is now structure
   back there to soften, which is the whole trick.

   Square on purpose. V6 rounds frosted panels at 44–56px, but that rule is for
   panels sitting inside a page, not full-bleed slabs; a 48px radius on a
   viewport-wide band reads as a mistake. Radius stays with .glass. */
/* --alt is the tinted glass: same 80% surface, bronco tint instead of white.
   V6 gives bronco tint as the alternate surface, and it is what stops a run of
   white panels reading as one flat stretch. */
.section, .section--alt {
  /* Same glass as .header--solid, deliberately: rgba(255,255,255,.80) and
     blur(20px) saturate(140%). The header is the one piece of chrome that is
     always on screen, so it sets what glass means on this site. Any change here
     belongs there too, or the page stops matching its own header. */
  background: rgba(255, 255, 255, .80);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  backdrop-filter: blur(20px) saturate(140%);
}

/* --alt is the tinted glass: same 80% surface, bronco tint instead of white.
   V6 names bronco tint as the alternate surface, and it is what stops a run of
   white panels reading as one flat stretch. Must sit AFTER the shared rule:
   equal specificity, so source order decides. */
.section--alt { background: rgba(228, 237, 244, .80); }

/* A 2px lift so the panels read as objects sitting ON the ground rather than
   holes cut out of it, now that there is real ground showing between them.
   V6 sets --sw-shadow: none and this is a deliberate departure, kept tiny:
   2px offset, tight blur, navy rather than black so it stays in the palette. */
.hero, .section, .section--alt, .section--invert, .shine {
  box-shadow: 0 2px 10px rgba(12, 27, 54, .10);
}
.section--invert { background: var(--sw-navy); color: var(--sw-text-invert); }
.section--invert .muted,
.shine .muted,
.footer .muted { color: var(--sw-mist); }

.section-head { max-width: min(700px, 100%); margin-bottom: clamp(48px, 6vw, 96px); }
.section-head p { color: var(--sw-text-muted); margin-top: var(--sw-space-6); max-width: var(--sw-measure); }
.section--invert .section-head p { color: var(--sw-mist); }

/* Scroll reveal — the LVMH cadence: a section settles rather than appears.
   Ports to Framer Motion whileInView. */
.reveal { opacity: 0; transform: translate3d(0, 22px, 0); }
.reveal.is-in {
  opacity: 1; transform: none;
  transition: opacity .9s var(--sw-ease), transform .9s var(--sw-ease);
}

/* Stagger — items in a group arrive one after another instead of as a slab.
   The container carries [data-stagger]; JS numbers the children into --i and
   the delay falls out of that. Ports to Framer Motion staggerChildren.

   A section that contains a stagger group fades but does NOT translate —
   otherwise the group moves twice, once with the section and once on its own,
   and the second move reads as a stutter. */
.reveal:has([data-stagger]) { transform: none; }

/* No panel ever travels. This became visible once --sw-gap dropped to the frame:
   a section sliding 22px while its neighbour sits still makes the seam between
   them collapse from 31px to 9px over the transition, and that collapsing seam
   is the "skip". The panels now only fade; all movement lives on the contents.
   Ordered after the :has() rule above so it wins on source order at equal
   specificity — do not move it up. */
.reveal.section, .reveal.shine, .reveal.hero { transform: none; }

[data-stagger] > * {
  opacity: 0;
  transform: translate3d(0, 26px, 0);
}
[data-stagger].is-in > * {
  opacity: 1;
  transform: none;
  transition: opacity .8s var(--sw-ease), transform .8s var(--sw-ease);
  transition-delay: calc(var(--i, 0) * var(--sw-stagger-step, 90ms));
}
/* Wide grids get a shorter step — nine cards at 90ms each is a five-second
   wait for the last one, which stops reading as cadence and starts reading
   as a slow page. */
[data-stagger="tight"] { --sw-stagger-step: 55ms; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; }
  [data-stagger] > * { opacity: 1; transform: none; transition: none; }
}

.mark { width: 1em; height: 1em; flex: none; fill: currentColor; }

/* ------------------------------------------------------------- buttons --- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--sw-space-2);
  min-height: 52px; padding: 0 var(--sw-space-6);
  border: 1px solid transparent; border-radius: var(--sw-radius);
  font-size: 10px; font-weight: 500; line-height: 1;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase;
  text-decoration: none; cursor: pointer;
  transition: background-color .3s var(--sw-ease), color .3s var(--sw-ease), border-color .3s var(--sw-ease);
}
.btn--primary { background: var(--sw-oxblood); color: var(--sw-white); }
.btn--primary:hover, .btn--primary:focus-visible { background: var(--sw-oxblood-deep); }
.btn--navy { background: var(--sw-navy); color: var(--sw-white); }
.btn--navy:hover, .btn--navy:focus-visible { background: var(--sw-navy-mid); }
.btn--outline { background: transparent; color: var(--sw-navy); border-color: var(--sw-navy); }
.btn--outline:hover, .btn--outline:focus-visible { background: var(--sw-navy); color: var(--sw-white); }
.btn--ghost { background: transparent; color: var(--sw-white); border-color: rgba(255,255,255,.55); }
.btn--ghost:hover, .btn--ghost:focus-visible { background: var(--sw-white); color: var(--sw-navy); border-color: var(--sw-white); }

:where(a, button, input, textarea, select):focus-visible {
  outline: 2px solid var(--sw-oxblood); outline-offset: 3px;
}
.section--invert :where(a, button):focus-visible,
.hero :where(a, button):focus-visible { outline-color: var(--sw-white); }

/* ================================================================ HEADER ===
   V6 p03: the script-only lockup is permitted in a site header above the nav,
   because WINDOW WASHING is already carried by the page.

   ⚠️ Squared on request (2026-08-11). V6 p03 and p09 both call for the glass
   panel to be rounded along its lower edge, and p08 carries the token for it
   (--sw-radius-glass, 44–56px). Radius 0 here is a deliberate departure from
   the written guideline — V7 needs to drop that token and the p03/p09 language
   or the two will disagree.
   =========================================================================== */
/* At rest the header sits INSIDE the hero — same inset, so the glass panel is
   part of the framed panel rather than a bar floating over it. Once the hero
   scrolls away it detaches: flush to the viewport top and full width, which is
   what a sticky nav should be once it is doing navigation rather than
   introduction. */
.header {
  position: fixed; left: 0; right: 0; top: var(--sw-frame); z-index: 60;
  display: flex; justify-content: center;
  padding-inline: var(--sw-frame);
  pointer-events: none;
  transition: top .45s var(--sw-ease), padding-inline .45s var(--sw-ease);
}
.header--solid { top: 0; padding-inline: 0; }

/* First visit only: the header drops in behind the welcome screen as it lifts,
   so the page assembles in one movement instead of the nav simply being there.

   The transform is on .header, which already sits at top:--sw-frame, so the
   start position has to clear both its own height and that offset — hence the
   calc rather than a flat -100%.

   `backwards` matters: without it the header sits in its final position for the
   1.95s before the animation starts, which is the whole thing it is meant to
   avoid. On a repeat visit .no-intro drops the rule and the nav is just there. */
html:not(.no-intro) .header {
  animation: header-drop .6s var(--sw-ease) 1.95s backwards;
}
@keyframes header-drop {
  from { transform: translateY(calc(-100% - var(--sw-frame))); }
  to   { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  html:not(.no-intro) .header { animation: none; }
}
.header__panel {
  pointer-events: auto;
  width: 100%; min-height: var(--sw-header-h);
  display: flex; align-items: center; gap: var(--sw-space-6);
  padding: 0 clamp(20px, 3vw, 40px);
  border-radius: var(--sw-radius);
  background: rgba(12, 27, 54, .46);
  backdrop-filter: blur(20px) saturate(140%);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  transition: background-color .4s var(--sw-ease);
}
.header--solid .header__panel {
  background: rgba(255, 255, 255, .80);
}

/* Desktop: the header falls INTO the hero. No panel, no glass — the white
   wordmark and nav sit straight on the footage, and the scrim behind the hero
   copy is what keeps them legible. The white glass arrives with .header--solid
   the moment the hero leaves the viewport, so the background "shows on scroll".
   Scoped to desktop widths on purpose: under 961px the drawer toggle needs the
   navy glass to survive whatever a service card slides beneath it. */
/* Desktop header, three states:
     top of page          transparent, falls into the hero
     scrolled, over hero  the navy glass, same as it always was
     past the hero        white glass (.header--solid)
   --scrolled and --solid are both present past the hero; the solid rule wins
   on SOURCE ORDER at equal (0,2,0) specificity, so keep it last. */
@media (min-width: 1151px) {
  .header__panel {
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    transition: background-color .4s var(--sw-ease), backdrop-filter .4s var(--sw-ease);
  }
  .header--scrolled .header__panel {
    background: rgba(12, 27, 54, .46);
    backdrop-filter: blur(20px) saturate(140%);
    -webkit-backdrop-filter: blur(20px) saturate(140%);
  }
  .header--solid .header__panel {
    background: rgba(255, 255, 255, .80);
    backdrop-filter: blur(20px) saturate(140%);
    -webkit-backdrop-filter: blur(20px) saturate(140%);
  }
}

.header__logo { display: block; flex: none; margin-right: auto; }
.header__logo img { width: clamp(120px, 13vw, 158px); height: auto; }
.header__logo .logo--navy { display: none; }
.header--solid .header__logo .logo--white { display: none; }
.header--solid .header__logo .logo--navy  { display: block; }

.nav { display: flex; align-items: center; gap: clamp(16px, 2.2vw, 30px); }
.nav a {
  font-size: 10px; font-weight: 500;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase;
  text-decoration: none; color: var(--sw-white);
  padding-block: 6px; border-bottom: 1px solid transparent;
  transition: border-color .3s var(--sw-ease); white-space: nowrap;
}
.nav a:hover, .nav a:focus-visible { border-bottom-color: currentColor; }
.header--solid .nav a { color: var(--sw-navy); }

.header .btn { min-height: 42px; padding-inline: var(--sw-space-5); white-space: nowrap; }
.header--solid .btn--ghost { color: var(--sw-navy); border-color: var(--sw-navy); }
.header--solid .btn--ghost:hover { background: var(--sw-navy); color: var(--sw-white); }

.nav-toggle { display: none; }
/* Collapse to the drawer at 1150px, not 960px. The nav went from five items to
   seven on 2026-08-18 and seven plus the logo plus the CTA needs about 1200px.
   At the old breakpoint the band from 961 to ~1200 rendered the nav at 689px
   inside a 986px panel and crushed "Request a Quote" into four lines. */
@media (max-width: 1150px) {
  .nav, .header .btn { display: none; }
  .nav-toggle {
    display: inline-flex; align-items: center; gap: 10px;
    background: none; border: 0; padding: 8px 0; cursor: pointer;
    font-size: 10px; font-weight: 500; letter-spacing: var(--sw-track-eyebrow);
    text-transform: uppercase; color: var(--sw-white);
  }
  .header--solid .nav-toggle { color: var(--sw-navy); }
}

/* ========================================================== PAGE LOAD =====
   The same curtain as the menu, lifting instead of dropping — a blind going
   up rather than something you wait behind. Panes start covering, so there is
   no flash of hero before they arrive.

   Three deliberate constraints:
   · Pure CSS. No script clears it, so a JS failure can never strand someone
     behind an opaque overlay. The inline head script only SUPPRESSES it.
   · Once per session, not per navigation. Charming on arrival, irritating by
     the third page.
   · Short, because the hero poster is the LCP element and this sits on top
     of it. Measured: a 0.84s curtain cost ~1.08s of LCP (128ms -> 1212ms on
     localhost). Cut to 0.40s + 32ms stagger = 0.53s to fully clear.
   =========================================================================== */
/* --- Welcome screen -------------------------------------------------------
   Navy field. "Welcome" arrives, then the full lockup writes itself beneath it,
   then the whole thing lifts away.

   Greeting ABOVE the mark, deliberately: it reads as a welcome that introduces
   the business, rather than a logo with a caption under it.

   Pure CSS on purpose: nothing in JS clears this, so a script error can never
   leave a visitor behind an opaque overlay. Runs once per session — the
   sessionStorage flag in <head> adds .no-intro on every later navigation.

   Timeline, deliberately tight; a welcome screen that outstays its welcome is
   just a delay:
       0.00  navy is already there (no fade in — it covers first paint)
       0.15  "Welcome" fades up
       0.50  the lockup starts writing, left to right
       1.50  writing finishes
       1.75  the screen lifts
       2.25  gone, and hidden so it cannot trap a click
   --------------------------------------------------------------------------- */
.welcome {
  position: fixed; inset: 0; z-index: 100;
  background: var(--sw-navy);
  display: grid; place-items: center;
  pointer-events: none;
  animation: welcome-out .5s var(--sw-ease) 1.75s forwards;
}
.welcome__inner { text-align: center; padding: 0 var(--sw-gutter-page); }

/* Light 300 caps, widely tracked — the guideline's display setting. The
   trailing letter-space is pulled back out on the right so a centered word
   actually sits centered; letter-spacing pads the right of every glyph
   including the last. */
.welcome__line {
  margin: 0 0 clamp(20px, 3vw, 34px);
  margin-right: -0.36em;
  font-family: var(--sw-font-display);
  font-size: clamp(15px, 1.8vw, 21px);
  font-weight: 300;
  letter-spacing: 0.36em;
  text-transform: uppercase;
  color: var(--sw-white);
  opacity: 0;
  animation: welcome-line .55s var(--sw-ease) .15s forwards;
}

/* The signature. clip-path sweeps the lockup in from the left, so it is written
   rather than faded in. Eased with a slow finish, because a pen slows into the
   final stroke. Longer than the script alone took — there is more mark to draw.

   A raster wordmark cannot be drawn with stroke-dasharray; that needs a vector
   path and what the client supplied is a bitmap. This is the closest honest
   equivalent. If the .ai ever arrives, swap it for a real stroke animation. */
.welcome__mark {
  display: block;
  width: min(500px, 80vw); height: auto;
  clip-path: inset(0 100% 0 0);
  animation: sign 1s cubic-bezier(.22,.68,.32,1) .5s forwards;
}

@keyframes sign          { to { clip-path: inset(0 0 0 0); } }
@keyframes welcome-line  { to { opacity: 1; } }
@keyframes welcome-out   { to { transform: translateY(-100%); visibility: hidden; } }

/* Second visit in the same session: no screen at all. */
html.no-intro .welcome { display: none; }

@media (prefers-reduced-motion: reduce) {
  /* No writing, no lift — the screen is simply not used. */
  .welcome { display: none; }
}

/* --- Menu: curtain wipe -------------------------------------------------
   Restored 2026-08-17. The same bad edit that removed the hero block took this
   with it, which left .drawer at its default position:static — so the whole
   menu sat in normal flow at the top of the page as a 52px band, pushing the
   site down and showing its oxblood CTA above the header.

   Five navy panes drop in sequence to cover the screen, then the links rise
   behind them. Closing collapses each pane downward, in reverse order.
   Panes of glass, and a wipe is a squeegee pull.
   ------------------------------------------------------------------------- */
.drawer {
  position: fixed; inset: 0;
  z-index: 80;
  /* Hidden, not just transparent: the links keep their box when closed and
     would otherwise still take clicks across the whole viewport. The delay
     holds it visible until the panes have finished collapsing. */
  visibility: hidden;
  transition: visibility 0s linear .5s;
}
.drawer[data-open="true"] {
  visibility: visible;
  transition-delay: 0s;
}

.drawer__curtain {
  position: absolute; inset: 0;
  display: grid; grid-template-columns: repeat(5, 1fr);
}
.drawer__curtain span {
  background: var(--sw-navy);
  border-right: 1px solid rgba(255, 255, 255, .07);
  transform: scaleY(0);
  transition: transform .42s var(--sw-ease);
  will-change: transform;
  transform-origin: top;                               /* opening wipes downward too */
}
.drawer__curtain span:last-child { border-right: 0; }
.drawer[data-open="true"] .drawer__curtain span { transform: scaleY(1); }

/* Stagger in on open, and unwind in reverse on close so the last pane to
   arrive is the first to leave. */
.drawer[data-open="true"] .drawer__curtain span:nth-child(1) { transition-delay: 0ms; }
.drawer[data-open="true"] .drawer__curtain span:nth-child(2) { transition-delay: 55ms; }
.drawer[data-open="true"] .drawer__curtain span:nth-child(3) { transition-delay: 110ms; }
.drawer[data-open="true"] .drawer__curtain span:nth-child(4) { transition-delay: 165ms; }
.drawer[data-open="true"] .drawer__curtain span:nth-child(5) { transition-delay: 220ms; }
.drawer__curtain span:nth-child(1) { transition-delay: 220ms; }
.drawer__curtain span:nth-child(2) { transition-delay: 165ms; }
.drawer__curtain span:nth-child(3) { transition-delay: 110ms; }
.drawer__curtain span:nth-child(4) { transition-delay: 55ms; }
.drawer__curtain span:nth-child(5) { transition-delay: 0ms; }

.drawer__inner {
  position: relative;
  height: 100%;
  padding: var(--sw-space-8) var(--sw-gutter-page);
  display: flex; flex-direction: column; gap: var(--sw-space-5);
  overflow-y: auto;
}
/* Content follows the panes rather than arriving with them. */
.drawer__inner > * {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .1s var(--sw-ease), transform .1s var(--sw-ease);
}
.drawer[data-open="true"] .drawer__inner > * {
  opacity: 1; transform: none;
  transition-duration: .5s;
}
.drawer[data-open="true"] .drawer__inner > *:nth-child(1) { transition-delay: .30s; }
.drawer[data-open="true"] .drawer__inner > *:nth-child(2) { transition-delay: .36s; }
.drawer[data-open="true"] .drawer__inner > *:nth-child(3) { transition-delay: .42s; }
.drawer[data-open="true"] .drawer__inner > *:nth-child(4) { transition-delay: .48s; }
.drawer[data-open="true"] .drawer__inner > *:nth-child(5) { transition-delay: .54s; }
.drawer[data-open="true"] .drawer__inner > *:nth-child(6) { transition-delay: .60s; }
.drawer[data-open="true"] .drawer__inner > *:nth-child(7) { transition-delay: .66s; }

.drawer a { color: var(--sw-white); text-decoration: none; }
.drawer__close {
  align-self: flex-end; background: none; border: 0; color: var(--sw-white);
  font-size: 10px; font-weight: 500; letter-spacing: var(--sw-track-eyebrow);
  text-transform: uppercase; cursor: pointer; padding: var(--sw-space-2) 0;
}
.drawer .btn { margin-top: var(--sw-space-4); }

@media (prefers-reduced-motion: reduce) {
  /* No wipe, no stagger — the menu is simply there or not. */
  .drawer, .drawer__curtain span, .drawer__inner > * { transition: none !important; }
  .drawer__curtain span { transform: scaleY(1); }
}


/* ================================================================== HERO ===
   Restored 2026-08-17 after a bad edit removed this block. */
.hero {
  position: relative;
  min-height: min(100svh, 940px);
  display: flex; align-items: flex-end;
  overflow: hidden;
  background: var(--sw-navy-deep);
  color: var(--sw-white);
  padding-block: calc(var(--sw-header-h) + 48px) clamp(48px, 7vw, 88px);
  isolation: isolate;
}
.hero__media { position: absolute; inset: 0; z-index: -2; }
.hero__media video, .hero__media img {
  width: 100%; height: 100%; object-fit: cover;
  /* 28%, chosen off the actual frames rather than by feel.

     The reel is 1080x1920 portrait in a landscape hero, so `cover` shows only
     ~39% of the frame height. Sampling four frames and plotting where that
     window lands: 48% clipped his head, 6% kept the head but cut the squeegee
     and the glass out of the bottom. 28% is the only band that holds the head
     AND the cleaning action in every frame. */
  object-position: 50% 28%;
}
/* Photography rule p09: navy scrim 45-60% where type sits on top.

   Three layers, and the top one is doing a different job from the other two.

   The BLUE WASH is a color cast, not a darkener — measured, it moves white
   text from 12.9:1 to 12.5:1, so it buys almost nothing in contrast. What it
   buys is consistency: the footage swings from blown-out glass to dark siding
   as it plays, and without the wash the type sits on whatever the frame happens
   to be doing. With it, the whole hero settles into one cool field and the
   headline reads the same from the first frame to the last.

   Navy-mid rather than Bronco blue on purpose. Bronco is a pale powder blue and
   at any strength that reads over footage it goes milky; navy-mid is the same
   family as the ground and keeps the heritage register. It also keeps the
   guideline's "Bronco never shares a view with oxblood" clean, since the hero
   carries an oxblood CTA. */
.hero__scrim {
  position: absolute; inset: 0; z-index: -1;
  background:
    linear-gradient(180deg, rgba(30,58,107,.26) 0%, rgba(30,58,107,.20) 55%, rgba(30,58,107,.30) 100%),
    linear-gradient(90deg, rgba(5,14,31,.80) 0%, rgba(5,14,31,.60) 46%, rgba(5,14,31,.26) 80%, rgba(5,14,31,.18) 100%),
    linear-gradient(180deg, rgba(5,14,31,.50) 0%, rgba(5,14,31,.06) 32%, rgba(5,14,31,.58) 100%);
}
@media (max-width: 860px) {
  .hero__scrim {
    /* The blue wash has to be repeated here. This block sets `background`
       wholesale, which replaces the desktop stack including the wash — so
       mobile silently lost it. Same values, so the two read as one hero. */
    background:
      linear-gradient(180deg, rgba(30,58,107,.26) 0%, rgba(30,58,107,.20) 55%, rgba(30,58,107,.30) 100%),
      linear-gradient(180deg,
        rgba(5,14,31,.68) 0%, rgba(5,14,31,.40) 26%,
        rgba(5,14,31,.60) 46%, rgba(5,14,31,.74) 72%, rgba(5,14,31,.88) 100%);
  }
  /* Portrait viewport crops far less, so it needs less correction. */
  .hero__media video, .hero__media img { object-position: 50% 22%; }
}
/* Mist drops to ~2.3:1 over footage — the hero eyebrow reverts to white. */
.hero .eyebrow--invert { color: var(--sw-white); }

.hero__inner { position: relative; width: 100%; }
/* 780, not 680. The h1 caps at 64px, where "WINDOW CLEANING" measures 738px with the
   0.14em display tracking, so a 680px column could never hold it on one line no matter
   how wide the viewport got: at 1920 the column is still 680 and the phrase still broke.
   Widening the column is the fix rather than shrinking the type, which is brand-governed.
   Nothing else in here stretches: .hero__sub self-limits at 52ch, and the badge and the
   buttons are intrinsically sized, so this only ever affects the headline. */
.hero__copy { max-width: 780px; }
.hero h1 { margin-bottom: var(--sw-space-5); }

/* "WINDOW CLEANING" is one idea and should not split across two lines. The h1 sets
   text-wrap: balance, which evens out line lengths and was happily breaking between
   the two words.

   The binding constraint is the COLUMN, not the viewport: .hero__copy above had to be
   widened to 780px before the phrase could fit at the 64px cap. This nowrap then holds
   it together.

   Measured, not guessed: the phrase needs 392px at the clamp's 34px floor and 738px at
   its 64px cap, with the 0.14em display tracking. A 375px phone leaves roughly 345px of
   column, so down there it genuinely cannot be held together and forcing it would
   overflow, which is worse than the break. From 480px up it fits, so that is where the
   rule starts. */
@media (min-width: 480px) {
  .nowrap { white-space: nowrap; }
}
/* This is an <h2> as of 2026-08-23, at Aaron's request, so the hero carries a
   real H1/H2 pair. It has to opt OUT of the global heading treatment: h1-h4 are
   Light 300 uppercase at 0.14em tracking and up to 40px, which is right for a
   four-word section title and completely wrong for a 200-character sentence
   (it renders as a wall of tracked caps). Everything below restores the
   subheading it looks like, while the tag stays an h2 for structure. */
.hero__sub {
  font-size: clamp(17px, 1.5vw, 19px);
  color: rgba(255,255,255,.92);
  max-width: 52ch; margin-bottom: var(--sw-space-7);
  font-family: var(--sw-font-ui);
  font-weight: var(--sw-weight-body);
  text-transform: none;
  letter-spacing: 0;
  line-height: var(--sw-leading-body);
}
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--sw-space-3); }

.hero__badge {
  display: inline-flex; align-items: center; gap: var(--sw-space-3);
  margin-bottom: var(--sw-space-6);
  padding: 10px var(--sw-space-4);
  border: 1px solid rgba(255,255,255,.34);
  font-size: 10px; font-weight: 500;
  letter-spacing: var(--sw-track-label); text-transform: uppercase;
}
.hero__badge .mark { color: var(--sw-white); font-size: 13px; }

/* The hero copy rises as the welcome screen lifts (1.75s), not after it.
   Gated on html:not(.no-intro), and the delays carry the same prefix — an
   `animation` shorthand resets animation-delay to 0, so a lower-specificity
   delay rule never lands. */
html:not(.no-intro) .hero__copy > * {
  animation: hero-rise .55s var(--sw-ease) backwards;
}
html:not(.no-intro) .hero__copy > *:nth-child(1) { animation-delay: 1.85s; }
html:not(.no-intro) .hero__copy > *:nth-child(2) { animation-delay: 1.90s; }
html:not(.no-intro) .hero__copy > *:nth-child(3) { animation-delay: 1.95s; }
html:not(.no-intro) .hero__copy > *:nth-child(4) { animation-delay: 2.00s; }
html:not(.no-intro) .hero__copy > *:nth-child(5) { animation-delay: 2.05s; }

/* Transform only — deliberately NO opacity. Chrome excludes opacity-0 elements
   from LCP candidacy, so fading the hero H1 in would cost the LCP measurement.
   The welcome screen is opaque and does the hiding. */
@keyframes hero-rise {
  from { transform: translate3d(0, 16px, 0); }
  to   { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .hero__copy > * { animation: none; }
}

/* ============================================================ PROOF BAR ===
   Lives INSIDE the hero now, as a frosted strip over the footage rather than an
   opaque navy band cutting the video off below it. Same glass treatment as the
   header panel, so the two read as one system.

   Deliberately smaller than it was: it is supporting evidence under the
   headline, not a section of its own. */
.proof {
  margin-top: clamp(28px, 4vw, 52px);
  background: rgba(5, 14, 31, .38);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  border: 1px solid rgba(255,255,255,.16);
  color: var(--sw-white);
}
/* No backdrop-filter (older Safari, Firefox with it disabled): fall back to a
   heavier tint, because 38% navy over moving footage is not readable alone. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .proof { background: rgba(5, 14, 31, .72); }
}
.proof__grid { display: grid; grid-template-columns: repeat(4, 1fr); }
.proof__item { padding: clamp(14px, 1.7vw, 22px) clamp(12px, 1.5vw, 20px); border-right: 1px solid rgba(255,255,255,.16); }
.proof__item:last-child { border-right: 0; }
.proof__num {
  font-family: var(--sw-font-display); font-weight: 200;
  font-size: clamp(24px, 2.4vw, 32px); line-height: 1;
  letter-spacing: 0.02em; margin-bottom: var(--sw-space-2);
}

/* The rolling figures. Each digit is a fixed-height window with a reel of
   thirty digits behind it; only the transform animates, so this costs one
   composited layer per column and no layout at all.

   1ch per column keeps the width stable while the reel spins — proportional
   digit widths would make the whole strip breathe as numbers pass. Jost has
   tabular figures, so 1ch is exactly one digit. */
.odo { display: inline-flex; align-items: flex-start; }
.odo__col {
  display: block;
  width: 1ch; height: 1em;
  overflow: hidden;
}
.odo__reel {
  display: block;
  transform: translateY(0);
  transition: transform 1.5s cubic-bezier(.18,.72,.24,1);
  will-change: transform;
}
.odo__reel > span { display: block; height: 1em; line-height: 1; text-align: center; }
.odo__suffix { display: block; }
/* Commas and decimal points sit between the reels as plain characters. They
   get no column width of their own (1ch would gap them oddly against digits)
   and no overflow clip, since nothing about them moves. */
.odo__sep { display: block; }

@media (prefers-reduced-motion: reduce) {
  /* JS jumps the reels straight to their stop; kill the travel. */
  .odo__reel { transition: none; }
}
.proof__label {
  font-size: 9px; font-weight: 500;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase; color: var(--sw-mist);
}
@media (max-width: 860px) {
  .proof__grid { grid-template-columns: repeat(2, 1fr); border-top: 1px solid rgba(255,255,255,.16); }
  .proof__item { border-bottom: 1px solid rgba(255,255,255,.16); }
}

/* ============================================================== SERVICES ===
   Cards are translucent white so the prism drifts behind them.
   =========================================================================== */
/* The hairline lives on the CELL, not on the grid behind it. Painting it as a
   grid background showing through 1px gaps is what made the lines "break": the
   stagger fades each cell, and a half-faded white card lets the grey ground
   through, so the line stops reading as a line and bleeds across the whole
   cell. Outlines sit in the gap and overlap cleanly, and a fading cell now
   takes its own hairline with it. */
.services__grid {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 1px; background: transparent; border: 1px solid var(--sw-border);
}
.services__grid > * { outline: 1px solid var(--sw-border); }
@media (max-width: 1000px) { .services__grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px)  { .services__grid { grid-template-columns: 1fr; } }

/* Hover fills the cell with the work itself, under a navy scrim, and the type
   inverts. That is the guideline's own photography rule doing the job —
   p09: "a navy scrim at 45–60% when type sits on top" — rather than a new
   decorative idea. Square, no shadow, hairline grid unchanged.

   Only four cards carry imagery: the reel genuinely shows exterior, hard
   water, tracks and a storefront. Gutters, solar and Christmas lights are NOT
   in the footage and are left type-only rather than illustrated with a photo
   of something else. */
.service {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--sw-white);
  padding: var(--sw-space-7) var(--sw-space-6);
  display: flex; flex-direction: column; gap: var(--sw-space-4);
  text-decoration: none; color: inherit;
  transition: background-color .35s var(--sw-ease);
}
.service:hover, .service:focus-visible { background: var(--sw-bronco-tint); }

.service__img {
  position: absolute; inset: 0; z-index: -1;
  opacity: 0;
  transition: opacity .45s var(--sw-ease);
}
.service__img img {
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1.06);
  transition: transform 1.1s var(--sw-ease);
}
/* The scrim rides on the image wrapper so it scales with nothing. */
.service__img::after {
  content: "";
  position: absolute; inset: 0;
  background: rgba(5, 14, 31, .60);      /* top of the 45–60% band — the stills
                                            have blown-out sky and glass in them */
}

/* ⚠️ PLACEHOLDER ILLUSTRATIONS — five services have no photography yet.
   Screens, interior, gutters, solar and Christmas lights are line drawings, not
   photographs, and are deliberately drawn so nobody mistakes them for the real
   shoot. They come out the moment Aaron sends footage; the shot list is in the
   README.

   They are already midnight navy with thin bronco line work, so the 60% scrim
   the photographs need would bury them and buy nothing — there is no blown-out
   sky in a drawing. 22% is enough to seat the type.

   Measured against the brightest pixel in a drawing (a bronco stroke at the
   group's .50 times an inner .9, so .45 effective, under the .22 scrim):
   white 8.2:1, mist 5.2:1 — both past AA — and the art sits 17.5/255 off its
   ground, comparable to the page veining at ~15. Mist is the binding constraint
   because the Christmas card is the feature card and uses it for body copy. */
/* Aaron's own photographs are portrait phone shots (768x1024) dropped into
   wide landscape card slots, so object-fit: cover crops top and bottom hard and
   the default 50% center lands below the subject. Bias upward. The feature card
   spans two columns, so its band is tighter still and the roofline lights are
   the thing that must survive the crop. */
.service__img--tall img { object-position: 50% 40%; }
.service__img--xmas img { object-position: 50% 34%; }

/* The Christmas photograph is a night shot: measured over the card's text band
   its median luminance is 0.010, so white already sits at 17.4:1 with NO scrim
   at all. The 60% the daylight stills need was buying nothing here except
   burying the lights, which are the entire subject. Dropped to 30%.

   The one real risk is the bright tail. About 1% of pixels in that band are
   bulb cores (p99 luminance 0.30, max 0.92), and mist body copy over one of
   those measures 3.0:1 at this scrim. So the body copy goes white on this card:
   white over the same p99 pixel is 5.1:1, and over the median 18.1:1. Isolated
   bulb centers can still pass behind a glyph; they are smaller than a stroke
   and the surrounding field is near black, which is the trade for showing the
   lights at all. Re-measure if the photograph is ever swapped. */
.service__img--xmas::after { background: rgba(5, 14, 31, .30); }
/* Scoped through .service--feature to score (0,2,1). At (0,1,1) it ties with
   `.service--feature p` further down the file and loses on source order, which
   is the same trap this stylesheet has sprung several times. */
.service--feature .service__img--xmas ~ p { color: var(--sw-white); }

.service__img--illus::after { background: rgba(5, 14, 31, .22); }
/* No Ken Burns on a drawing — the 1.06 scale just softens the strokes. */
.service__img--illus img { transform: none; }
.service:hover .service__img--illus img,
.service:focus-visible .service__img--illus img { transform: none; }
.service:has(.service__img):hover,
.service:has(.service__img):focus-visible { background: var(--sw-navy); }
.service:hover .service__img,
.service:focus-visible .service__img { opacity: 1; }
.service:hover .service__img img,
.service:focus-visible .service__img img { transform: scale(1); }

.service:hover h3, .service:focus-visible h3 { color: var(--sw-white); }
.service:hover p,  .service:focus-visible p  { color: var(--sw-white); }
/* Full white, not mist and not 90% — measured against the brightest pixel in
   each still, mist fell to 3.0:1 and 90% white to 4.2:1. White holds 5.1:1. */
.service:hover .service__index,
.service:focus-visible .service__index { color: var(--sw-white); }
.service:hover .service__more,
.service:focus-visible .service__more { color: var(--sw-white); }
/* Cards with no photo keep the light hover — no color flip without an image.
   The feature card is excluded: it is navy at rest and navy-mid on hover, so
   the dark text these rules restore would sit on a dark card. Left in, it
   measured 1.53:1 for the heading and 1.29:1 for the body — invisible, and the
   reason the Christmas Lights card looked like it went dark on hover.

   :not(:has()) takes the specificity of its argument, so this selector is
   (0,4,1) and beat the (0,1,1) of `.service--feature h3` no matter where either
   sat in the file. Excluding the card is the fix; source order is not. */
.service:not(:has(.service__img)):not(.service--feature):hover h3 { color: var(--sw-navy); }
.service:not(:has(.service__img)):not(.service--feature):hover p  { color: var(--sw-text-muted); }
.service:not(:has(.service__img)):not(.service--feature):hover .service__index { color: var(--sw-slate); }
.service:not(:has(.service__img)):not(.service--feature):hover .service__more  { color: var(--sw-navy); }

@media (hover: none) {
  /* No hover on touch — show the photograph outright rather than hide it. */
  .service__img { opacity: 1; }
  .service:has(.service__img) { background: var(--sw-navy); }
  .service:has(.service__img) h3 { color: var(--sw-white); }
  .service:has(.service__img) p { color: var(--sw-white); }
  .service:has(.service__img) .service__index { color: var(--sw-white); }
  .service:has(.service__img) .service__more { color: var(--sw-white); }
}
.service__index { font-size: 9px; font-weight: 500; letter-spacing: var(--sw-track-eyebrow); color: var(--sw-slate); }
.service h3 { color: var(--sw-navy); }
.service p { font-size: 15px; line-height: 1.7; color: var(--sw-text-muted); margin: 0; }
.service__more {
  margin-top: auto; padding-top: var(--sw-space-4);
  font-size: 9px; font-weight: 500;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase; color: var(--sw-navy);
  display: inline-flex; align-items: center; gap: 8px;
}
.service__more::after {
  content: ""; width: 18px; height: 1px; background: currentColor;
  transition: width .35s var(--sw-ease);
}
.service:hover .service__more::after { width: 34px; }

/* Eight cards, three columns: the last row is short by one. Christmas Lights
   takes the gap rather than leaving it, which suits the card that ships first
   (Phase 1, Sept 14) anyway. Only at three columns — at two the count is even
   and a span would re-open the hole on the other side. */
@media (min-width: 1001px) {
  .services__grid > .service--feature { grid-column: span 2; }

  /* The Christmas photograph started as 768x1024 portrait. Full-bleed across
     the two-column card it was a 1199x197 letterbox: 1.56x upscale, and only
     12% of the source height visible. Hence the blur.

     So it stops being a wash and becomes a panel on the right. At 46% of the
     card it renders around 550px wide, which is a DOWNSCALE of the source, not
     an upscale, and shows roughly a quarter of the frame instead of 12%. The
     type moves off the photograph onto the navy, which is why the body copy
     goes back to mist.

     The source file itself was also re-cropped (588x1024, was 768x1024): the
     original left ~23% was a bare tree in front of a neighbouring house, dead
     weight in every context this image is used, not a CSS artefact. It read as
     a vertical line breaking the gradient because a warm gap between branches
     sat right where the feather ramps from opaque to clear. object-position
     could not pan around it — a portrait source in a wide box is already
     width-bound, so the full source width always shows; there is no horizontal
     slack to hide behind. Trimming the file was the only fix. Original kept as
     xmas-01-original-with-tree.jpg.bak, not served.

     42% vertical keeps the roofline string, the doorframe outline, and the lit
     doorway in frame together — the fullest "before and after" the strip
     allows. Checked against 30/36/48% before settling here. */
  .service--feature:has(.service__img--xmas) { padding-right: 50%; }
  .service--feature .service__img--xmas {
    left: auto; right: 0; width: 46%;
    opacity: 1;                       /* the seasonal card shows its work at rest */
    overflow: hidden;                 /* see below — the panel must clip its own child */
  }
  .service--feature:hover .service__img--xmas { opacity: 1; }
  .service--feature .service__img--xmas img { object-position: 50% 42%; }
  /* Feather the left edge into the card rather than butting a hard seam.
     Two stops, not the original four: three interior stops (0/22/62/100) put
     a kink in the gradient's rate of change at each boundary, and a slope
     discontinuity reads as a faint line even though the color is continuous
     — a Mach band, confirmed by measuring curvature (spiked to 0.00139 right
     at 62%). Real, but not the main problem — see the `overflow: hidden`
     just above for that.

     The actual line: .service__img base has `transform: scale(1.06)` on its
     <img> at rest (a Ken Burns effect, reset to scale(1) on hover), and
     without overflow:hidden ON THIS PANEL SPECIFICALLY, the ancestor `.service`
     card's overflow:hidden was the only clip in effect — at 1199px wide, far
     bigger than this 406px panel. Measured the gap directly: the scaled <img>
     sat 12px left of its own wrapper. The gradient ::after is sized to the
     wrapper's box, so that 12px was raw, unmasked photograph sitting straight
     against plain navy — present at rest (scale 1.06 bleeds), gone on hover
     (scale 1 exactly fills the box). Every other `.service__img` usage is
     100% of its card, so the ancestor's clip was always sufficient there;
     this bug is specific to a panel narrower than its card, introduced when
     this card stopped being full-bleed. No text sits on the gradient (it
     lives on solid navy to the left, see the `p` rule below), so there was
     no legibility number to recheck for either fix. */
  .service--feature .service__img--xmas::after {
    background: linear-gradient(90deg, var(--sw-navy) 0%, rgba(12,27,54,0) 100%);
  }
  .service--feature .service__img--xmas ~ p { color: var(--sw-mist); }
}
.service--feature { background: var(--sw-navy); }
.service--feature:hover, .service--feature:focus-visible { background: var(--sw-navy-mid); }
.service--feature h3 { color: var(--sw-white); }
.service--feature p, .service--feature .service__index { color: var(--sw-mist); }
.service--feature .service__more { color: var(--sw-white); }
/* Held explicitly through hover as well, so no later rule can pull the copy
   dark again. Measured on navy-mid: white 11.2:1, mist 7.1:1. */
.service--feature:hover h3,
.service--feature:focus-visible h3,
.service--feature:hover .service__more,
.service--feature:focus-visible .service__more { color: var(--sw-white); }
.service--feature:hover p,
.service--feature:focus-visible p,
.service--feature:hover .service__index,
.service--feature:focus-visible .service__index { color: var(--sw-mist); }

/* ========================================================= GLASS PANEL ==== */
.shine { position: relative; background: var(--sw-navy); color: var(--sw-white); overflow: hidden; padding-block: var(--sw-band); }
.shine__media { position: absolute; inset: 0; }
.shine__media img,
.shine__media video { width: 100%; height: 100%; object-fit: cover; object-position: 50% 28%; }
.shine__media::after {
  content: ""; position: absolute; inset: 0;
  /* The blue wash came off on 2026-08-18: it now belongs to the hero alone.
     What stays is the directional navy scrim, identical to .shot--right, which
     is the layer the panel's type actually leans on. So this block reads like
     the other photographic sections rather than like a second hero. */
  background:
    linear-gradient(90deg, rgba(5,14,31,.30) 0%, rgba(5,14,31,.72) 62%, rgba(5,14,31,.88) 100%);
}
.shine__inner { position: relative; display: flex; justify-content: flex-end; }
.shine__panel {
  width: min(720px, 94%);
  margin-block: clamp(56px, 8vw, 104px);
  padding: clamp(32px, 4vw, 56px);
  padding-right: max(clamp(32px, 4vw, 56px), var(--sw-gutter-page));
  background: rgba(5, 14, 31, .52);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);
  border-radius: var(--sw-radius);
}
.shine__panel h2 { margin-bottom: var(--sw-space-5); }
.shine__panel p { color: rgba(255,255,255,.90); }
.shine__list { list-style: none; margin: 0 0 var(--sw-space-7); padding: 0; }
.shine__list li {
  display: flex; gap: var(--sw-space-4); align-items: flex-start;
  padding-block: var(--sw-space-4); border-bottom: 1px solid rgba(255,255,255,.18); font-size: 16px;
}
.shine__list li:first-child { border-top: 1px solid rgba(255,255,255,.18); }
.shine__list .mark { color: var(--sw-white); font-size: 14px; margin-top: 7px; }
@media (max-width: 860px) {
  .shine__panel {
    width: 100%; padding-inline: var(--sw-gutter-page);
    border-radius: var(--sw-radius);
    margin-block: clamp(40px, 14vw, 72px) 0;
  }
  .shine__media img { object-position: 50% 28%; }
}

/* =============================================================== PROCESS ===
   The rule above each step charges in left to right rather than just being
   there — the numbers read as a sequence, so the rules should too.

   It is a pseudo-element, not the border it replaces: border-width can't sweep,
   and animating it would relayout on every frame. scaleX on a 3px bar is
   compositor-only. The 3px the border used to occupy is now carried by
   padding-top so the geometry is unchanged.

   It fires ~260ms behind its own card, so the step settles first and the rule
   draws across after it — charging, not arriving together. */
.process__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sw-space-7); }
.step { position: relative; padding-top: calc(var(--sw-space-5) + 3px); }
.step::before {
  content: "";
  position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: var(--sw-navy);
  transform: scaleX(0);
  transform-origin: left center;
}
.step:nth-child(4)::before { background: var(--sw-oxblood); }

[data-stagger].is-in .step::before {
  transform: scaleX(1);
  transition: transform .85s var(--sw-ease);
  transition-delay: calc(var(--i, 0) * var(--sw-stagger-step, 90ms) + 260ms);
}

@media (prefers-reduced-motion: reduce) {
  /* No sweep — the rules are simply drawn. */
  .step::before { transform: none; transition: none; }
}
.step__num { font-family: var(--sw-font-display); font-weight: 200; font-size: 44px; line-height: 1; color: var(--sw-navy); margin-bottom: var(--sw-space-4); }
.step h3 { color: var(--sw-navy); margin-bottom: var(--sw-space-3); }
.step p { font-size: 15px; line-height: 1.7; color: var(--sw-text-muted); margin: 0; }
@media (max-width: 900px) { .process__grid { grid-template-columns: repeat(2, 1fr); gap: var(--sw-space-6); } }
@media (max-width: 520px) { .process__grid { grid-template-columns: 1fr; } }

/* ============================================================= HARD WATER === */
.water__grid { display: grid; grid-template-columns: 1.05fr .95fr; gap: clamp(40px, 6vw, 88px); align-items: start; }
/* Four across, matching the service grid and the proof strip rather than
   stacking as full-width bars. Same construction as every other card group on
   the page: 1px gaps, the hairline carried on the cell as an outline so a
   fading card takes its own line with it. */
.water__facts {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 1px; background: transparent; border: 1px solid var(--sw-border);
}
.water__facts > * { outline: 1px solid var(--sw-border); }
.water__fact {
  background: var(--sw-white);
  padding: var(--sw-space-7) var(--sw-space-6);
  display: flex; flex-direction: column; gap: var(--sw-space-4);
}
/* Same type ramp as a service card, so the two groups read as one family. */
.water__fact h4 {
  color: var(--sw-navy); margin: 0;
  font-size: clamp(13px, 1.1vw, 15px); letter-spacing: var(--sw-track-label);
  text-transform: uppercase;
}
.water__fact p { font-size: 15px; line-height: 1.7; color: var(--sw-text-muted); margin: 0; }
@media (max-width: 1000px) { .water__facts { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px)  { .water__facts { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .water__grid { grid-template-columns: 1fr; } }

/* ============================================================ COMPARE ====== */
/* Before/after sliders. Grid of squared cards (--sw-radius is 0 everywhere
   except the frosted glass panels), each holding a draggable comparison and
   a caption underneath in the same voice as a .service card. */
.compare__grid {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 1px; background: var(--sw-border); border: 1px solid var(--sw-border);
}
@media (max-width: 900px) { .compare__grid { grid-template-columns: 1fr; } }

.compare__card { background: var(--sw-white); }

/* The frame is the draggable image area. Fixed aspect rather than fixed height,
   so it holds its shape at any card width instead of cropping tighter on a
   narrow phone than it does on desktop. */
.compare {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  touch-action: pan-y;
  cursor: ew-resize;
}
.compare__img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  -webkit-user-select: none; user-select: none;
  -webkit-user-drag: none;
}
/* Reveals the RIGHT side of the frame: the after photo is clipped away on
   the left, up to the handle position, so before always sits left of the
   handle and after always sits right of it, matching the badge's own
   left-to-right reading. */
.compare__reveal {
  position: absolute; inset: 0;
  clip-path: inset(0 0 0 50%);
}
.is-dragging .compare__reveal { transition: none; }
.compare__reveal { transition: clip-path .25s var(--sw-ease); }

.compare__handle {
  position: absolute; top: 0; bottom: 0; left: 50%;
  width: 40px; margin-left: -20px;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; padding: 0;
  cursor: ew-resize; -webkit-tap-highlight-color: transparent;
}
.is-dragging .compare__handle { transition: none; }
.compare__handle { transition: left .25s var(--sw-ease); }
.compare__handle::before {
  /* the vertical line the whole width of the frame */
  content: ''; position: absolute; top: 0; bottom: 0; left: 50%;
  width: 2px; background: var(--sw-white);
  box-shadow: 0 0 6px rgba(5,14,31,.5);
  transform: translateX(-50%);
}
.compare__handle::after {
  /* the round grip, drawn as a double chevron with borders (no image) */
  content: '';
  width: 44px; height: 44px; border-radius: 50%;
  background: rgba(12,27,54,.55);
  border: 2px solid var(--sw-white);
  backdrop-filter: blur(6px);
  box-shadow: 0 2px 10px rgba(5,14,31,.35);
}
.compare__handle:focus-visible { outline: none; }
.compare__handle:focus-visible::after { outline: 2px solid var(--sw-white); outline-offset: 3px; }

/* The four-point mark sits inside the grip, same sparkle as the header and
   the review badges. Given position: absolute, it paints above the ::after
   circle without a z-index: static content (the generated circle) paints
   before positioned descendants regardless of source order, which is also
   how the vertical line stays above it.

   20px, not the 13-15px used next to fine print elsewhere: this one is the
   only thing inside a 44px circle and has to read as a deliberate icon on
   its own, not a small accent next to text. Confirmed it was rendering
   correctly at the smaller size too — checked by scaling it to 80px, since
   a thin white sparkle on a translucent navy circle is genuinely hard to
   see at 15px, not broken. */
.compare__handle .mark {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  font-size: 20px;
  color: var(--sw-white);
}

.compare__badge {
  position: absolute; top: var(--sw-space-4); left: var(--sw-space-4);
  z-index: 1; pointer-events: none;
  background: var(--sw-navy); color: var(--sw-white);
  font-size: 10px; font-weight: 500; letter-spacing: var(--sw-track-label);
  text-transform: uppercase;
  padding: 6px 10px;
}

.compare__caption { padding: var(--sw-space-6) var(--sw-space-6) var(--sw-space-7); }
.compare__caption h3 { color: var(--sw-navy); margin-bottom: var(--sw-space-2); }
.compare__caption p { font-size: 15px; line-height: 1.7; color: var(--sw-text-muted); margin: 0; }

@media (prefers-reduced-motion: reduce) {
  .compare__reveal, .compare__handle { transition: none; }
}

/* ====================================================== BEFORE / AFTER ===== */
/* Side by side, NOT the wipe slider above. These pairs are Aaron's own job
   photographs, shot handheld between the two passes, so the camera moves and a
   wipe would jump at the seam, which still holds at full resolution. The
   exterior pair only exists at 360x480, so this grid caps its own width instead
   of blowing it up across a full column. The interior pair was swapped for its
   3024x4032 phone originals on 2026-09-29 and is sharp at this size. */
.ba {
  display: grid; grid-template-columns: repeat(2, 1fr);
  gap: 1px; background: var(--sw-border); border: 1px solid var(--sw-border);
  max-width: 740px;
}
@media (max-width: 560px) { .ba { grid-template-columns: 1fr; max-width: 360px; } }
.ba__card { background: var(--sw-white); }
.ba__frame { position: relative; aspect-ratio: 3 / 4; overflow: hidden; }
.ba__frame img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.ba__tag {
  position: absolute; top: var(--sw-space-4); left: var(--sw-space-4);
  z-index: 1; pointer-events: none;
  background: var(--sw-navy); color: var(--sw-white);
  font-size: 10px; font-weight: 500; letter-spacing: var(--sw-track-label);
  text-transform: uppercase;
  padding: 6px 10px;
}

/* ================================================================ BLOG ===== */
/* Long-form reading. Everything else on this site is a section of short copy in
   a wide layout; a post, and the privacy page, is one column at a reading
   measure. That gets its own container rather than bending .section-head to a
   job it was not built for. Spacing is top-margin only, so the base p margin is
   zeroed here or every gap would double. */
/* In px, not ch. Jost is narrow enough that 62ch measured out at 88 characters a line;
   540px lands near 75, which is where long-form reading stops being work. */
.post { max-width: 540px; }
.post > * + * { margin-top: var(--sw-space-5); }
.post p { margin-bottom: 0; }
.post p, .post li { font-size: 17px; line-height: var(--sw-leading-body); }
.post .lede { font-size: 20px; color: var(--sw-navy); }
.post h2 { font-size: clamp(21px, 2.4vw, 28px); margin-top: clamp(40px, 5vw, 64px); }
.post ul, .post ol { padding-left: 1.25em; }
.post li + li { margin-top: var(--sw-space-3); }
.post strong { font-weight: 500; color: var(--sw-navy); }
.post figure, .post .ba { margin-top: clamp(28px, 4vw, 44px); }
/* Capped by height, not width: these are phone photos, mostly portrait, and at full
   column width one is taller than the screen it is being read on. */
.post figure img { width: auto; height: auto; max-width: 100%; max-height: min(70vh, 620px); }
.post figcaption, .post__note {
  font-size: 13px; line-height: 1.6; color: var(--sw-text-muted);
  margin-top: var(--sw-space-3);
}
.post blockquote { border-left: 2px solid var(--sw-navy); padding-left: var(--sw-space-5); }
.post blockquote p { font-size: 19px; color: var(--sw-navy); }
.post cite {
  display: block; margin-top: var(--sw-space-3);
  font-size: 13px; font-style: normal; color: var(--sw-text-muted);
}

/* The index. Each card carries its own border instead of sharing a 1px gap the
   way .compare__grid does: that trick paints the gap color into any empty cell,
   and a blog does not stay at a multiple of three. */
.posts { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sw-space-6); }
@media (max-width: 1000px) { .posts { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 640px)  { .posts { grid-template-columns: 1fr; } }
.posts__card {
  display: flex; flex-direction: column;
  background: var(--sw-white); border: 1px solid var(--sw-border);
  color: inherit; text-decoration: none;
  transition: border-color .3s var(--sw-ease);
}
.posts__card:hover, .posts__card:focus-visible { border-color: var(--sw-navy); }
.posts__img { position: relative; display: block; aspect-ratio: 4 / 3; overflow: hidden; }
.posts__img img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform .8s var(--sw-ease);
}
.posts__card:hover .posts__img img,
.posts__card:focus-visible .posts__img img { transform: scale(1.04); }
.posts__body { display: block; padding: var(--sw-space-6) var(--sw-space-6) var(--sw-space-7); }
.posts__date {
  display: block; margin-bottom: var(--sw-space-3);
  font-size: 10px; font-weight: 500; letter-spacing: var(--sw-track-label);
  text-transform: uppercase; color: var(--sw-text-muted);
}
.posts__body h3 { color: var(--sw-navy); margin-bottom: var(--sw-space-3); }
.posts__body p { font-size: 15px; line-height: 1.7; color: var(--sw-text-muted); margin: 0; }
@media (prefers-reduced-motion: reduce) {
  .posts__card, .posts__img img { transition: none; }
  .posts__card:hover .posts__img img,
  .posts__card:focus-visible .posts__img img { transform: none; }
}

/* ------------------------------------------------- BLOG: article and index --
   The furniture around a post is generated by scripts/blog.mjs at build time
   (byline, contents list, quote card, service card, more posts), so the CSS
   here styles markup no post file contains by hand. Layout after RadPad's
   blog, in this brand: Light caps headings, navy and oxblood, square corners. */

/* Category tag and date line, shared by cards, the feature and the byline. */
.post-tag {
  display: block; margin-bottom: var(--sw-space-3);
  font-size: 10px; font-weight: 500; letter-spacing: var(--sw-track-label);
  text-transform: uppercase; color: var(--sw-oxblood);
}
.post-meta {
  display: block; margin-top: var(--sw-space-5);
  font-size: 11px; font-weight: 500; letter-spacing: .12em;
  text-transform: uppercase; color: var(--sw-text-muted);
}
.posts__body { display: flex; flex-direction: column; flex: 1; }
.posts__body .post-meta { margin-top: auto; padding-top: var(--sw-space-5); }

/* Hero byline: who, when, how long. */
.byline {
  margin-top: var(--sw-space-5);
  font-size: 11px; font-weight: 500; letter-spacing: .14em;
  text-transform: uppercase; color: rgba(255,255,255,.82);
}

/* Article and sidebar. The article keeps a reading measure (about 78 characters
   of Jost at 18px); the sidebar holds the contents list and the quote card. */
.article-layout {
  display: grid;
  grid-template-columns: minmax(0, 640px) 300px;
  justify-content: center;
  gap: clamp(48px, 6vw, 104px);
  align-items: start;
}
.article-layout .post { max-width: 640px; }
.article-layout .post p, .article-layout .post li { font-size: 18px; line-height: 1.8; }
.article-layout .post .lede { font-size: clamp(20px, 1.8vw, 23px); line-height: 1.6; }
.article-layout .post h2 {
  position: relative; padding-top: var(--sw-space-5);
  scroll-margin-top: calc(var(--sw-header-h) + 32px);
}
.article-layout .post h2::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 40px; height: 2px; background: var(--sw-oxblood);
}
.article-layout .post a:not(.service-card) {
  color: var(--sw-navy); text-decoration: underline;
  text-decoration-color: var(--sw-bronco); text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}
.article-layout .post a:not(.service-card):hover { text-decoration-color: var(--sw-oxblood); }
.article-layout .post figure img { max-height: none; width: 100%; }

.article-aside {
  position: sticky; top: calc(var(--sw-header-h) + 40px);
  display: grid; gap: var(--sw-space-6);
}
.toc { border-left: 1px solid var(--sw-bronco-pale); padding: 2px 0 2px var(--sw-space-5); }
.toc__label {
  margin: 0 0 var(--sw-space-4);
  font-size: 10px; font-weight: 500; letter-spacing: var(--sw-track-label);
  text-transform: uppercase; color: var(--sw-oxblood);
}
.toc ol { list-style: none; margin: 0; padding: 0; }
.toc li + li { margin-top: var(--sw-space-3); }
.toc a { font-size: 14px; line-height: 1.45; color: var(--sw-text-muted); text-decoration: none; }
.toc a:hover, .toc a:focus-visible { color: var(--sw-navy); }

.aside-cta { background: var(--sw-navy); color: var(--sw-white); padding: var(--sw-space-6); }
.aside-cta .eyebrow { margin-bottom: var(--sw-space-3); }
.aside-cta__title {
  margin: 0 0 var(--sw-space-3);
  font-family: var(--sw-font-display); font-weight: 300; font-size: 20px;
  letter-spacing: .1em; text-transform: uppercase; line-height: 1.25;
}
.aside-cta__text { margin: 0 0 var(--sw-space-5); font-size: 15px; line-height: 1.65; color: rgba(255,255,255,.8); }
.aside-cta .btn { width: 100%; justify-content: center; text-align: center; }
.aside-cta__tel {
  display: block; margin-top: var(--sw-space-4); text-align: center;
  font-size: 11px; font-weight: 500; letter-spacing: .14em; text-transform: uppercase;
  color: var(--sw-white); text-decoration: none;
}
.aside-cta__tel:hover { text-decoration: underline; }

/* The service or area page the post is drawn from. */
.service-card {
  display: grid; grid-template-columns: 220px 1fr;
  margin-top: clamp(48px, 6vw, 72px);
  background: var(--sw-white); border: 1px solid var(--sw-border);
  color: inherit; text-decoration: none;
  transition: border-color .3s var(--sw-ease);
}
.service-card:hover, .service-card:focus-visible { border-color: var(--sw-navy); }
.service-card__img { position: relative; overflow: hidden; min-height: 180px; }
.service-card__img img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.service-card__body { display: block; padding: var(--sw-space-6); }
.service-card__title {
  display: block; margin-bottom: var(--sw-space-3);
  font-family: var(--sw-font-display); font-weight: 300; font-size: 20px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--sw-navy); line-height: 1.25;
}
.service-card__text { display: block; font-size: 15px; line-height: 1.65; color: var(--sw-text-muted); }
.service-card__more {
  display: block; margin-top: var(--sw-space-4);
  font-size: 11px; font-weight: 500; letter-spacing: .14em; text-transform: uppercase; color: var(--sw-navy);
}

/* Index: the newest post as a feature, with an oxblood rule like the section heads. */
.post-feature {
  display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  background: var(--sw-white); border: 1px solid var(--sw-border);
  color: inherit; text-decoration: none;
  transition: border-color .3s var(--sw-ease);
}
.post-feature:hover, .post-feature:focus-visible { border-color: var(--sw-navy); }
.post-feature__img { position: relative; overflow: hidden; min-height: 340px; }
.post-feature__img img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transition: transform .8s var(--sw-ease);
}
.post-feature:hover .post-feature__img img { transform: scale(1.03); }
.post-feature__body {
  display: flex; flex-direction: column; justify-content: center;
  padding: clamp(28px, 4vw, 56px);
  border-left: 3px solid var(--sw-oxblood);
}
.post-feature__body h3 {
  margin: 0 0 var(--sw-space-4); color: var(--sw-navy);
  font-size: clamp(22px, 2.2vw, 30px); line-height: 1.2;
}
.post-feature__body p { margin: 0; font-size: 16px; line-height: 1.7; color: var(--sw-text-muted); }
.post-feature__cta { align-self: flex-start; margin-top: var(--sw-space-6); }
.posts-head { margin: clamp(56px, 7vw, 96px) 0 var(--sw-space-6); }
.more-posts .posts-head { margin-top: 0; }
.posts-head h2 { margin: var(--sw-space-3) 0 0; }

@media (max-width: 1000px) {
  .article-layout { grid-template-columns: minmax(0, 1fr); }
  .article-aside { position: static; }
  .toc { display: none; }
  .post-feature { grid-template-columns: 1fr; }
  .post-feature__img { min-height: 0; aspect-ratio: 16 / 10; }
  .post-feature__body { border-left: 0; border-top: 3px solid var(--sw-oxblood); }
}
@media (max-width: 600px) {
  .service-card { grid-template-columns: 1fr; }
  .service-card__img { min-height: 0; aspect-ratio: 16 / 9; }
  .article-layout .post p, .article-layout .post li { font-size: 17px; }
}
@media (prefers-reduced-motion: reduce) {
  .service-card, .post-feature, .post-feature__img img { transition: none; }
  .post-feature:hover .post-feature__img img { transform: none; }
}

/* ============================================================== AREAS ====== */
.areas__list { display: flex; flex-wrap: wrap; gap: 1px; background: transparent; border: 1px solid rgba(255,255,255,.18); }
.areas__list > * { outline: 1px solid rgba(255,255,255,.18); }
.areas__list a {
  flex: 1 1 auto; min-width: 180px; background: var(--sw-navy);
  padding: var(--sw-space-5); text-decoration: none; color: var(--sw-white);
  font-size: 13px; font-weight: 500; letter-spacing: var(--sw-track-label); text-transform: uppercase;
  transition: background-color .3s var(--sw-ease);
}
.areas__list a:hover, .areas__list a:focus-visible { background: var(--sw-navy-mid); }
.areas__note { margin-top: var(--sw-space-6); color: var(--sw-mist); font-size: 15px; }

/* ========================================================= TESTIMONIALS ====
   Three tiles at a time. On rotation the current three fall out one after
   another and the next three fall in behind them — a relay, not a carousel.
   Nothing slides sideways and there is no track to drag.

   The falling is done on .quote__inner, never on .quote itself. The cell is
   what paints white over the grid's hairline background; fading the cell would
   flash the grey through on every rotation. Moving the contents keeps the grid
   dead still while the words change.
   =========================================================================== */
.quotes { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--sw-border); border: 1px solid var(--sw-border); }
.quote {
  background: var(--sw-white);
  padding: var(--sw-space-7) var(--sw-space-6);
  display: flex; flex-direction: column;
  overflow: hidden;                                  /* clips the fall */
  min-height: clamp(268px, 25vw, 324px);             /* holds the row still as
                                                        quote lengths change */
}
.quote__inner { display: flex; flex-direction: column; flex: 1; }
/* A customer's own photo of the finished job, when the source review carried
   one. .quote__inner is a column flex container, and float has no effect on
   a flex item -- it would just sit inert rather than wrapping the text -- so
   this is a lead image, full width, above the stars, rather than an inset.
   Square corners: --sw-radius is 0 everywhere but the frosted glass panels.
   Negative margin cancels the tile's own padding on three sides so the photo
   reaches the edges instead of floating in an inset box. */
.quote__photo {
  display: block; width: calc(100% + var(--sw-space-6) * 2); height: 140px;
  object-fit: cover;
  margin: calc(var(--sw-space-7) * -1) calc(var(--sw-space-6) * -1) var(--sw-space-6);
}
.quote__stars { display: flex; gap: 5px; color: var(--sw-navy); margin-bottom: var(--sw-space-5); font-size: 13px; }
.quote blockquote { margin: 0 0 var(--sw-space-6); font-size: 16px; line-height: 1.75; }
.quote figcaption {
  margin-top: auto; padding-top: var(--sw-space-4); border-top: 1px solid var(--sw-border);
  font-size: 9px; font-weight: 500; letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase; color: var(--sw-slate);
}

/* Three states. "above" is the staging position and is deliberately untransitioned
   — a tile snaps up there while invisible, then falls from it. */
.quote__inner[data-state="above"] {
  opacity: 0; transform: translate3d(0, -34px, 0); transition: none;
}
.quote__inner[data-state="in"] {
  opacity: 1; transform: none;
  transition: opacity .62s var(--sw-ease), transform .62s var(--sw-ease);
  transition-delay: calc(var(--i, 0) * 120ms);
}
.quote__inner[data-state="below"] {
  opacity: 0; transform: translate3d(0, 28px, 0);
  transition: opacity .42s var(--sw-ease), transform .42s var(--sw-ease);
  transition-delay: calc(var(--i, 0) * 95ms);
}

/* Realtor section: the same quote cards as the rotator, standing still. Two
   across because there are exactly two realtor reviews in Aaron's analysis —
   the point of the section is that the channel is real but barely worked. */
.quotes--pair { grid-template-columns: repeat(2, 1fr); }
.quotes--pair .quote__inner { opacity: 1; transform: none; }
@media (max-width: 900px) { .quotes--pair { grid-template-columns: 1fr; } }

.realtors__cta {
  display: flex; gap: var(--sw-space-4); flex-wrap: wrap;
  margin-top: clamp(32px, 4vw, 56px);
}

/* The roof-tile quote in the story split. A plaque rule, not italics — the
   guideline's pull-quote treatment. */
.story__quote {
  margin: clamp(28px, 3.5vw, 44px) 0 0;
  padding-left: var(--sw-space-5);
  /* Bronco, not oxblood: the split body is NAVY, and dark red on dark blue is
     mud — plus V6 p09 keeps oxblood and navy-adjacent bronco apart anyway.
     White text, mist caption: slate on navy is the 1.9:1 trap in tokens.css. */
  border-left: var(--sw-rule-plaque) solid var(--sw-bronco);
}
.story__quote blockquote { margin: 0 0 var(--sw-space-3); font-size: 16px; line-height: 1.75; color: var(--sw-white); }
.story__quote figcaption {
  font-size: 10px; letter-spacing: var(--sw-track-label); text-transform: uppercase;
  color: var(--sw-mist); font-weight: 500;
}

.quotes__foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sw-space-5);
  margin-top: var(--sw-space-6); flex-wrap: wrap;
}
.quotes__count {
  font-size: 9px; font-weight: 500; letter-spacing: var(--sw-track-eyebrow);
  text-transform: uppercase; color: var(--sw-slate);
}
/* Which trio you are on. Hairline ticks, not dots — dots read as a carousel
   and this deliberately is not one. */
.quotes__ticks { display: flex; gap: var(--sw-space-3); }
.quotes__tick {
  width: 34px; height: 2px; border: 0; padding: 0; cursor: pointer;
  background: var(--sw-border);
  transition: background-color .4s var(--sw-ease);
}
.quotes__tick[aria-current="true"] { background: var(--sw-navy); }
.quotes__tick:focus-visible { outline: 2px solid var(--sw-navy); outline-offset: 4px; }

.quotes__all {
  margin-left: auto;
  font-size: 9px; font-weight: 500;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase; color: var(--sw-navy);
  display: inline-flex; align-items: center; gap: 8px;
}
.quotes__all::after {
  content: ""; width: 18px; height: 1px; background: currentColor;
  transition: width .35s var(--sw-ease);
}
.quotes__all:hover::after, .quotes__all:focus-visible::after { width: 34px; }
.quotes__all:focus-visible { outline: 2px solid var(--sw-navy); outline-offset: 4px; }

/* Every tile opens the lightbox: cursor and a hover lift say so before the
   click does. The tile is already a <figure>, not a link, since it is not
   navigation -- it stays on the page and shows the same content larger. */
.quote { cursor: zoom-in; transition: box-shadow .25s var(--sw-ease); }
.quote:hover, .quote:focus-within { box-shadow: 0 2px 16px rgba(12,27,54,.14); }
.quote:focus-visible { outline: 2px solid var(--sw-navy); outline-offset: -2px; }

@media (max-width: 900px) {
  .quotes { grid-template-columns: 1fr; }
  .quote { min-height: 0; }
}

/* ========================================================= PROOF WALL ====== */
/* The reviews page's photo-led wall. CSS columns rather than grid: a true
   masonry that packs to each card's own height, which matters here because
   the photos keep their real aspect ratio (no crop) and so genuinely vary in
   height card to card -- a grid would force every card in a row to the
   tallest one and leave visible gaps under the shorter photos. */
.proof-wall {
  columns: 3 280px; column-gap: 1px;
  background: var(--sw-border);
}
.proof-wall .quote {
  break-inside: avoid; display: inline-block; width: 100%;
  min-height: 0;                          /* the rotator's row-holding min-height does not apply to a masonry card */
  margin-bottom: 1px;
  background: var(--sw-white);
}
/* Natural aspect, not the rotator's cropped 140px strip: this wall's whole
   point is showing the photo as the customer actually took it. */
.proof-wall .quote__photo {
  width: 100%; height: auto; object-fit: initial;
}
@media (max-width: 640px) { .proof-wall { columns: 1 100%; } }

/* ============================================================ LIGHTBOX ===== */
/* Shared by the homepage rotator and the reviews page's photo wall -- one
   dialog, populated by JS per click rather than one instance per surface.

   Anchored, not centered: the panel is a drawer pinned to an edge, sliding in
   from the right on desktop and up from the bottom on mobile, rather than a
   card fading in over the middle of the screen. site.js drives the timing --
   [hidden] is removed first, then .is-open is added a frame later so the
   browser actually paints the off-screen starting position before animating
   away from it, and removed again only after LB_CLOSE_MS on the way out, so
   the slide-out is visible instead of the panel vanishing mid-transition. */
.lightbox {
  position: fixed; inset: 0; z-index: 200;
}
.lightbox[hidden] { display: none; }
.lightbox__scrim {
  position: absolute; inset: 0;
  background: rgba(5,14,31,.72); backdrop-filter: blur(4px);
  opacity: 0; transition: opacity .4s var(--sw-ease);
}
.lightbox.is-open .lightbox__scrim { opacity: 1; }

.lightbox__panel {
  position: fixed;
  background: var(--sw-white);
  display: flex; flex-direction: column;
  overflow-y: auto;
  box-shadow: 0 24px 64px rgba(5,14,31,.4);
  transition: transform .45s var(--sw-ease);
}

/* Desktop and up: a drawer off the right edge, full height. */
@media (min-width: 641px) {
  .lightbox__panel {
    top: 0; right: 0; bottom: 0;
    width: min(480px, 100vw);
    transform: translate3d(100%, 0, 0);
  }
  .lightbox.is-open .lightbox__panel { transform: translate3d(0, 0, 0); }
}

/* Mobile: a sheet off the bottom edge. Square corners even here -- this
   project's one rounded-corner exception is the frosted glass header panel,
   and a bottom sheet is not that. */
@media (max-width: 640px) {
  .lightbox__panel {
    left: 0; right: 0; bottom: 0;
    width: 100%; max-height: 86vh;
    transform: translate3d(0, 100%, 0);
  }
  .lightbox.is-open .lightbox__panel { transform: translate3d(0, 0, 0); }
}

.lightbox__close {
  position: absolute; top: var(--sw-space-4); right: var(--sw-space-4); z-index: 1;
  width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(12,27,54,.55); color: var(--sw-white);
  border: 0; font-size: 20px; line-height: 1; cursor: pointer;
  backdrop-filter: blur(6px);
}
.lightbox__close:hover, .lightbox__close:focus-visible { background: var(--sw-navy); }
.lightbox__close:focus-visible { outline: 2px solid var(--sw-white); outline-offset: 2px; }
.lightbox__media { flex: none; background: var(--sw-navy-deep); }
.lightbox__media[hidden] { display: none; }
.lightbox__media img {
  display: block; width: 100%; max-height: 60vh;
  object-fit: contain;                    /* the whole photo, not a crop -- this is the "see full image" view */
}
.lightbox__body { padding: var(--sw-space-7) var(--sw-space-6); }
.lightbox__stars { display: flex; gap: 5px; color: var(--sw-navy); margin-bottom: var(--sw-space-5); font-size: 14px; }
.lightbox__quote { margin: 0 0 var(--sw-space-5); font-size: 18px; line-height: 1.75; color: var(--sw-ink); }
.lightbox__name {
  margin: 0; padding-top: var(--sw-space-4); border-top: 1px solid var(--sw-border);
  font-size: 10px; font-weight: 500; letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase; color: var(--sw-slate);
}
@media (prefers-reduced-motion: reduce) {
  .lightbox__scrim { backdrop-filter: none; transition: none; }
  .lightbox__panel { transition: none; }
}
@media (prefers-reduced-motion: reduce) {
  /* No fall and no rotation — JS holds on the first three and these states
     collapse to plain visibility. */
  .quote__inner[data-state] { opacity: 1; transform: none; transition: none; }
}

/* ================================================================== FAQ ==== */
/* Dividers charge in the same way the process rules do — the list draws itself
   top to bottom as you arrive at it.

   The borders are kept but turned transparent rather than removed, and the
   sweeping bar is parked over them at -1px. That way the box model is untouched:
   drop the border and every row shifts up a pixel, six times over.

   The +/- marker already owns summary::before and summary::after, so the divider
   uses details::after and the list's top rule uses .faq::before. */
.faq {
  max-width: 860px;
  position: relative;
  border-top: 1px solid transparent;
}
.faq details {
  position: relative;
  border-bottom: 1px solid transparent;
}
.faq::before,
.faq details::after {
  content: "";
  position: absolute; left: 0; right: 0; height: 1px;
  background: var(--sw-border);
  transform: scaleX(0);
  transform-origin: left center;
}
.faq::before        { top: -1px; }
.faq details::after { bottom: -1px; }

.faq.is-in::before,
.faq.is-in details::after {
  transform: scaleX(1);
  transition: transform .85s var(--sw-ease);
}
/* The top rule leads, then each divider follows its own row. */
.faq.is-in::before        { transition-delay: 200ms; }
.faq.is-in details::after { transition-delay: calc(var(--i, 0) * var(--sw-stagger-step, 90ms) + 320ms); }

@media (prefers-reduced-motion: reduce) {
  .faq::before, .faq details::after { transform: none; transition: none; }
}
/* A question is interface text, not a headline. The guideline's "headlines
   always caps at 0.14em" was being applied here and it made the questions hard
   to read — all-caps kills word shape, and 2.66px of tracking in a half-width
   column pushes most questions onto two lines.

   Sentence case, near-normal tracking, and a size that holds its own. Contrast
   was never the issue: it measured 17:1 before and after. */
.faq summary {
  list-style: none; cursor: pointer;
  padding: var(--sw-space-6) 44px var(--sw-space-6) 0; position: relative;
  font-family: var(--sw-font-display); font-weight: 400;
  font-size: clamp(17px, 1.6vw, 21px); line-height: 1.4;
  letter-spacing: 0.01em; text-transform: none; color: var(--sw-navy);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: ""; position: absolute; right: 6px; top: 50%; width: 14px; height: 1px; background: var(--sw-navy); }
.faq summary::before {
  content: ""; position: absolute; right: 12px; top: 50%;
  width: 1px; height: 14px; margin-top: -7px; background: var(--sw-navy);
  transition: transform .3s var(--sw-ease), opacity .3s var(--sw-ease);
}
.faq details[open] summary::before { transform: scaleY(0); opacity: 0; }
.faq details > div { max-width: 68ch; color: var(--sw-text-muted); }

/* ============================================================== CONTACT ==== */
.contact__grid { display: grid; grid-template-columns: .85fr 1.15fr; gap: clamp(40px, 6vw, 88px); }
@media (max-width: 960px) { .contact__grid { grid-template-columns: 1fr; } }

.contact__aside dl { margin: var(--sw-space-7) 0 0; }
.contact__aside dt { font-size: 9px; font-weight: 500; letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase; color: var(--sw-mist); margin-bottom: var(--sw-space-2); }
.contact__aside dd {
  margin: 0 0 var(--sw-space-6);
  font-family: var(--sw-font-display); font-weight: 300; font-size: 20px;
  letter-spacing: var(--sw-track-display); text-transform: uppercase;
}
.contact__aside dd a { text-decoration: none; border-bottom: 1px solid rgba(255,255,255,.4); }
.contact__aside dd a:hover { border-bottom-color: var(--sw-white); }

.form { display: grid; gap: var(--sw-space-5); }
.form__row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sw-space-5); }
@media (max-width: 620px) { .form__row { grid-template-columns: 1fr; } }
.field { display: grid; gap: var(--sw-space-2); }
.field label, .field__legend { font-size: 9px; font-weight: 500; letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase; color: var(--sw-mist); }
.field input, .field select, .field textarea {
  background: transparent; color: var(--sw-white);
  border: 0; border-bottom: 1px solid rgba(255,255,255,.38); border-radius: 0;
  padding: 10px 0; font-size: 16px; font-weight: 400;
  transition: border-color .3s var(--sw-ease);
}
.field textarea { resize: vertical; min-height: 96px; }
.field input::placeholder, .field textarea::placeholder { color: rgba(255,255,255,.46); }
.field input:focus, .field select:focus, .field textarea:focus { border-bottom-color: var(--sw-white); outline: none; }
.field select option { color: var(--sw-ink); }

.checks { display: flex; flex-wrap: wrap; gap: var(--sw-space-3); }
.check { position: relative; }
.check input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.check span {
  display: inline-block; padding: 10px var(--sw-space-4);
  border: 1px solid rgba(255,255,255,.38);
  font-size: 9px; font-weight: 500; letter-spacing: var(--sw-track-label);
  text-transform: uppercase; cursor: pointer;
  transition: background-color .25s var(--sw-ease), border-color .25s var(--sw-ease);
}
.check input:checked + span { background: var(--sw-white); color: var(--sw-navy); border-color: var(--sw-white); }
.check input:focus-visible + span { outline: 2px solid var(--sw-white); outline-offset: 2px; }

.form__foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sw-space-5); }
.form__foot .fine { font-size: 13px; color: var(--sw-mist); max-width: 42ch; }

/* =============================================================== FOOTER ==== */
.footer { background: var(--sw-navy-deep); color: var(--sw-white); padding-block: var(--sw-space-9) var(--sw-space-7); }
.footer__grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: var(--sw-space-7); }
@media (max-width: 900px) { .footer__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .footer__grid { grid-template-columns: 1fr; } }
.footer img { width: 200px; height: auto; margin-bottom: var(--sw-space-5); }
.footer h4 { color: var(--sw-mist); margin-bottom: var(--sw-space-5); font-size: 9px; letter-spacing: var(--sw-track-eyebrow); }
.footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sw-space-3); }
.footer a { color: rgba(255,255,255,.86); text-decoration: none; font-size: 15px; }
.footer a:hover { text-decoration: underline; }
.footer__bar {
  margin-top: var(--sw-space-8); padding-top: var(--sw-space-5);
  border-top: 1px solid rgba(255,255,255,.16);
  display: flex; flex-wrap: wrap; gap: var(--sw-space-5); justify-content: space-between;
  font-size: 9px; font-weight: 500; letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase; color: var(--sw-mist);
}

/* ======================================================== MOBILE CALL BAR == */
.callbar { display: none; }
@media (max-width: 860px) {
  .callbar {
    position: fixed; inset: auto 0 0 0; z-index: 65;
    display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid rgba(255,255,255,.2);
  }
  /* OPAQUE. The "minimal buttons" pass set every .btn transparent on mobile,
     which caught these — and a sticky bar you can read the page through looks
     broken, not minimal. Minimal applies to buttons sitting ON something; this
     bar IS the surface. */
  .callbar a {
    min-height: 58px; border-radius: 0; border: 0;
    background: var(--sw-navy);
    color: var(--sw-white);
    backdrop-filter: none; -webkit-backdrop-filter: none;
    box-shadow: none;
  }
  .callbar a + a { background: var(--sw-oxblood); }
  .callbar a:active { background: var(--sw-navy-mid); }
  .callbar a + a:active { background: var(--sw-oxblood-deep); }

  body { padding-bottom: 84px; }
}

/* ------------------------------------------------- service card expansion ---
   Hover lifts the card out of the grid and grows it down and to the right, so
   the photograph gets room and the CTA appears under it.

   It grows via a transform on an absolutely-positioned inner layer rather than
   by changing the cell's size: changing the cell reflows all nine and the whole
   grid jumps. This way the grid never moves and the work stays on the
   compositor.

   Cards in the last column grow LEFT instead, or they would push past the frame
   and be clipped by the grid's own border. */
.services__grid { position: relative; }
.service {
  transition: background-color .35s var(--sw-ease),
              transform .45s var(--sw-ease),
              box-shadow .45s var(--sw-ease);
  transform-origin: top left;
}
@media (hover: hover) and (min-width: 1000px) {
  .service:hover, .service:focus-within {
    transform: scale(1.16);
    z-index: 5;
    box-shadow: 0 24px 60px rgba(5, 14, 31, .28);
  }
  .services__grid > *:nth-child(3n) { transform-origin: top right; }
  .services__grid > *:nth-child(n+7) { transform-origin: bottom left; }
  .services__grid > *:nth-child(9)   { transform-origin: bottom right; }

  /* The CTA is present but folded away until the card opens. */
  .service__more {
    max-height: 0; opacity: 0; overflow: hidden;
    transition: max-height .4s var(--sw-ease), opacity .3s var(--sw-ease), margin-top .4s var(--sw-ease);
    margin-top: 0;
  }
  .service:hover .service__more,
  .service:focus-within .service__more {
    max-height: 3em; opacity: 1; margin-top: var(--sw-space-4);
  }
  /* The feature card already breaks the hover-only pattern for its photograph
     ("the seasonal card shows its work at rest") because it ships first and
     is meant to be read without an interaction. Same logic for its own CTA:
     folding "Book for this season" down to opacity 0 left it as a barely
     visible ghost at rest, which read as broken rather than deliberate.
     Scoped to .service--feature only — the other seven cards keep the fold. */
  .service--feature .service__more {
    max-height: 3em; opacity: 1; margin-top: var(--sw-space-4);
  }
}
@media (prefers-reduced-motion: reduce) {
  .service { transition: background-color .35s var(--sw-ease); }
  .service:hover, .service:focus-within { transform: none; box-shadow: none; }
  .service__more { max-height: none; opacity: 1; }
}

/* ================================================================ SHOT ====
   The section head, on the pattern the subscription block already uses and the
   one that actually works here: a full-bleed photograph with the copy floating
   over it in a glass panel, rather than a picture parked above the type.

   Sides alternate down the page so it reads as a rhythm instead of a template.
   ========================================================================== */
.shot {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--sw-navy);
  margin-bottom: clamp(48px, 6vw, 96px);
  /* pull out to the section's own edges — the head is full-bleed, the content
     below it stays on the gutter */
  margin-inline: calc(var(--sw-gutter-page) * -1);
  display: flex;
}
.shot--right { justify-content: flex-end; }

.shot__media { position: absolute; inset: 0; z-index: -1; }
.shot__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 42%; }
/* Scrim runs toward whichever side the panel sits on, so the photograph stays
   open on the other. Same 45-60% band the guideline sets for type on imagery. */
.shot--left  .shot__media::after,
.shot--right .shot__media::after {
  content: ""; position: absolute; inset: 0;
}
.shot--left .shot__media::after {
  background: linear-gradient(90deg, rgba(5,14,31,.88) 0%, rgba(5,14,31,.72) 38%, rgba(5,14,31,.30) 100%);
}
.shot--right .shot__media::after {
  background: linear-gradient(90deg, rgba(5,14,31,.30) 0%, rgba(5,14,31,.72) 62%, rgba(5,14,31,.88) 100%);
}

.shot__panel {
  position: relative;
  width: min(720px, 94%);
  margin-block: clamp(56px, 8vw, 104px);
  padding: clamp(32px, 4vw, 56px);
  background: rgba(5, 14, 31, .52);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .shot__panel { background: rgba(5, 14, 31, .82); }
}
/* The head inside the panel inverts — it was written for a white surface. */
.shot__panel .section-head { margin-bottom: 0; max-width: none; }
.shot__panel h2 { color: var(--sw-white); }
.shot__panel .eyebrow { color: var(--sw-mist); }
.shot__panel p { color: rgba(255,255,255,.90); }

/* Provenance, on screen, in oxblood for AI and navy for real — so nobody in the
   room has to ask which is which, and a placeholder cannot quietly ship. */
.shot__flag {
  position: absolute; left: 0; bottom: 0; z-index: 2;
  background: var(--sw-navy); color: var(--sw-white);
  font-size: 9px; font-weight: 500;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase;
  padding: 7px 14px;
}
.shot__flag.placeholder { background: var(--sw-oxblood); }

/* On a phone a shot STACKS — full-width photograph, then a solid navy panel
   under it — instead of floating the copy on top of the image. Overlaying works
   at 1400px where there is room either side of the panel; at 390px the panel
   covers the photograph almost entirely and you get a dark rectangle with a
   sliver of house behind it.

   This is the same shape .split already takes on mobile, which is the one that
   reads properly. Now every section behaves the same way down there. */
@media (max-width: 860px) {
  .shot {
    display: block;
    margin-inline: calc(var(--sw-gutter-page) * -1);
  }
  .shot__media {
    position: relative; inset: auto;
    height: clamp(190px, 44vw, 280px);
    z-index: 0;
  }
  /* No scrim: nothing sits on the photograph any more. */
  .shot--left .shot__media::after,
  .shot--right .shot__media::after { display: none; }

  .shot__panel {
    width: 100%;
    margin-block: 0;
    padding: var(--sw-space-7) var(--sw-gutter-page);
    background: var(--sw-navy);
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .shot__flag { top: auto; bottom: auto; }
  .shot__media .shot__flag,
  .shot .shot__flag { bottom: auto; top: clamp(190px, 44vw, 280px); transform: translateY(-100%); }
}

/* A section that leads with a photograph starts ON the photograph. The section
   padding was putting a band of surface color above the image, which read as a
   dead strip — worst on the alt sections, where that strip is bronco tint.

   The only tinted band left between blocks is the --sw-gap seam itself, which is
   the point: that is where the window-light ground shows through as you scroll. */
/* Retired 2026-08-18. Two exceptions used to zero the padding on any section
   leading with a full-bleed shot or split. Because only SOME sections hit them,
   the air across a boundary came out at 6px, 94px or 182px depending on which
   pair happened to meet. Every section carries --sw-band now, so a boundary is
   always pad + seam + pad.

   The cost: a leading photograph no longer touches the seam, it sits in a band
   of glass. That was a deliberate look once and this is a deliberate reversal.
   To restore it, put both of these back together:
     section:has(> .wrap > .shot:first-child)  -> padding-top: 0
     section:has(> .wrap > .split:first-child) -> padding-block: 0 */

/* =================================================== INTERIOR PAGE HERO ====
   Every page other than the homepage opens with this. Deliberately NOT the
   homepage treatment: that one is a full-viewport film, and a second
   full-screen film on an interior page reads as a second homepage rather than
   a section of the same site. This states what the page is and gets out of the
   way, at roughly half the height.

   Same navy + scrim language as the homepage hero so the two read as one
   family, and the same --sw-band / --sw-frame rhythm as every other block. */
.phero {
  position: relative;
  background: var(--sw-navy);
  color: var(--sw-white);
  margin-inline: var(--sw-frame);
  margin-top: var(--sw-frame);
  margin-bottom: var(--sw-gap);
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(12, 27, 54, .10);
  min-height: clamp(340px, 46vh, 520px);
  display: flex; align-items: flex-end;
}
.phero__media { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.phero__media img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 42%; }
/* Directional scrim toward the copy, same 45-60% band the guideline sets for
   type over imagery. The copy sits bottom-left, so the weight goes left. */
.phero__media::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, rgba(5,14,31,.92) 0%, rgba(5,14,31,.72) 46%, rgba(5,14,31,.34) 100%);
}
.phero__inner {
  position: relative; z-index: 1;
  padding-block: clamp(40px, 6vw, 72px);
}
.phero h1 { font-size: clamp(30px, 4.4vw, 52px); line-height: 1.10; margin-bottom: var(--sw-space-4); }
.phero__sub {
  font-size: clamp(16px, 1.4vw, 18px);
  color: rgba(255,255,255,.92);
  max-width: 54ch;
  line-height: var(--sw-leading-body);
  margin-bottom: var(--sw-space-6);
}
.phero__actions { display: flex; flex-wrap: wrap; gap: var(--sw-space-3); }

/* Breadcrumb. Small, quiet, and a real nav landmark for screen readers. */
.crumb {
  font-size: 9px; font-weight: 500;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase;
  color: var(--sw-mist); margin-bottom: var(--sw-space-4);
}
.crumb a { color: var(--sw-mist); text-decoration: none; border-bottom: 1px solid rgba(255,255,255,.28); }
.crumb a:hover, .crumb a:focus-visible { color: var(--sw-white); border-bottom-color: var(--sw-white); }
.crumb span { opacity: .72; }

@media (max-width: 860px) {
  .phero { min-height: 0; }
  .phero__media::after {
    background: linear-gradient(180deg, rgba(5,14,31,.72) 0%, rgba(5,14,31,.88) 100%);
  }
}

/* ===================================================== SCROLL BEHAVIOR ====
   No scroll snap. A proximity magnet was tried on 2026-08-17 and removed the
   same day — even at proximity strength it fights you on a page where most
   sections are taller than the viewport, because every pause near a boundary
   gets grabbed. Scrolling is left entirely alone.

   scroll-padding-top stays: it keeps anchor jumps (#services, #faq) clear of
   the fixed header, which is a different job and still needed. */
html {
  scroll-padding-top: calc(var(--sw-header-h) + var(--sw-frame) + var(--sw-gap));
}

/* ============================================================ SPLIT ===
   The section layout: 50/50, photograph full-cover on one side, copy on a navy
   panel on the other. Used by the FAQ and the hard-water block, and it is the
   shape every content section should take — a head band over an image followed
   by a text grid left too much empty page below it.
   ========================================================================== */
.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  align-items: stretch;
  /* full-bleed to the section edges — the head band already bleeds, so this
     matches rather than sitting inside the gutter */
  margin-inline: calc(var(--sw-gutter-page) * -1);
}
.split__media {
  position: relative;
  margin: 0;
  overflow: hidden;
  min-height: clamp(420px, 52vw, 760px);
}
/* video as well as img: the area pages run short muted job clips in this slot */
.split__media img, .split__media video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 50%;
  display: block;
}
.split__media .shot__flag { position: absolute; left: 0; bottom: 0; z-index: 2; }

/* SOLID navy, not glass.

   Glass was tried here first and came out grey. backdrop-filter frosts whatever
   is BEHIND the element, and this panel sits beside the photograph in the other
   grid column — so it was blurring the white page, and 52% navy over white is
   grey. The subscription block gets away with glass because its panel overlaps
   the image; nothing is behind this one.

   Solid navy is also the more consistent answer: it is the same surface the
   service-area and quote blocks already use. */
.split__body {
  background: var(--sw-navy);
  color: var(--sw-white);
  padding: clamp(36px, 4.5vw, 72px);
  display: flex; flex-direction: column; justify-content: center;
}
/* The list was written for a light surface; invert it. */
.split__body .eyebrow { color: var(--sw-mist); }
.split__body h2 { color: var(--sw-white); }
.split__body .faq summary { color: var(--sw-white); }
.split__body .faq details[open] summary { color: var(--sw-white); }
.split__body .faq summary:hover,
.split__body .faq summary:focus-visible { color: var(--sw-bronco); }
.split__body .faq details > div { color: rgba(255,255,255,.88); }
.split__body .faq summary::after,
.split__body .faq summary::before { background: var(--sw-white); }
.split__body .faq::before,
.split__body .faq details::after { background: rgba(255,255,255,.22); }
.split__body .section-head { margin-bottom: clamp(24px, 2.6vw, 40px); }
.split .faq { max-width: none; }

@media (max-width: 900px) {
  .split { grid-template-columns: 1fr; }
  .split__media { min-height: clamp(240px, 46vw, 340px); }
  .split__body { padding-inline: var(--sw-gutter-page); }
}
/* ^ this block was left unclosed by an earlier edit and swallowed every rule
   below it, so the whole interaction pass only applied under 900px wide. Brace
   TOTALS still balanced, which is why a count did not catch it — check depth at
   the line, not the sum. */

/* ================================================== INTERACTION POLISH ====
   The same care the service cards got, applied to everything that responds to
   a pointer or a click. One easing, one language: things settle, they do not
   snap.
   ========================================================================== */

/* --- FAQ: the answer opens, it does not appear ---------------------------
   Height is set by site.js, not by CSS.

   Two CSS-only routes were tried and neither moved: a 0fr -> 1fr grid track
   (resolved to 0px in both states) and max-height with a cap (applied
   inconsistently — repeated measurements disagreed with each other). An
   explicit pixel height is deterministic, and it also removes the cap, so a
   long answer can never clip the way a max-height guess would.

   The native <details> toggle still drives it, so keyboard, find-in-page and
   no-JS all keep working. Without JS the panel is simply open. */
.faq details > div {
  overflow: hidden;
  opacity: 0;
  transition: height .42s var(--sw-ease), opacity .3s var(--sw-ease);
  /* The padding moves to the child. Left on this element it survives
     height:0 and the panel never collapses past 32px — measured exactly that. */
  padding-bottom: 0;
}
.faq details > div > * { padding-bottom: var(--sw-space-6); }
.faq details[open] > div { opacity: 1; }

/* The question itself lifts slightly and the marker rotates into a minus. */
.faq summary { transition: color .28s var(--sw-ease), padding-left .28s var(--sw-ease); }
.faq summary:hover, .faq summary:focus-visible { color: var(--sw-oxblood); padding-left: 6px; }
.faq summary::after,
.faq summary::before { transition: transform .32s var(--sw-ease), opacity .32s var(--sw-ease); }
.faq details[open] summary::after { transform: rotate(180deg); }
.faq details[open] summary { color: var(--sw-navy); }

/* --- Links and buttons ---------------------------------------------------- */
.btn { transition: background-color .3s var(--sw-ease), color .3s var(--sw-ease),
                   border-color .3s var(--sw-ease), transform .2s var(--sw-ease); }
.btn:hover, .btn:focus-visible { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

/* Nav links draw their underline from the left rather than fading it in. */
.nav a { position: relative; border-bottom-color: transparent !important; }
.nav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -3px; height: 1px;
  background: currentColor;
  transform: scaleX(0); transform-origin: left;
  transition: transform .32s var(--sw-ease);
}
.nav a:hover::after, .nav a:focus-visible::after { transform: scaleX(1); }

/* Service-area chips fill from the bottom. */
.areas__list a { position: relative; overflow: hidden; }
.areas__list a::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: var(--sw-navy-mid);
  transform: scaleY(0); transform-origin: bottom;
  transition: transform .34s var(--sw-ease);
}
.areas__list a:hover::before, .areas__list a:focus-visible::before { transform: scaleY(1); }
.areas__list a { transition: color .3s var(--sw-ease); }

/* Footer links slide a little on hover, same idiom as the FAQ questions. */
.footer a { transition: color .28s var(--sw-ease), padding-left .28s var(--sw-ease); }
.footer a:hover, .footer a:focus-visible { padding-left: 5px; }

/* The review ticks grow rather than just recolouring. */
.quotes__tick { transition: background-color .4s var(--sw-ease), transform .3s var(--sw-ease); }
.quotes__tick:hover { transform: scaleY(2.5); }

/* Form fields: the underline thickens from the left as you focus. */
.field input, .field textarea { transition: border-color .3s var(--sw-ease), background-color .3s var(--sw-ease); }

@media (prefers-reduced-motion: reduce) {
  .faq details > div { transition: none; }
  .btn:hover, .btn:focus-visible { transform: none; }
  .nav a::after, .areas__list a::before { transition: none; }
  .quotes__tick:hover { transform: none; }
  .faq summary:hover, .footer a:hover { padding-left: 0; }
}

/* Flip: photograph on the right instead of the left. Alternating the side down
   the page keeps the rhythm the shots already established. */
.split--flip { direction: rtl; }
.split--flip > * { direction: ltr; }

/* Copy inside a split panel is on navy, so the muted greys written for a light
   surface have to invert — slate on navy measures 1.9:1. */
.split__body .muted,
.split__body .lede { color: var(--sw-mist); }
.split__body p { color: rgba(255,255,255,.90); }

/* The fact cards sit below the split, full width, on the monogram ground. */
.water__facts { margin-top: clamp(32px, 4vw, 64px); }

/* The quote block and the footer are one unit — both navy, and a seam of ground
   between them just breaks a single dark mass in half for no reason. Every
   other seam stays; this one closes. */
main > section:last-child { margin-bottom: 0; }

/* The quarterly plan is the offer, not one of nine equal boxes. Aaron's own
   sales approach on the call was to steer to quarterly in the first
   conversation (the Glide model), so it comes out of the pile, sits above it,
   and starts selected. Anyone who only wants a one-off unticks it. */
.check--lead {
  display: flex; align-items: center; gap: var(--sw-space-3);
  margin-bottom: var(--sw-space-5);
  padding: var(--sw-space-4) var(--sw-space-5);
  border: 1px solid rgba(255,255,255,.34);
  font-size: 11px; font-weight: 500;
  letter-spacing: var(--sw-track-label); text-transform: uppercase;
  cursor: pointer;
  transition: background-color .3s var(--sw-ease), border-color .3s var(--sw-ease);
}
.check--lead:hover { border-color: var(--sw-white); background: rgba(255,255,255,.06); }
.check--lead:has(input:checked) { background: rgba(255,255,255,.10); border-color: var(--sw-white); }

/* ============================================== MOBILE REFINEMENT ========
   Two things read as unfinished on a phone: the menu showed its construction,
   and the buttons were desktop chrome shrunk down rather than designed for a
   thumb.
   ========================================================================== */

/* The drawer's five panes are a WIPE, not a layout. Once they have landed they
   are one navy field, and the hairlines between them just expose the mechanism
   — you can see the page through the seams mid-animation. They stay separate
   while moving and become seamless the moment they arrive. */
.drawer__curtain span { border-right: 0; }
.drawer__curtain span + span { margin-left: -1px; }

@media (max-width: 860px) {
  /* Buttons: full-bleed and thumb-sized. 52px was a desktop control on a phone;
     the WCAG target minimum is 44px and comfortable is well past it. Tracking
     comes down because 0.34em on a 10px label at phone width is most of the
     button's width in letter-spacing alone. */
  .btn {
    min-height: 58px;
    font-size: 11px;
    letter-spacing: 0.22em;
    padding-inline: var(--sw-space-6);
  }
  .hero__actions { display: grid; grid-template-columns: 1fr; gap: var(--sw-space-3); }
  .hero__actions .btn { width: 100%; }

  /* The ghost button was a 1px outline on footage — at phone size that reads as
     a placeholder. Give it a surface of its own. */
  .hero__actions .btn--ghost {
    background: rgba(255,255,255,.10);
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    border-color: rgba(255,255,255,.55);
  }

  /* Only the five section anchors get the big treatment. This used to be
     `.drawer a:not(.btn)`, which also caught the service list, the city list
     and the email — the email came out at 26px and 430px wide in a 390px
     drawer, which is what broke the panel. */
  .drawer__nav a {
    font-size: 26px;
    padding-block: var(--sw-space-4);
  }
  .drawer .btn { min-height: 58px; width: 100%; }
}

/* A pressed state, because on touch there is no hover to confirm the tap. */
.btn:active { transform: translateY(1px); }

/* ================================================= DRAWER: FULL MENU =====
   A phone menu should be the whole site, not five anchors and a button. Every
   service and every city is reachable from here, grouped and labelled, so the
   menu does the job the footer does on desktop.
   ========================================================================== */
.drawer__inner { gap: 0; padding-block: var(--sw-space-6) var(--sw-space-8); }

.drawer__top {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: var(--sw-space-7);
}
.drawer__mark { width: 132px; height: auto; }

/* The five section anchors stay big — they are the primary route. */
.drawer__nav { display: flex; flex-direction: column; }
.drawer__nav a {
  font-family: var(--sw-font-display); font-weight: 300; text-transform: uppercase;
  font-size: clamp(22px, 6.4vw, 30px); letter-spacing: var(--sw-track-display);
  padding-block: var(--sw-space-4);
  border-bottom: 1px solid rgba(255,255,255,.16);
  transition: padding-left .3s var(--sw-ease), color .3s var(--sw-ease);
}
.drawer__nav a:hover, .drawer__nav a:focus-visible { padding-left: 8px; color: var(--sw-bronco); }

/* Everything else in two quiet columns underneath. */
.drawer__cols {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: var(--sw-space-6) var(--sw-space-5);
  margin-top: var(--sw-space-7);
}
.drawer__col { display: flex; flex-direction: column; gap: var(--sw-space-3); }
.drawer__col h4 {
  color: var(--sw-mist); margin: 0 0 var(--sw-space-2);
  font-size: 9px; letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase;
}
.drawer__col a {
  font-size: 15px; line-height: 1.35;
  color: rgba(255,255,255,.86);
  transition: color .25s var(--sw-ease), padding-left .25s var(--sw-ease);
}
.drawer__col a:hover, .drawer__col a:focus-visible { color: var(--sw-white); padding-left: 4px; }

.drawer__foot {
  margin-top: var(--sw-space-8);
  padding-top: var(--sw-space-6);
  border-top: 1px solid rgba(255,255,255,.16);
  display: flex; flex-direction: column; gap: var(--sw-space-4);
}
.drawer__tel {
  font-family: var(--sw-font-display); font-weight: 300;
  font-size: 26px; letter-spacing: 0.06em;
}
/* An email address is not a label. Uppercased and tracked at 0.28em it was
   ~430px wide in a 390px drawer, so it pushed the whole panel sideways and
   everything in it went off-screen. Lower case, normal tracking, and allowed to
   break if it still does not fit. */
.drawer__mail {
  font-size: 15px; letter-spacing: 0; text-transform: none;
  color: var(--sw-mist);
  overflow-wrap: anywhere;
  max-width: 100%;
}
/* Nothing in the drawer may exceed its width, whatever it contains. */
.drawer__inner, .drawer__inner * { min-width: 0; max-width: 100%; }
.drawer__inner { overflow-x: hidden; }
.drawer__note {
  margin: var(--sw-space-2) 0 0;
  font-size: 9px; letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase;
  color: var(--sw-mist);
}

/* The stagger has more to carry now, so it runs quicker per item or the last
   row arrives a second and a half after the first. */
.drawer[data-open="true"] .drawer__inner > *:nth-child(1) { transition-delay: .26s; }
.drawer[data-open="true"] .drawer__inner > *:nth-child(2) { transition-delay: .31s; }
.drawer[data-open="true"] .drawer__inner > *:nth-child(3) { transition-delay: .36s; }
.drawer[data-open="true"] .drawer__inner > *:nth-child(4) { transition-delay: .41s; }

/* ------------------------------------------------------ buttons, minimal ---
   The frosted fill and the full-width slabs read as app chrome. Back to the
   brand's own language: a hairline, wide caps, and one oxblood rule to mark the
   primary. Still 56px tall for the thumb — minimal is about weight, not size. */
@media (max-width: 860px) {
  .btn {
    min-height: 56px;
    font-size: 10px;
    letter-spacing: 0.28em;
    padding-inline: var(--sw-space-6);
    background: transparent;
    border: 1px solid rgba(255,255,255,.42);
    color: var(--sw-white);
  }
  .hero__actions .btn--ghost {
    background: transparent;
    backdrop-filter: none; -webkit-backdrop-filter: none;
    border-color: rgba(255,255,255,.42);
  }
  /* Primary is marked by a filled rule along its foot, not by a slab of color. */
  .btn--primary {
    background: transparent;
    border-color: rgba(255,255,255,.42);
    box-shadow: inset 0 -3px 0 0 var(--sw-oxblood);
  }
  .btn--primary:active { box-shadow: inset 0 -2px 0 0 var(--sw-oxblood); }
  /* On a light surface the hairline has to invert. */
  .section .btn, .split__body .btn { border-color: rgba(255,255,255,.42); }
}

/* ============================================== QUOTE FORM: THE BUTTON ===
   The submit was a flat oxblood rectangle sitting beside grey small print, and
   it did not read as the thing to press. It is now the widest, tallest element
   in the form, with the note moved beneath it rather than competing alongside.

   This is the one place on the page where a solid oxblood fill is right: the
   guideline allows one accent per view, and on a navy form the single action
   is exactly what it is for. */
.form__foot {
  display: flex; flex-direction: column; gap: var(--sw-space-4);
  align-items: stretch;
  margin-top: var(--sw-space-6);
}
.btn--submit {
  width: 100%;
  min-height: 64px;
  background: var(--sw-oxblood);
  border: 1px solid var(--sw-oxblood);
  color: var(--sw-white);
  font-size: 12px;
  letter-spacing: 0.30em;
  cursor: pointer;
  transition: background-color .3s var(--sw-ease), transform .18s var(--sw-ease);
}
.btn--submit:hover, .btn--submit:focus-visible {
  background: var(--sw-oxblood-deep);
  border-color: var(--sw-oxblood-deep);
  transform: translateY(-2px);
}
.btn--submit:active { transform: translateY(0); }
.form__note {
  margin: 0;
  font-size: 11px; line-height: 1.6; color: var(--sw-mist);
}
/* Outside .field on purpose, so the consent line doesn't pick up the animated focus underline. */
.consent {
  display: flex; gap: var(--sw-space-3); align-items: flex-start;
  font-size: 13px; line-height: 1.6; color: var(--sw-mist);
  cursor: pointer;
}
.consent input {
  flex: 0 0 auto; width: 18px; height: 18px; margin: 3px 0 0;
  accent-color: var(--sw-white); cursor: pointer;
}
.consent input:focus-visible { outline: 2px solid var(--sw-white); outline-offset: 2px; }
.form__trap { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
.form__status { margin: 0; font-size: 14px; line-height: 1.6; color: var(--sw-white); }
.form__status.is-error { border-left: 2px solid var(--sw-oxblood); padding-left: var(--sw-space-3); }
/* Mobile keeps the fill here — a hairline on the one action people came to
   press would be minimal at the cost of being findable. */
@media (max-width: 860px) {
  .btn--submit {
    background: var(--sw-oxblood);
    border-color: var(--sw-oxblood);
    box-shadow: none;
    min-height: 62px;
  }
}

/* ================================================ QUOTE ASIDE: THE PROOF ==
   The column beside the form used to be a contact card — phone, email, service
   area. The phone and email are in the footer, the drawer and the sticky mobile
   bar already, so repeating them here bought nothing and pushed the one thing
   that actually persuades below the fold.

   On the call the explicit decision was to let the reviews carry it rather than
   the two-year tenure. So the count is the loudest thing in this column. */
.vouch {
  padding-bottom: var(--sw-space-7);
  margin-bottom: var(--sw-space-7);
  border-bottom: 1px solid rgba(255,255,255,.18);
}
.vouch__stars { display: flex; gap: 6px; color: var(--sw-white); font-size: 14px; margin-bottom: var(--sw-space-5); }
.vouch__num {
  font-family: var(--sw-font-display); font-weight: 200;
  font-size: clamp(56px, 7vw, 92px); line-height: .9;
  letter-spacing: 0.01em; margin: 0; color: var(--sw-white);
}
.vouch__label {
  margin: var(--sw-space-3) 0 0;
  font-size: 10px; font-weight: 500;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase;
  color: var(--sw-mist);
}
.vouch__line {
  margin: var(--sw-space-5) 0 0;
  font-size: 17px; line-height: 1.6; color: rgba(255,255,255,.90);
  max-width: 34ch;
}
.vouch__link {
  display: inline-block; margin-top: var(--sw-space-5);
  font-size: 10px; font-weight: 500;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase;
  color: var(--sw-white);
  border-bottom: 1px solid rgba(255,255,255,.42);
  padding-bottom: 4px;
  transition: border-color .3s var(--sw-ease), padding-left .3s var(--sw-ease);
}
.vouch__link:hover, .vouch__link:focus-visible { border-color: var(--sw-white); padding-left: 4px; }

/* The intro reads before the fields now, not in a column beside them. */
.form__intro {
  margin: 0 0 var(--sw-space-7);
  font-size: 17px; line-height: 1.65; color: rgba(255,255,255,.90);
  max-width: 54ch;
}

/* The real review quote beside the form. Hidden until reviews.json holds real
   data — see site.js. Sized to sit under the count without competing with it. */
.vouch__quote {
  margin: var(--sw-space-6) 0 0;
  padding-left: var(--sw-space-5);
  border-left: 2px solid rgba(255,255,255,.34);
}
.vouch__quote blockquote {
  margin: 0;
  font-size: 16px; line-height: 1.7; color: rgba(255,255,255,.90);
  max-width: 36ch;
  /* Belt and braces. The JS trims to ~180 characters, but reviews are user
     text and a single unbroken run could still overflow — this caps the block
     at six lines whatever arrives, so the aside can never push the form out of
     shape again. */
  display: -webkit-box;
  -webkit-line-clamp: 6;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.vouch__quote figcaption {
  margin-top: var(--sw-space-3);
  font-size: 9px; font-weight: 500;
  letter-spacing: var(--sw-track-eyebrow); text-transform: uppercase;
  color: var(--sw-mist);
}

/* ================================================= FORM FIELDS: AUTOFILL ==
   Chrome paints autofilled inputs with its own pale background and dark text,
   which overrode the underlined-field design completely — filled fields showed
   as light boxes next to empty fields that were still hairlines, and the form
   read as half-broken.

   There is no property to switch that off. The working trick is an inset
   box-shadow large enough to cover the field, plus -webkit-text-fill-color,
   because `color` alone does not win against the autofill style. The long
   transition delay keeps Chrome from repainting its own background over the
   top on focus. */
.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus,
.field textarea:-webkit-autofill,
.field select:-webkit-autofill {
  -webkit-box-shadow: inset 0 0 0 1000px var(--sw-navy) !important;
          box-shadow: inset 0 0 0 1000px var(--sw-navy) !important;
  -webkit-text-fill-color: var(--sw-white) !important;
  caret-color: var(--sw-white);
  border-bottom-color: rgba(255,255,255,.38);
  transition: background-color 9999s ease-in-out 0s;
}

/* The rest of what made it read cheap: a native resize grabber in the corner,
   fields that gave no feedback until you left them, and a label that sat at
   the same weight whether the field was active or not. */
.field { position: relative; }
.field textarea {
  resize: none;                 /* the grabber is the giveaway; height is fixed */
  min-height: 132px;
}
.field input, .field textarea, .field select {
  padding: 14px 0 12px;
  border-bottom-width: 1px;
}
/* The underline thickens and brightens from the left as you focus, rather than
   just changing color. */
.field::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--sw-white);
  transform: scaleX(0); transform-origin: left;
  transition: transform .35s var(--sw-ease);
  pointer-events: none;
}
.field:focus-within::after { transform: scaleX(1); }
.field label {
  transition: color .3s var(--sw-ease);
}
.field:focus-within label { color: var(--sw-white); }

/* ------------------------------------------------ services grid on mobile ---
   Same shape as the sections: full-width photograph on top, copy underneath.

   A 96px thumbnail beside the text was tried first and it made the cards look
   like a directory listing — the photograph was too small to show anything and
   the row read as an icon plus a paragraph. Full width is the treatment that
   works everywhere else on the phone, so it is the treatment here. */
@media (max-width: 620px) {
  .services__grid { grid-template-columns: 1fr; }

  .service {
    display: flex; flex-direction: column;
    padding: 0;
    gap: 0;
  }
  /* The image sits first in the DOM after the index, so order lifts it to the
     top without touching the markup or the reading order for a screen reader. */
  .service__img {
    order: -1;
    position: relative; inset: auto;
    width: 100%; height: clamp(170px, 42vw, 230px);
    opacity: 1; z-index: 0;
  }
  .service__img img { transform: none; }
  /* A light scrim only — nothing sits on the photograph now. */
  .service__img::after { background: rgba(5,14,31,.08); }

  .service__index,
  .service h3,
  .service p,
  .service__more {
    padding-inline: var(--sw-space-6);
  }
  .service__index { padding-top: var(--sw-space-6); }
  .service h3 { margin-top: var(--sw-space-3); font-size: 18px; }
  .service p { margin-top: var(--sw-space-3); font-size: 15px; }
  .service__more { padding-bottom: var(--sw-space-6); margin-top: var(--sw-space-5); }

  /* The card keeps its light surface — the photo is above the type, not behind
     it, so nothing needs to invert.

     :not(.service--feature) on every one of these, and it is load-bearing.
     `:has()` takes the specificity of its argument, so `.service:has(.service__img) h3`
     scores (0,3,1) and beats `.service--feature h3` at (0,1,1) no matter which
     comes last — the Christmas heading rendered navy on navy and vanished.
     Third time this trap has bitten in this stylesheet. */
  .service:has(.service__img):not(.service--feature) { background: var(--sw-white); }
  .service:has(.service__img):not(.service--feature) h3 { color: var(--sw-navy); }
  .service:has(.service__img):not(.service--feature) p { color: var(--sw-text-muted); }
  .service:has(.service__img):not(.service--feature) .service__index { color: var(--sw-slate); }
  .service:has(.service__img):not(.service--feature) .service__more { color: var(--sw-navy); }

  /* The Christmas card stays navy. */
  .service--feature,
  .service--feature:has(.service__img) { background: var(--sw-navy); }
  .service--feature:has(.service__img) h3 { color: var(--sw-white); }
  .service--feature:has(.service__img) p,
  .service--feature:has(.service__img) .service__index { color: var(--sw-mist); }
  .service--feature:has(.service__img) .service__more { color: var(--sw-white); }

  /* No lift on touch — there is no hover to lift out of. */
  .service:hover, .service:focus-within { transform: none; box-shadow: none; }
}

