/* ==========================================================================
   777 HVAC — premium navy design system
   Self-contained. System fonts only. No external requests.
   ========================================================================== */

/* ----- Reset ----- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  font-family: var(--sans);
  color: var(--ink);
  background: var(--paper);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
img, picture, svg { display: block; max-width: 100%; }
img { height: auto; }
a { color: inherit; text-decoration: none; }
button, input, textarea, select { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--azure); outline-offset: 3px; border-radius: 3px; }

.skip-link {
  position: absolute; left: 12px; top: -60px; z-index: 200;
  background: var(--azure); color: #04101F; font-weight: 600;
  padding: 0.7em 1.1em; border-radius: 10px; transition: top 0.25s var(--ease);
}
.skip-link:focus { top: 12px; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ----- Tokens ----- */
:root {
  /* Navy scale */
  --navy-950: #050D18;
  --navy-900: #081525;
  --navy-850: #0A1A2F;
  --navy-800: #0E2138;
  --navy-700: #143253;
  --navy-600: #1D466E;

  /* Neutrals */
  --paper: #FFFFFF;
  --mist:  #F4F7FB;
  --mist-2:#EAF0F7;
  --ink:   #0C1D33;
  --muted: #566579;
  --line:  rgba(12, 29, 51, 0.10);
  --line-dark: rgba(255, 255, 255, 0.12);

  /* Accents */
  --azure:   #4DA3FF;
  /* Darkened from #2E86E6, which only reached 3.70:1 on white — under the
     4.5:1 minimum for the small bold text that uses it (eyebrows, textlinks,
     licence fine print). This clears 5.0:1 on paper and 4.66:1 on mist. */
  --azure-600: #1D6FCC;
  --azure-glow: rgba(77, 163, 255, 0.35);
  --brass:   #C9A96A;

  /* Type */
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;

  /* Rhythm */
  --container: 1200px;
  --gutter: clamp(20px, 5vw, 64px);
  --section-y: clamp(72px, 10vw, 140px);
  --radius: 16px;
  --radius-lg: 24px;
  --shadow: 0 24px 60px -24px rgba(8, 21, 37, 0.35);
  --shadow-sm: 0 12px 30px -16px rgba(8, 21, 37, 0.28);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* ----- Typography ----- */
h1, h2, h3, h4 { font-family: var(--serif); font-weight: 600; line-height: 1.08; letter-spacing: -0.015em; color: var(--ink); }
/* Hero headline only — .display is on the three hero h1s and nothing else.
   Dropped from a 5.1rem ceiling to 3.4rem (~33% smaller on desktop, ~27% on
   phones). Line-height opens slightly because 1.02 was set for very large type
   and reads cramped once the size comes down. */
.display {
  font-size: clamp(1.9rem, 4.3vw, 3.4rem);
  line-height: 1.06;
  letter-spacing: -0.025em;
}
h2.section-title { font-size: clamp(2rem, 4.4vw, 3.3rem); letter-spacing: -0.025em; }
h3 { font-size: clamp(1.25rem, 2vw, 1.5rem); letter-spacing: -0.01em; }
p { color: var(--muted); }
.lead { font-size: clamp(1.1rem, 1.7vw, 1.35rem); line-height: 1.55; color: var(--ink); }

.eyebrow {
  font-family: var(--sans);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--azure-600);
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
}
.eyebrow::before {
  content: "";
  width: 26px; height: 1.5px;
  background: var(--azure); opacity: 0.8;
  display: inline-block;
}
.band-dark .eyebrow, .eyebrow.on-dark { color: var(--azure); }
/* Over photography the azure eyebrow measured 1.07:1 against the sky — it was
   effectively invisible. White carries it; the azure dash keeps the accent. */
.hero .eyebrow.on-dark { color: #fff; }

/* ----- Layout ----- */
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
section { position: relative; }
.section { padding-block: var(--section-y); }
/* The header is fixed, so a deep-linked section would otherwise start underneath it. */
section[id] { scroll-margin-top: clamp(88px, 11vh, 116px); }
.stack > * + * { margin-top: 1.1rem; }
.center { text-align: center; }
.measure { max-width: 62ch; }
.center.measure, .center .measure { margin-inline: auto; }

/* ----- Buttons ----- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.55em;
  padding: 0.95em 1.7em;
  border-radius: 999px;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 0.98rem;
  letter-spacing: 0.005em;
  border: 1.5px solid transparent;
  cursor: pointer;
  transition: transform 0.35s var(--ease), background 0.3s var(--ease), box-shadow 0.35s var(--ease), border-color 0.3s var(--ease), color 0.3s var(--ease);
  will-change: transform;
}
.btn svg { width: 1.05em; height: 1.05em; }
.btn-primary {
  background: var(--azure);
  color: #04101F;
  box-shadow: 0 14px 30px -12px var(--azure-glow);
}
.btn-primary:hover { background: #6FB6FF; transform: translateY(-2px); box-shadow: 0 20px 40px -12px var(--azure-glow); }
.btn-ghost { background: transparent; border-color: rgba(255,255,255,0.35); color: #fff; }
.btn-ghost:hover { border-color: #fff; background: rgba(255,255,255,0.08); transform: translateY(-2px); }
.btn-outline { background: transparent; border-color: var(--line); color: var(--ink); }
.btn-outline:hover { border-color: var(--navy-700); background: var(--mist); transform: translateY(-2px); }
.btn-lg { padding: 1.05em 2.1em; font-size: 1.05rem; }

.textlink { color: var(--azure-600); font-weight: 600; display: inline-flex; align-items: center; gap: 0.4em; transition: gap 0.3s var(--ease); }
.textlink:hover { gap: 0.7em; }
.textlink svg { width: 0.9em; height: 0.9em; }
.band-dark .textlink { color: var(--azure); }

/* ==========================================================================
   Preloader
   ========================================================================== */
/* Brand moment, then out — 2s total (1.5s hold + 0.5s fade). The beats are
   staged so nothing is cut off: logo settles by 0.5s, the sheen rakes across
   from 0.35s to 1.5s, and the fade takes over the instant it lands.
   Hidden outright under reduced-motion (see the block at the end of the
   file): clamping duration there would still leave the delay running. */
.preloader {
  position: fixed; inset: 0; z-index: 300;
  display: grid; place-items: center;
  background: var(--navy-900);
  animation: preloader-out 0.5s var(--ease) 1.5s forwards;
}

/* Engraved: fill sits darker than the navy, with a shadow along the top
   inner edge and a cool highlight along the bottom — light from above. */
.preloader-logo {
  position: relative;
  font-family: var(--serif);
  font-size: clamp(2.1rem, 7vw, 4.5rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.06em;
  /* letter-spacing also trails the final glyph, which pushes the text
     off-centre by half a step; the negative margin takes that back. */
  margin-right: -0.06em;
  color: var(--navy-950);
  text-shadow:
    0 -1px 1px rgba(0, 0, 0, 0.8),
    0 1px 0 rgba(140, 190, 250, 0.38),
    0 2px 8px rgba(0, 0, 0, 0.40);
  animation: logo-in 0.5s var(--ease) both;
}

/* Light raking across the engraving. Percentage background-position resolves
   against (container - image), so with a 200% image the range below carries
   the highlight edge-to-edge exactly once. */
.preloader-logo::after {
  content: attr(data-text);
  position: absolute; inset: 0;
  background: linear-gradient(100deg,
    transparent 40%,
    rgba(180, 216, 255, 0.50) 50%,
    transparent 60%);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: logo-sheen 1.15s linear 0.35s both;
}

@keyframes logo-in {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: none; }
}
@keyframes logo-sheen {
  from { background-position: 140% 0; }
  to   { background-position: -40% 0; }
}
@keyframes preloader-out {
  to { opacity: 0; visibility: hidden; }
}

/* ==========================================================================
   Header
   ========================================================================== */
.site-header {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  transition: background 0.4s var(--ease), backdrop-filter 0.4s var(--ease), box-shadow 0.4s var(--ease), border-color 0.4s var(--ease);
  border-bottom: 1px solid transparent;
}
.site-header .container {
  display: flex; align-items: center; justify-content: space-between;
  height: 76px;
}
.site-header.scrolled, .site-header.solid {
  background: rgba(8, 21, 37, 0.72);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  border-bottom-color: var(--line-dark);
  box-shadow: 0 8px 30px -20px rgba(0,0,0,0.6);
}

.brand { display: inline-flex; align-items: center; gap: 0.7rem; color: #fff; }
.brand .logo { height: 30px; width: auto; }

.nav { display: flex; align-items: center; gap: clamp(1.1rem, 2.4vw, 2.4rem); }
.nav-links { display: flex; align-items: center; gap: clamp(1rem, 2.2vw, 2.2rem); list-style: none; }
.nav-links a {
  color: rgba(255,255,255,0.82); font-size: 0.96rem; font-weight: 500;
  position: relative; padding: 0.4em 0; transition: color 0.25s var(--ease);
}
.nav-links a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1.5px; width: 0;
  background: var(--azure); transition: width 0.3s var(--ease);
}
.nav-links a:hover, .nav-links a[aria-current="page"] { color: #fff; }
.nav-links a:hover::after, .nav-links a[aria-current="page"]::after { width: 100%; }

.nav-cta { display: flex; align-items: center; gap: 0.9rem; }
.nav-phone {
  display: inline-flex; align-items: center; gap: 0.45em;
  color: #fff; font-weight: 600; font-size: 0.95rem;
}
.nav-phone svg { width: 1.05em; height: 1.05em; color: var(--azure); }
.nav-phone span { color: rgba(255,255,255,0.9); }
.nav .btn-primary { padding: 0.7em 1.35em; font-size: 0.92rem; }

/* Hamburger */
.nav-toggle {
  display: none; width: 44px; height: 44px; background: transparent; border: 0;
  cursor: pointer; align-items: center; justify-content: center; position: relative; z-index: 120;
}
.nav-toggle span, .nav-toggle span::before, .nav-toggle span::after {
  content: ""; display: block; width: 24px; height: 2px; background: #fff; border-radius: 2px;
  transition: transform 0.35s var(--ease), opacity 0.2s var(--ease);
}
.nav-toggle span::before { position: absolute; transform: translateY(-7px); }
.nav-toggle span::after  { position: absolute; transform: translateY(7px); }
.nav-open .nav-toggle span { background: transparent; }
.nav-open .nav-toggle span::before { transform: rotate(45deg); }
.nav-open .nav-toggle span::after  { transform: rotate(-45deg); }

/* ==========================================================================
   Hero
   ========================================================================== */
.hero {
  position: relative; min-height: 100svh; display: flex; align-items: flex-end;
  color: #fff; overflow: hidden;
}
.hero.hero-page { min-height: 66vh; }
/* Layered on POSITIVE z-indexes (media 0 / overlay 1 / copy 2), not negative
   ones. These used to be -2 and -1, which worked only while .hero was not a
   stacking context. ScrollTrigger's pin puts `transform: translate(0,0)` on
   .hero, and a transform creates one — at which point negative-z children
   paint behind the hero's own (transparent) background and the hero renders
   as a black box. Positive values are correct either way. */
.hero-media { position: absolute; inset: 0; z-index: 0; }
.hero-inner { position: relative; z-index: 2; }
.scroll-cue { z-index: 2; }
/* <picture> needs the explicit box: it's the img's offset parent, so without a
   height here the img's height:100% resolves against auto and collapses to the
   photo's natural aspect ratio — leaving the lower part of the hero empty
   (very visible in portrait, where the hero is far taller than the photo). */
.hero-media picture { display: block; width: 100%; height: 100%; }
/* Same trap, every other framed photo on the site: <picture> is the img's
   containing block, so an img set to height:100% resolves that against auto
   and collapses to the photo's natural aspect ratio — leaving a gap inside a
   frame that has its own aspect-ratio. Give the picture the box too. */
.split-media picture,
.svc-media picture,
.team-card picture,
.cta-band .cta-media picture { display: block; width: 100%; height: 100%; }
.hero-media img { width: 100%; height: 100%; object-fit: cover; }

/* ----- Homepage hero: the whole photo, cropped only when it has to be -----
   The box's minimum height is the photo's own height at the screen's width
   (1920x1071 landscape; the 1080x1920 portrait cut on tall screens), so the
   photo normally shows edge to edge with nothing cut off. It is capped at the
   screen height: only when the photo would be taller than the screen — or the
   copy needs more room than the photo gives — does the box change shape, and
   object-fit: cover crops just enough to fill it.
   min-height rather than aspect-ratio: an aspect-ratio box capped by
   max-height shrinks its WIDTH to keep the ratio (the iPad hero came out
   576px wide), and it clips copy instead of growing to hold it. */
.hero:not(.hero-page) { min-height: min(calc(100vw * 1071 / 1920), 100svh); }
@media (max-aspect-ratio: 1/1) {
  .hero:not(.hero-page) { min-height: min(calc(100vw * 16 / 9), 100svh); }
}
/* Phones: the photo is ~667px tall at 375px wide, and the desktop padding
   (20vh on top) pushed the copy past that, forcing a crop. The top padding
   only has to clear the 77px fixed header. */
@media (max-width: 700px) {
  .hero:not(.hero-page) .hero-inner { --hero-pt: 96px; --hero-pb: 40px; }
}

/* ----- Scroll-scrubbed hero video -----
   The <picture> underneath stays exactly as it was and does double duty as the
   poster: it is what shows before the video can paint, and what stays if the
   video never loads at all, so the hero is never empty.

   .is-ready is added by site.js only once the video has enough data to seek —
   fading in on that flag rather than on load avoids a black rectangle sitting
   over the photo while the file buffers. Nothing here runs under reduced
   motion; site.js leaves the element alone and the photo simply stays. */
.hero-video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity 0.6s var(--ease); pointer-events: none;
}
.hero-video.is-ready { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .hero-video { display: none; } }
/* Homepage hero runs CLEAN — no filter, no scrim (see .hero-overlay below).
   The photograph is shown exactly as shot so it can be judged on its own.
   To re-apply the graded version, restore:
     filter: brightness(1.16) saturate(1.38) contrast(1.03); */
.hero:not(.hero-page) .hero-media img { filter: none; }
/* Overlay density on both heroes is set empirically: sample the rendered photo
   behind each text block, take the 95th-percentile-brightest pixel (worst case
   for white type) and keep it above the WCAG AA floor — 3:1 for the display
   headline, 4.5:1 for the lead and eyebrow. The values below are the lightest
   ones that still clear it, so the photographs read as photographs.

   Inner pages: same scrim-over-wash approach as the homepage below, sized for
   the wider headlines these pages carry. Worst case lands ~4.3:1 on the
   headline and ~5:1 on the smaller copy. */
/* All heroes now run their photograph clean — no scrim on any page. The type
   is held by the layered text-shadows further down instead. Kept as an empty
   positioned element so the markup and stacking order stay intact.

   WARNING: shadows are not credited by the WCAG contrast algorithm, so the
   white hero copy is legible but not formally AA. The previously measured-good
   inner-page scrim, if it is ever wanted back, was:
     radial-gradient(92% 86% at 34% 52%, rgba(5,13,24,.72) 0%, rgba(5,13,24,.44) 50%,
       rgba(5,13,24,.08) 82%, transparent 100%),
     linear-gradient(180deg, rgba(5,13,24,.14) 0%, rgba(5,13,24,0) 34%,
       rgba(5,13,24,0) 70%, rgba(5,13,24,.30) 92%, rgba(5,13,24,.56) 100%);      */
.hero-overlay { position: absolute; inset: 0; z-index: 1; background: none; }
/* Homepage. A full-width wash was the wrong tool here: mapping the pixels that
   actually failed showed the trouble was two small specular hot spots inside
   the headline box — the sun flaring through the trees, and the sunlit roofline
   — while the entire lower half of the frame was being darkened to solve
   nothing. So the cover is a scrim shaped to the copy column instead, and the
   rest of the photograph is left alone. Against the old wash: mean frame
   luminance up ~22%, and every text block *gained* contrast rather than
   trading it away — headline 3.2 -> 5.3, eyebrow 4.9 -> 5.1 worst case.
   Measured with the type hidden outright, so the text-shadows below are not
   credited; weakening this further puts the lead under 4.5 on its own. */
/* Homepage hero: scrim removed entirely, photograph shown clean.

   WARNING — this drops the white hero type below WCAG AA. Something has to go
   back before launch. The measured-good version, tuned against the copy column
   at x 13%->67% / y 21%->70% (eyebrow 8.4:1, headline 5.3:1, lead 5.7:1), was:

     linear-gradient(96deg,
       rgba(5,13,24,0.74) 0%, rgba(5,13,24,0.72) 34%, rgba(5,13,24,0.68) 52%,
       rgba(5,13,24,0.48) 66%, rgba(5,13,24,0.16) 78%, rgba(5,13,24,0) 90%),
     linear-gradient(180deg,
       rgba(5,13,24,0.10) 0%, rgba(5,13,24,0) 26%, rgba(5,13,24,0) 76%,
       rgba(5,13,24,0.13) 92%, rgba(5,13,24,0.30) 100%);                        */
/* RESTORED. The clean-photograph experiment above shipped, and the warning it
   left was accurate: measured on the live pages, white hero type came out at
   3.22:1 on About and 1.54:1 on Services — against a WCAG AA floor of 3.0 for
   large text and 4.5 for body. The Services headline was effectively
   unreadable. These are the measured-good gradients from the note above, put
   back. If the clean look is wanted again, it needs darker photography or dark
   type, not the removal of the cover. */
/* Homepage scrim removed on request (Sept 2026): photo shown exactly as shot.
   The gradient that was here is quoted in the comment above. */
.hero:not(.hero-page) .hero-overlay { background: none; }
/* Interior heroes (services, about) had NO cover at all, which is where the
   1.54:1 came from — a bright, sunlit living-room photograph under white type.
   Weighted to the left, where the copy sits. */
/* Interior hero scrim removed on request (Sept 2026): photos shown as shot. */
.hero-page .hero-overlay { background: none; }
/* Narrow screens carried their own scrims, and those are off too, so the
   photograph is unmodified at every width rather than clean on desktop and
   covered on a phone.

   This is where legibility is thinnest, and worth knowing before any hero photo
   is swapped: in portrait the copy runs the full width and sits over open sky,
   which is precisely the case these gradients were written for — the eyebrow
   measured 1.4:1 there before they existed. If a hero ever needs its cover
   back, start with these.

   @media (max-width: 700px) {
     .hero:not(.hero-page) .hero-overlay {
       background: linear-gradient(180deg,
         rgba(5,13,24,0.10) 0%, rgba(5,13,24,0.18) 18%, rgba(5,13,24,0.66) 32%,
         rgba(5,13,24,0.78) 70%, rgba(5,13,24,0.93) 100%);
     }
     .hero-page .hero-overlay {
       background: linear-gradient(180deg,
         rgba(5,13,24,0.18) 0%, rgba(5,13,24,0.30) 20%,
         rgba(5,13,24,0.62) 42%, rgba(5,13,24,0.90) 100%);
     }
   }                                                                          */
.hero-inner {
  --hero-pt: clamp(120px, 20vh, 200px); --hero-pb: clamp(56px, 9vh, 96px);
  padding-block: var(--hero-pt) var(--hero-pb);
}
/* The headline crosses the sunlit wall of the house; the shadow keeps the
   stroke edges readable there without darkening the whole frame. */
/* Each block carries a little of its own cover so the scrim behind it doesn't
   have to do all the work — a tight shadow for the edge plus a wide soft one
   that sits the type on the photo. Worth most on the small copy, which has the
   stricter 4.5 floor and had no shadow at all before. */
/* 16ch was tuned to the old 5.1rem type; at the smaller size it forces the
   headline into a narrow, tall column, so the measure widens to compensate. */
.hero h1 { color: #fff; max-width: 21ch; text-shadow: 0 1px 2px rgba(5,13,24,0.45), 0 2px 18px rgba(5,13,24,0.50); }
.hero .lead { color: rgba(255,255,255,0.9); max-width: 52ch; margin-top: 1.5rem; text-shadow: 0 1px 2px rgba(5,13,24,0.50), 0 1px 12px rgba(5,13,24,0.45); }
.hero .eyebrow { text-shadow: 0 1px 2px rgba(5,13,24,0.60), 0 1px 9px rgba(5,13,24,0.55); }
/* The homepage hero runs with no scrim at all, so the type has to defend
   itself. The values above assume an overlay underneath and are far too light
   on bare photography — these are roughly double, in three layers: a tight one
   for edge definition, a mid blur to darken the ground immediately around each
   glyph, and a wide soft one to lift the whole block off the picture. Scoped so
   the inner pages, which still carry their calibrated scrim, are untouched. */
.hero h1 {
  text-shadow:
    0 1px 2px rgba(3,10,20,0.62),
    0 3px 14px rgba(3,10,20,0.55),
    0 10px 44px rgba(3,10,20,0.45);
}
.hero .lead {
  color: #fff;
  text-shadow:
    0 1px 2px rgba(3,10,20,0.72),
    0 2px 10px rgba(3,10,20,0.62),
    0 6px 28px rgba(3,10,20,0.48);
}
.hero .eyebrow {
  text-shadow:
    0 1px 2px rgba(3,10,20,0.85),
    0 2px 10px rgba(3,10,20,0.70);
}
/* The rating pill is the weakest thing in the hero — small type on a sunlit
   lawn — so it gets an actual backing rather than only a shadow. */
.hero .rating-badge {
  background: rgba(5,13,24,0.42);
  border: 1px solid rgba(255,255,255,0.16);
  border-radius: 999px;
  padding: 0.42em 0.85em;
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  text-shadow: 0 1px 2px rgba(3,10,20,0.8);
}
.hero-actions { display: flex; flex-wrap: wrap; gap: 0.9rem; margin-top: 2.4rem; }

/* ----- Two-path hero CTAs -----
   Each button carries a label and a sub-label, so the visitor self-selects on
   their situation ("Something's broken") rather than on an action verb ("Book
   online") that means nothing until you know which of them you are. The
   sub-label states what actually happens, so neither path is a mystery click. */
.hero-paths .btn { align-items: center; text-align: left; padding-block: 0.85em; }
.path-label { display: flex; flex-direction: column; line-height: 1.2; }
.path-sub {
  font-weight: 500; font-size: 0.82em; opacity: 0.72; margin-top: 0.18em;
  letter-spacing: 0.005em;
}
/* The urgent path keeps the solid fill: at 2am with no heat it should be the
   loudest thing on the screen. The planned path stays ghosted — it is the
   considered decision and does not need to shout. */
.path-urgent .path-sub { opacity: 0.78; }

/* Service headings lead with the plain term someone actually searched for, and
   keep the original line beneath as a subhead. "Dependable warmth, all winter"
   is a nicer sentence than "Furnace repair" — but a visitor scanning for
   somebody to fix their furnace does not stop on it, and search engines have no
   idea what it refers to. */
.svc-sub {
  display: block; margin-top: 0.35rem;
  font-family: var(--sans); font-weight: 400;
  font-size: clamp(0.95rem, 1.35vw, 1.1rem); line-height: 1.4;
  letter-spacing: 0; color: var(--muted);
}
.band-dark .svc-sub { color: rgba(255, 255, 255, 0.72); }

/* ----- Hero quick capture -----
   Deliberately plain against the photograph: a white card would compete with
   the headline, and this is the second thing read, not the first. */
.hero-quick {
  margin-top: 1.2rem; max-width: 460px;
  background: rgba(8, 21, 37, 0.72);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius); padding: 1.15rem 1.15rem 1rem;
  box-shadow: 0 24px 50px -24px rgba(4, 12, 24, 0.7);
}
.hero-quick[hidden] { display: none; }
.quick-title { font-family: var(--serif); font-size: 1.05rem; color: #fff; margin-bottom: 0.7rem; }
.quick-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
.quick-form input, .quick-form select {
  width: 100%; padding: 0.7em 0.85em; border-radius: 10px;
  /* 16px exactly — anything smaller and iOS zooms the page on focus and never
     zooms back, which on a hero form is the worst possible moment for it. */
  font-size: 16px; font-family: var(--sans);
  background: rgba(255, 255, 255, 0.94); border: 1.5px solid transparent; color: var(--ink);
}
.quick-form select { margin-top: 0.6rem; }
.quick-form input:focus, .quick-form select:focus { outline: none; border-color: var(--azure); }
.quick-submit { width: 100%; margin-top: 0.7rem; padding: 0.85em 1.4em; }
.quick-note { margin-top: 0.6rem; font-size: 0.78rem; color: rgba(255, 255, 255, 0.72); line-height: 1.45; }
.quick-note a { color: #fff; text-decoration: underline; text-underline-offset: 2px; }
.quick-done { text-align: left; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ----- After-hours notice -----
   Revealed by site.js only outside business hours. Sits directly under the
   paths so it reframes them rather than appearing as unrelated chrome. */
.hero-afterhours {
  display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap;
  margin-top: 1.1rem; font-size: 0.92rem; color: rgba(255, 255, 255, 0.82);
  text-shadow: 0 1px 3px rgba(5, 13, 24, 0.6);
}
.hero-afterhours[hidden] { display: none; }
.hero-afterhours a { color: #fff; font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
.hero-afterhours a:hover { color: var(--azure); }
.ah-dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--brass);
  box-shadow: 0 0 0 3px rgba(201, 169, 106, 0.22); flex: none;
}
.hero-page .hero-inner { --hero-pt: clamp(150px, 24vh, 230px); --hero-pb: clamp(48px, 8vh, 84px); }

/* ----- Subtle dark layer under the hero text (all pages) -----
   Not a scrim over the photo: a soft, feathered shade sized to the text block
   alone, so the rest of the picture stays exactly as shot. It covers the copy
   box (the padding is shared through --hero-pt / --hero-pb), then scales out
   so the fade to transparent happens well beyond the text edges and there is
   no visible shape. Sits inside .hero-inner's stacking context, so it paints
   under the type and moves with it. Tune strength with the alpha stops. */
.hero-inner::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  top: var(--hero-pt); bottom: var(--hero-pb);
  left: var(--gutter); width: min(calc(100% - 2 * var(--gutter)), 44rem);
  background: radial-gradient(closest-side,
    rgba(5,13,24,0.34) 0%, rgba(5,13,24,0.28) 45%,
    rgba(5,13,24,0.12) 78%, rgba(5,13,24,0) 100%);
  transform: scale(1.9, 1.7);
}
@media (max-width: 700px) {
  .hero-inner::before { transform: scale(1.35, 1.4); }
}
.hero-page h1 { max-width: 20ch; }

/* ----- Services hero: the whole photograph, never cropped ----- */
/* services-hero.jpg is 1920x1080. Giving the hero box that same 16:9 ratio
   shows the frame edge to edge with nothing cut off — and no letterbox bars
   either, because box and photo are the same shape. object-fit: contain is the
   guarantee: if the ratios ever drift (a replacement photo, a stray min-height)
   the photo letterboxes rather than silently going back to cropping.
   The ratio holds at EVERY width, because the copy has to stay on the photo —
   see the type scaling below. */
/* The navy goes on .hero-media, NOT on .hero. .hero-media is z-index:-2 and
   .hero creates no stacking context, so a background on .hero paints straight
   over the photo and the hero goes blank. On .hero-media it sits behind the
   img (its own child) and fills whatever the contained photo doesn't cover. */
/* Centred, not bottom-aligned. The base .hero pins its copy to the bottom,
   which reads right on the 100svh homepage hero but sits far too low inside a
   fixed 16:9 frame. The padding is deliberately symmetric: with align-items
   centring the item, equal padding is what puts the TEXT on the frame's centre
   line — asymmetric values re-introduce the droop by shifting the copy inside
   its own box. Below 900px it goes back to flex-end, where the frame is short
   enough that bottom alignment is what keeps the copy clear of the header. */
.hero.hero-fit { aspect-ratio: 16 / 9; min-height: 0; align-items: center; }
.hero-fit .hero-media { background: var(--navy-900); }
.hero-fit .hero-media img { object-fit: contain; object-position: center top; }
.hero-fit .hero-inner { --hero-pt: clamp(40px, 6vw, 96px); --hero-pb: clamp(40px, 6vw, 96px); }

/* The copy must stay within the photograph, and the frame is 16:9 of the
   viewport width — so its height is a fixed fraction of the width, and the type
   has to be too. Everything below is sized in vw: the copy then occupies the
   same proportion of the frame at 320px as it does at 900px, which is why it
   cannot outgrow the photo at any width in between. Height-based units (vh,
   svh) would break that — a short landscape phone would overflow the frame.
   The px ceilings only bite as the query approaches 900px, where the frame is
   roomy; the px floors are legibility limits, and 320px is where they engage. */
/* The tightest case is 320px: the frame is only 180px tall there and the fixed
   header covers the top ~64px of it, so the copy has ~104px to live in. These
   sizes are set from that budget — the block clears the header at 320 and has
   room to spare by 390. */
@media (max-width: 900px) {
  .hero.hero-fit { align-items: flex-end; }
  .hero-fit .hero-inner { --hero-pt: 0px; --hero-pb: clamp(12px, 3.6vw, 44px); }
  .hero-fit .eyebrow { font-size: clamp(8px, 2.1vw, 15px); }
  .hero-fit h1 { font-size: clamp(17px, 5.2vw, 44px); line-height: 1.06; max-width: none; margin-top: 1.4vw; }
  .hero-fit .lead { font-size: clamp(10px, 3vw, 22px); line-height: 1.32; margin-top: 1.5vw; max-width: none; }
}
/* Under 360px the sums stop working: 180px of frame, ~70px of it behind the
   fixed header, leaves too little for eyebrow + headline + paragraph without
   dropping the copy below a legible size. The eyebrow goes — it's a decorative
   label the <h1> already implies, and losing it buys the headline the clearance
   it needs to sit below the logo rather than across it. */
@media (max-width: 359px) {
  .hero-fit .eyebrow { display: none; }
}

.scroll-cue {
  position: absolute; left: 50%; bottom: 26px; transform: translateX(-50%);
  color: rgba(255,255,255,0.7); font-size: 0.72rem; letter-spacing: 0.2em; text-transform: uppercase;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.scroll-cue .line { width: 1px; height: 34px; background: linear-gradient(rgba(255,255,255,0.7), transparent); animation: cue 2.2s var(--ease) infinite; transform-origin: top; }
@keyframes cue { 0%,100% { transform: scaleY(0.4); opacity: 0.4; } 50% { transform: scaleY(1); opacity: 1; } }

/* ==========================================================================
   Sections — light + dark bands
   ========================================================================== */
.band-dark { background: var(--navy-850); color: #fff; }
.band-dark h2, .band-dark h3, .band-dark h4 { color: #fff; }
.band-dark p { color: rgba(255,255,255,0.72); }
.band-dark .lead { color: rgba(255,255,255,0.9); }
.band-mist { background: var(--mist); }

/* Intro statement */
.intro-grid { display: grid; grid-template-columns: 1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
@media (min-width: 860px) { .intro-grid { grid-template-columns: 0.85fr 1.15fr; align-items: center; } }
.intro-statement { font-family: var(--serif); font-size: clamp(1.5rem, 3vw, 2.15rem); line-height: 1.28; letter-spacing: -0.02em; color: var(--ink); }
.intro-statement em { font-style: italic; color: var(--azure-600); }

/* Section head */
.section-head { max-width: 60ch; }
.section-head.center { margin-inline: auto; }
.section-head .eyebrow { margin-bottom: 1.1rem; }
.section-head p { margin-top: 1rem; }

/* ----- Services grid ----- */
/* grid-auto-rows: 1fr equalises the rows to each other. Cards within a row
   already match via the default stretch; what threw the grid off was a
   two-line <h3> ("System Installation & Replacement") making its whole row
   ~24px taller than the one above it at some widths. */
.services-grid { display: grid; grid-template-columns: 1fr; grid-auto-rows: 1fr; gap: 1.1rem; margin-top: clamp(2.4rem, 5vw, 3.6rem); }
@media (min-width: 620px) { .services-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 980px) { .services-grid { grid-template-columns: repeat(3, 1fr); } }

.service-card {
  position: relative; padding: 2rem 1.9rem 2.1rem; border-radius: var(--radius);
  background: var(--paper); border: 1px solid var(--line);
  transition: transform 0.4s var(--ease), box-shadow 0.4s var(--ease), border-color 0.4s var(--ease);
  overflow: hidden;
}
.service-card::after {
  content: ""; position: absolute; left: 0; top: 0; height: 3px; width: 0;
  background: linear-gradient(90deg, var(--azure), var(--azure-600)); transition: width 0.45s var(--ease);
}
.service-card:hover { transform: translateY(-6px); box-shadow: var(--shadow); border-color: transparent; }
.service-card:hover::after { width: 100%; }
/* Stretched link: the anchor sits on the heading so it keeps a meaningful
   accessible name, while its ::after covers the whole card so the entire
   surface is the click target. Focus lands on that same overlay, so keyboard
   users get an outline around the card rather than round three words of text. */
.service-card .card-link { color: inherit; text-decoration: none; }
.service-card .card-link::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.service-card .card-link:focus-visible { outline: none; }
.service-card .card-link:focus-visible::after { outline: 2px solid var(--azure); outline-offset: -4px; border-radius: calc(var(--radius) - 4px); }
.service-icon {
  width: 54px; height: 54px; border-radius: 14px; display: grid; place-items: center;
  background: linear-gradient(150deg, rgba(77,163,255,0.16), rgba(45,134,230,0.07));
  color: var(--azure-600); margin-bottom: 1.4rem;
}
.service-icon svg { width: 27px; height: 27px; }
.service-card h3 { margin-bottom: 0.55rem; }
.service-card p { font-size: 0.97rem; }
.band-dark .service-card { background: var(--navy-800); border-color: var(--line-dark); }
.band-dark .service-icon { background: rgba(77,163,255,0.14); color: var(--azure); }

/* Featured service card with photo */

/* ----- Split feature (why / image + text) ----- */
.split { display: grid; grid-template-columns: 1fr; gap: clamp(2rem, 5vw, 4.5rem); align-items: center; }
@media (min-width: 900px) { .split { grid-template-columns: 1fr 1fr; } .split.reverse .split-media { order: 2; } }
.split-media { position: relative; border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow); aspect-ratio: 4 / 5; }
.split-media img { width: 100%; height: 100%; object-fit: cover; }
.split-media.wide { aspect-ratio: 5 / 4; }
.split-media .badge {
  position: absolute; left: 20px; bottom: 20px; right: 20px;
  background: rgba(8,21,37,0.62); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  border: 1px solid var(--line-dark); border-radius: 14px; padding: 1rem 1.2rem; color: #fff;
}
.split-media .badge strong { display: block; font-family: var(--serif); font-size: 1.05rem; letter-spacing: -0.01em; }
.split-media .badge span { font-size: 0.85rem; color: rgba(255,255,255,0.75); }

/* Differentiator list */
.diff-list { list-style: none; display: grid; gap: 1.4rem; margin-top: 2rem; }
.diff-item { display: grid; grid-template-columns: auto 1fr; gap: 1rem; align-items: start; }
.diff-item .tick {
  width: 40px; height: 40px; border-radius: 11px; flex: none; display: grid; place-items: center;
  background: rgba(77,163,255,0.12); color: var(--azure-600);
}
.band-dark .diff-item .tick { background: rgba(77,163,255,0.16); color: var(--azure); }
.diff-item .tick svg { width: 20px; height: 20px; }
.diff-item h4 { font-family: var(--serif); font-size: 1.1rem; margin-bottom: 0.2rem; }
.diff-item p { font-size: 0.96rem; margin: 0; }

/* ----- Process ----- */
.process {
  --process-gap: clamp(1.6rem, 3vw, 2.4rem);
  display: grid; grid-template-columns: 1fr; gap: var(--process-gap);
  margin-top: clamp(2.4rem, 5vw, 3.6rem); counter-reset: step;
}
@media (min-width: 720px)  { .process { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .process { grid-template-columns: repeat(4, 1fr); } }

/* Each step hangs off a shared top baseline: number, rule, heading, copy. */
.step { position: relative; padding-top: 3.5rem; }
.step::before {
  counter-increment: step; content: counter(step, decimal-leading-zero);
  position: absolute; top: 0; left: 0;
  font-family: var(--serif); font-size: 2.4rem; line-height: 1; letter-spacing: -0.02em;
  color: var(--azure); opacity: 0.55;
}
/* Connector runs from the number across the grid gap into the next step. */
.step::after {
  content: ""; display: none; position: absolute;
  top: 1.1rem; left: 3.1rem; right: calc(-1 * var(--process-gap));
  height: 1px; background: var(--line);
}
@media (min-width: 720px)  { .step:nth-child(odd)::after { display: block; } }
@media (min-width: 1040px) {
  .step::after { display: block; }
  .step:last-child::after { display: none; }
}
.step h3 { font-size: 1.2rem; margin-bottom: 0.5rem; }
.step p { font-size: 0.95rem; margin: 0; }

/* ----- CTA band ----- */
.cta-band { position: relative; overflow: hidden; color: #fff; text-align: center; }
.cta-band .cta-media { position: absolute; inset: 0; z-index: -2; }
.cta-band .cta-media img { width: 100%; height: 100%; object-fit: cover; }
.cta-band .cta-overlay { position: absolute; inset: 0; z-index: -1; background: linear-gradient(180deg, rgba(8,21,37,0.9), rgba(5,13,24,0.94)); }
.cta-band h2 { color: #fff; max-width: 20ch; margin-inline: auto; }
.cta-band p { color: rgba(255,255,255,0.82); margin-top: 1rem; }
.cta-actions { display: flex; flex-wrap: wrap; gap: 0.9rem; justify-content: center; margin-top: 2.2rem; }

/* ----- Closing CTA carrying the full booking form inline ----- */
.cta-form { text-align: left; }
.cta-grid { display: grid; grid-template-columns: 1fr; gap: clamp(2.2rem, 4vw, 3.8rem); align-items: center; }
@media (min-width: 980px) { .cta-grid { grid-template-columns: 1fr 1fr; } }
.cta-form h2 { max-width: 17ch; margin-inline: 0; }
.cta-form .cta-actions { justify-content: flex-start; margin-top: 1.9rem; }
.cta-points { list-style: none; margin-top: 1.7rem; display: grid; gap: 0.75rem; }
.cta-points li { display: flex; gap: 0.65em; align-items: flex-start; color: rgba(255,255,255,0.87); font-size: 0.97rem; }
.cta-points svg { width: 1.05em; height: 1.05em; color: var(--azure); flex: none; margin-top: 0.25em; }
/* The band sets color:#fff and centres its text for the old copy-only layout;
   inside the card both would be wrong — white body copy on a white surface. */
.cta-form .form-card { text-align: left; color: var(--ink); }
.cta-form .form-card p { color: var(--muted); margin-top: 0; }
.cta-form .form-card h2, .cta-form .form-card h3 { color: var(--ink); margin-inline: 0; }
.cta-form .form-card .form-confirm { text-align: center; }
.cta-form .form-card .form-confirm h3 { margin-inline: auto; }
/* Compact pass — the inline card carries all eight fields beside the copy
   column, so it has to fit a viewport rather than own a page of its own.
   Nothing is dropped: spacing, control height and type size are trimmed, and
   the two selects are paired into a row in the markup. Scoped to .cta-form so
   a restored contact page keeps the roomier original. */
.cta-form .form-card { padding: clamp(1.25rem, 2.4vw, 1.85rem); }
.cta-form .field { margin-bottom: 0.7rem; }
.cta-form .field label { font-size: 0.78rem; margin-bottom: 0.28rem; }
.cta-form .field input,
.cta-form .field select,
.cta-form .field textarea { padding: 0.6em 0.85em; border-radius: 10px; font-size: 0.94rem; }
.cta-form .field textarea { min-height: 68px; }
.cta-form .field-row { gap: 0 0.75rem; }
.cta-form .field-hint { font-size: 0.73rem; margin: 0.15rem 0 0.65rem; }
.cta-form .form-submit { margin-top: 0.35rem; }
.cta-form .form-submit .btn { padding: 0.78em 1.6em; font-size: 0.98rem; }
.cta-form .form-note { font-size: 0.74rem; line-height: 1.45; }
/* Photo-forward: the old 0.90->0.94 wash was sized for text over the whole
   frame. Here the card carries its own contrast, so the scrim only needs to
   protect the copy column on the left and can let the dusk shot through. */
.cta-form .cta-overlay {
  background:
    linear-gradient(100deg, rgba(5,13,24,0.90) 0%, rgba(5,13,24,0.78) 38%, rgba(5,13,24,0.52) 62%, rgba(5,13,24,0.42) 100%),
    linear-gradient(180deg, rgba(5,13,24,0.30) 0%, rgba(5,13,24,0.10) 40%, rgba(5,13,24,0.45) 100%);
}

/* ==========================================================================
   Footer
   ========================================================================== */
.site-footer { background: var(--navy-950); color: rgba(255,255,255,0.7); padding-block: clamp(56px, 8vw, 84px) 2rem; }
.footer-top { display: grid; grid-template-columns: 1fr; gap: 2.6rem; padding-bottom: 3rem; border-bottom: 1px solid var(--line-dark); }
@media (min-width: 760px) { .footer-top { grid-template-columns: 1.5fr 1fr 1fr 1.2fr; gap: 2rem; } }
.footer-brand .logo { height: 30px; margin-bottom: 1.1rem; }
/* The global `p { color: var(--muted) }` was landing #566579 on near-black
   navy — 3.28:1. Footer copy needs its own light value. */
.footer-brand p { font-size: 0.95rem; max-width: 34ch; color: rgba(255,255,255,0.75); }
.footer-col h4 { font-family: var(--sans); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: rgba(255,255,255,0.5); margin-bottom: 1.1rem; }
.footer-col ul { list-style: none; display: grid; }
.footer-col a, .footer-col span { color: rgba(255,255,255,0.72); font-size: 0.95rem; transition: color 0.25s var(--ease); }
.footer-col a:hover { color: #fff; }
/* Tap targets. These were inline boxes 18px tall separated by 19px of dead
   grid gap — a thumb landing between two of them hit nothing. Turning the row
   gap into padding on the anchor itself makes the whole strip tappable and
   leaves the visual rhythm where it was: the pitch is the same, the gap is
   just inside the link now instead of beside it. */
.footer-col li a { display: block; padding: 0.5rem 0; }
.footer-bottom { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; align-items: center; padding-top: 1.8rem; font-size: 0.82rem; color: rgba(255,255,255,0.5); }
.footer-bottom .legal { display: flex; gap: 1.4rem; flex-wrap: wrap; }

/* ==========================================================================
   Services page detail sections
   ========================================================================== */
.svc-detail { display: grid; grid-template-columns: 1fr; gap: clamp(2rem, 5vw, 4rem); align-items: center; }
@media (min-width: 900px) { .svc-detail { grid-template-columns: 1fr 1fr; } .svc-detail.reverse .svc-media { order: 2; } }
.svc-media { border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow); aspect-ratio: 4/3; }
.svc-media img { width: 100%; height: 100%; object-fit: cover; }
.svc-body .num { font-family: var(--serif); font-size: 0.95rem; color: var(--azure-600); letter-spacing: 0.05em; margin-bottom: 0.6rem; }
.includes { list-style: none; display: grid; gap: 0.75rem; margin-top: 1.6rem; }
.includes li { display: grid; grid-template-columns: auto 1fr; gap: 0.7rem; align-items: start; font-size: 0.98rem; color: var(--ink); }
.includes li svg { width: 20px; height: 20px; color: var(--azure-600); flex: none; margin-top: 2px; }
.band-dark .includes li { color: rgba(255,255,255,0.86); }
.band-dark .includes li svg { color: var(--azure); }
.band-dark .svc-body .num { color: var(--azure); }

/* Service benefit chips */
.benefits { display: grid; grid-template-columns: 1fr; gap: 0.9rem; margin-top: 1.8rem; }
@media (min-width: 560px) { .benefits { grid-template-columns: 1fr 1fr; } }
.benefit {
  display: grid; grid-template-columns: auto 1fr; gap: 0.8rem; align-items: start;
  padding: 1.1rem 1.2rem; border-radius: 12px; background: rgba(255,255,255,0.05); border: 1px solid var(--line-dark);
}
.benefit svg { width: 22px; height: 22px; color: var(--azure); flex: none; }
.benefit strong { display: block; font-family: var(--serif); font-size: 1.02rem; color: #fff; margin-bottom: 0.15rem; }
.benefit span { font-size: 0.9rem; color: rgba(255,255,255,0.7); }

/* ==========================================================================
   About page
   ========================================================================== */
.values-grid { display: grid; grid-template-columns: 1fr; gap: 1.1rem; margin-top: clamp(2.2rem, 4vw, 3.2rem); }
@media (min-width: 680px) { .values-grid { grid-template-columns: repeat(3, 1fr); } }
.value {
  padding: 1.9rem 1.7rem; border-radius: var(--radius); background: var(--paper); border: 1px solid var(--line);
  transition: transform 0.4s var(--ease), box-shadow 0.4s var(--ease);
}
.value:hover { transform: translateY(-5px); box-shadow: var(--shadow-sm); }
.value .service-icon { margin-bottom: 1.1rem; }
.value h3 { font-size: 1.2rem; margin-bottom: 0.4rem; }
.value p { font-size: 0.96rem; }

.fact-row { display: grid; grid-template-columns: 1fr; gap: 1.1rem; margin-top: 2.4rem; }
@media (min-width: 620px) { .fact-row { grid-template-columns: repeat(3, 1fr); } }
.fact { padding: 1.6rem 1.5rem; border-radius: var(--radius); border: 1px solid var(--line-dark); background: rgba(255,255,255,0.04); }
.fact .k { font-family: var(--serif); font-size: 1.5rem; color: #fff; letter-spacing: -0.01em; margin-bottom: 0.25rem; }
.fact .v { font-size: 0.9rem; color: rgba(255,255,255,0.68); }

/* Technicians */
.team-grid { display: grid; grid-template-columns: 1fr; gap: 1.2rem; margin-top: clamp(2.2rem, 4vw, 3rem); }
@media (min-width: 700px) { .team-grid { grid-template-columns: 1fr 1fr; } }
.team-card { position: relative; border-radius: var(--radius-lg); overflow: hidden; aspect-ratio: 4/3; box-shadow: var(--shadow); }
.team-card img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.8s var(--ease); }
.team-card:hover img { transform: scale(1.05); }
/* Deeper, taller scrim: against the bright window light in the summer and
   air-quality shots the single-stop version let captions fall to ~1.9:1. */
.team-card figcaption { position: absolute; left: 0; right: 0; bottom: 0; padding: 1.5rem 1.4rem 1.3rem; color: #fff; background: linear-gradient(0deg, rgba(5,13,24,0.95) 0%, rgba(5,13,24,0.78) 38%, rgba(5,13,24,0.32) 72%, transparent 100%); }
/* Shadow as well as the scrim: these photos have blown-out windows directly
   behind the caption, where the gradient alone still left ~2.2:1. */
.team-card figcaption strong { display: block; font-family: var(--serif); font-size: 1.15rem; text-shadow: 0 1px 3px rgba(5,13,24,0.85); }
.team-card figcaption span { font-size: 0.9rem; color: rgba(255,255,255,0.78); }

/* ==========================================================================
   Contact page
   ========================================================================== */
.form-card { background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: clamp(1.6rem, 4vw, 2.6rem); box-shadow: var(--shadow-sm); }
.field { margin-bottom: 1.2rem; }
.field label { display: block; font-size: 0.85rem; font-weight: 600; color: var(--ink); margin-bottom: 0.5rem; letter-spacing: 0.01em; }
.field .req { color: var(--azure-600); }
.field input, .field select, .field textarea {
  width: 100%; padding: 0.9em 1em; border-radius: 12px; border: 1.5px solid var(--line);
  background: var(--mist); color: var(--ink); transition: border-color 0.25s var(--ease), background 0.25s var(--ease), box-shadow 0.25s var(--ease);
  font-size: 1rem;
}
.field textarea { resize: vertical; min-height: 120px; }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--azure); background: #fff; box-shadow: 0 0 0 4px rgba(77,163,255,0.14);
}
.field-row { display: grid; grid-template-columns: 1fr; gap: 0 1rem; }
@media (min-width: 540px) { .field-row { grid-template-columns: 1fr 1fr; } }
.form-note { font-size: 0.82rem; color: var(--muted); margin-top: 0.4rem; }
.form-submit { margin-top: 0.6rem; }
.form-submit .btn { width: 100%; }

/* Confirmation */
.form-confirm { display: none; text-align: center; padding: 1rem 0.5rem; }
.form-card.confirmed .form-confirm { display: block; animation: rise 0.5s var(--ease) both; }
.form-card.confirmed .form-body { display: none; }
.confirm-icon { width: 72px; height: 72px; border-radius: 50%; margin: 0 auto 1.4rem; display: grid; place-items: center; background: rgba(77,163,255,0.14); color: var(--azure-600); }
.confirm-icon svg { width: 36px; height: 36px; }
.form-confirm h3 { margin-bottom: 0.7rem; }
.form-confirm .confirm-contact { display: flex; flex-wrap: wrap; gap: 0.7rem; justify-content: center; margin-top: 1.6rem; }

/* ==========================================================================
   Reveal animations
   ========================================================================== */
.reveal { opacity: 0; transform: translateY(26px); transition: opacity 0.8s var(--ease), transform 0.8s var(--ease); }
.reveal.in { opacity: 1; transform: none; }
.reveal.d1 { transition-delay: 0.08s; }
.reveal.d2 { transition-delay: 0.16s; }
.reveal.d3 { transition-delay: 0.24s; }
.reveal.d4 { transition-delay: 0.32s; }
@keyframes rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }

/* ---- GSAP-driven motion ----
   <html class="gsap"> is set by site.js only once GSAP and ScrollTrigger have
   both actually loaded. Everything below is scoped to it, so if the libraries
   are missing the CSS-transition reveal above stays in charge and nothing is
   left stranded at opacity 0. */
html.gsap .reveal { transition: none; }

/* ---- Hero headline type-on ----
   site.js splits the headline into per-word / per-character spans only once
   GSAP is confirmed running, so the raw markup always ships the full sentence
   for crawlers and for the no-JS case. Characters are hidden with opacity, not
   removed, so the line breaks are settled before the first one appears and the
   headline never reflows mid-animation. Screen readers still get the whole
   string, since transparent text is read normally. */
html.gsap .hero .tw { display: inline-block; }
html.gsap .hero .tc { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  html.gsap .hero .tc { opacity: 1; }
}
/* Connector is drawn by tweening --draw on the parent .step; scaleX needs a
   left origin so it grows toward the next step rather than out of its centre. */
html.gsap .step::after { transform: scaleX(var(--draw, 1)); transform-origin: 0 50%; }

.scroll-progress {
  position: fixed; top: 0; left: 0; right: 0; height: 2px; z-index: 200;
  background: linear-gradient(90deg, var(--azure), var(--brass));
  transform: scaleX(0); transform-origin: 0 50%;
  pointer-events: none;
}

/* ==========================================================================
   Responsive — mobile nav
   ========================================================================== */
@media (max-width: 900px) {
  .nav-toggle { display: inline-flex; }
  .nav-links {
    position: fixed; inset: 0 0 auto auto; top: 0; height: 100svh; width: min(84vw, 360px);
    flex-direction: column; align-items: flex-start; justify-content: flex-start;
    background: var(--navy-900); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
    padding: 104px var(--gutter) 2rem; gap: 1.5rem;
    transform: translateX(105%); transition: transform 0.45s var(--ease);
    box-shadow: -30px 0 60px -30px rgba(0,0,0,0.6); border-left: 1px solid var(--line-dark);
    z-index: 110; overflow-y: auto;
  }
  .nav-links a { font-size: 1.3rem; font-weight: 600; }
  .nav-cta {
    position: fixed; inset: auto 0 0 auto; width: min(84vw, 360px);
    flex-direction: column; align-items: stretch; gap: 0.9rem;
    padding: 0 var(--gutter) 2.2rem; background: var(--navy-900);
    transform: translateX(105%); transition: transform 0.45s var(--ease);
    z-index: 111; border-left: 1px solid var(--line-dark);
  }
  .nav-cta .nav-phone { justify-content: flex-start; font-size: 1.05rem; padding: 0.4rem 0; }
  .nav-cta .btn-primary { width: 100%; padding: 0.9em 1.35em; font-size: 1rem; }
  .nav-open .nav-links, .nav-open .nav-cta { transform: translateX(0); }
  .nav-scrim { position: fixed; inset: 0; background: rgba(5,13,24,0.5); opacity: 0; pointer-events: none; transition: opacity 0.4s var(--ease); z-index: 105; }
  .nav-open .nav-scrim { opacity: 1; pointer-events: auto; }
}
@media (min-width: 901px) { .nav-scrim { display: none; } }

@media (max-width: 380px) {
  :root { --gutter: 18px; }
}

/* ==========================================================================
   Enhancements — added sections (guarantees, SLA, brands, area, reviews, FAQ,
   credentials, legal, mobile call bar). All reuse the existing tokens.
   ========================================================================== */

/* ----- The 777 Standard — guarantee / promise cards ----- */
.promise-grid { display: grid; grid-template-columns: 1fr; gap: 1.1rem; margin-top: clamp(2.2rem, 4vw, 3.4rem); }
@media (min-width: 560px) { .promise-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 980px) { .promise-grid { grid-template-columns: repeat(4, 1fr); } }
.promise {
  padding: 1.9rem 1.6rem 2rem; border-radius: var(--radius);
  background: var(--paper); border: 1px solid var(--line);
  transition: transform 0.4s var(--ease), box-shadow 0.4s var(--ease), border-color 0.4s var(--ease);
}
.promise:hover { transform: translateY(-5px); box-shadow: var(--shadow-sm); border-color: transparent; }
.promise .service-icon { margin-bottom: 1.1rem; }
.promise h3 { font-size: 1.14rem; margin-bottom: 0.45rem; }
.promise p { font-size: 0.93rem; }
.promise .fine { display: block; margin-top: 0.6rem; font-size: 0.8rem; font-weight: 600; color: var(--azure-600); letter-spacing: 0.01em; }
.band-dark .promise { background: var(--navy-800); border-color: var(--line-dark); }
.band-dark .promise .fine { color: var(--azure); }

/* ----- Credentials / trust chips (placeholder-aware) ----- */
.cred-chips { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-top: clamp(1.8rem, 3vw, 2.4rem); }
.cred-chips.center { justify-content: center; }
.cred-chip {
  display: inline-flex; align-items: center; gap: 0.5em;
  padding: 0.6em 1.05em; border-radius: 999px; font-size: 0.85rem; font-weight: 600;
  border: 1px solid var(--line); background: var(--mist); color: var(--ink);
}
.cred-chip svg { width: 1.05em; height: 1.05em; color: var(--azure-600); flex: none; }
.cred-chip.placeholder { border-style: dashed; color: var(--muted); background: transparent; }
.band-dark .cred-chip { background: rgba(255,255,255,0.05); border-color: var(--line-dark); color: rgba(255,255,255,0.88); }
.band-dark .cred-chip svg { color: var(--azure); }
.band-dark .cred-chip.placeholder { background: transparent; color: rgba(255,255,255,0.62); }

/* ----- Brands we service strip ----- */
.brands .section-head { margin-inline: auto; }
/* Fixed 4-up grid rather than a wrapping flex row: with eight names the row
   put seven on one line and orphaned "Bryant" alone underneath. */
.brands-row {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: clamp(0.7rem, 1.4vw, 1rem);
  margin-top: clamp(1.8rem, 3vw, 2.6rem);
}
@media (max-width: 700px) { .brands-row { grid-template-columns: repeat(2, 1fr); } }
/* Uniform plated wordmarks. Every tile is the same box and every name is set
   in the same weight, tracking and colour, so the row reads as one designed
   system — which is exactly what authentic manufacturer logos could never do,
   since each is a different shape, typeface and colour by design. Monochrome
   also keeps this clearly a compatibility list rather than an endorsement,
   which is what the note underneath says. */
.brand-name {
  display: grid; place-items: center; text-align: center;
  min-height: 78px; padding: 0.85rem 0.9rem;
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--paper);
  font-family: var(--sans);
  font-size: clamp(0.7rem, 1.05vw, 0.85rem);
  font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink); opacity: 0.5;
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease),
              box-shadow 0.3s var(--ease), border-color 0.3s var(--ease);
}
.brand-name:hover {
  opacity: 1; transform: translateY(-3px);
  border-color: transparent; box-shadow: var(--shadow-sm);
}
.brands-note { margin-top: 1.6rem; font-size: 0.82rem; color: var(--muted); max-width: 60ch; margin-inline: auto; }

/* ----- Service area ----- */
.area-grid { display: grid; grid-template-columns: 1fr; gap: clamp(2rem, 5vw, 4rem); align-items: center; }
@media (min-width: 880px) { .area-grid { grid-template-columns: 1fr 1fr; } }
.area-map {
  position: relative; border-radius: var(--radius-lg); overflow: hidden;
  border: 1px solid var(--line); background: linear-gradient(160deg, var(--navy-850), var(--navy-900)); aspect-ratio: 4 / 3;
  box-shadow: var(--shadow-sm);
}
/* Real satellite imagery, graded toward the navy palette. The gradient sits
   above the photo but below the SVG, so it darkens the map without dulling
   the markers and labels drawn on top of it. */
.area-map picture { position: absolute; inset: 0; }
.area-map img { width: 100%; height: 100%; object-fit: cover; filter: saturate(0.8) contrast(1.06) brightness(0.9); }
.area-map::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(8,21,37,0.45) 0%, rgba(8,21,37,0.10) 40%, rgba(8,21,37,0.58) 100%);
}
.area-map svg { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; }
.area-map .map-tag {
  position: absolute; left: 16px; bottom: 16px; z-index: 2; padding: 0.5em 0.9em; border-radius: 999px;
  background: rgba(8,21,37,0.62); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  border: 1px solid var(--line-dark); color: #fff; font-size: 0.78rem; font-weight: 600; letter-spacing: 0.02em;
}
.area-map .map-credit {
  position: absolute; right: 12px; bottom: 10px; z-index: 2;
  color: rgba(255,255,255,0.62); font-size: 0.62rem; letter-spacing: 0.01em;
  text-shadow: 0 1px 3px rgba(4,12,24,0.9);
}
.hood-list { columns: 2; column-gap: 1.6rem; list-style: none; margin-top: 1.6rem; }
.hood-list li { break-inside: avoid; padding: 0.35rem 0; display: flex; gap: 0.5em; align-items: center; font-size: 0.95rem; color: var(--ink); }
.hood-list li svg { width: 0.9em; height: 0.9em; color: var(--azure-600); flex: none; }
.area-fill { font-size: 0.82rem; color: var(--muted); margin-top: 1.4rem; }

/* ----- Reviews (real, verified customer reviews) ----- */
.rating-summary { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.5rem 0.9rem; margin-top: 1.4rem; }
.rating-summary .rs-stars { display: inline-flex; gap: 2px; color: var(--brass); }
.rating-summary .rs-stars svg { width: 1.3em; height: 1.3em; }
.rating-summary .rs-score { font-family: var(--serif); font-size: 1.5rem; font-weight: 700; color: var(--ink); line-height: 1; }
.rating-summary .rs-label { font-size: 0.9rem; color: var(--muted); font-weight: 600; }

.review-feature {
  max-width: 860px; margin: clamp(2rem, 4vw, 3rem) auto 0; text-align: center;
  padding: clamp(1.8rem, 4vw, 2.6rem); border-radius: var(--radius-lg);
  background: var(--mist); border: 1px solid var(--line);
}
.review-feature .review-stars { justify-content: center; margin-bottom: 1rem; }
.review-feature p { font-family: var(--serif); font-size: clamp(1.15rem, 2.2vw, 1.5rem); line-height: 1.4; letter-spacing: -0.01em; color: var(--ink); }
.review-feature cite { display: inline-flex; align-items: center; gap: 0.6rem; margin-top: 1.4rem; font-style: normal; }

.reviews-grid { display: grid; grid-template-columns: 1fr; gap: 1.1rem; margin-top: clamp(1.6rem, 3vw, 2.2rem); }
@media (min-width: 700px) { .reviews-grid { grid-template-columns: repeat(3, 1fr); } }
.review-card {
  padding: 1.7rem 1.6rem; border-radius: var(--radius); border: 1px solid var(--line);
  background: var(--paper); display: flex; flex-direction: column; gap: 0.9rem; box-shadow: var(--shadow-sm);
}
.review-stars { display: flex; gap: 0.15rem; color: var(--brass); }
.review-stars svg { width: 1.15em; height: 1.15em; }
.review-text { font-size: 0.97rem; color: var(--ink); margin: 0; }
.review-meta { margin-top: auto; display: flex; align-items: center; gap: 0.7rem; padding-top: 0.3rem; }
.review-meta .avatar, .review-feature cite .avatar { width: 38px; height: 38px; border-radius: 50%; background: linear-gradient(150deg, var(--azure), var(--azure-600)); color: #04101F; display: grid; place-items: center; font-weight: 700; font-family: var(--serif); flex: none; }
.review-name { display: flex; flex-direction: column; font-weight: 700; font-size: 0.95rem; color: var(--ink); }
.review-name span { font-weight: 500; font-size: 0.78rem; color: var(--muted); }
.reviews-connect { margin-top: 1.6rem; font-size: 0.82rem; color: var(--muted); text-align: center; }

/* ----- FAQ accordion (native <details>, keyboard-accessible, JS-off safe) ----- */
.faq { max-width: 840px; margin-inline: auto; margin-top: clamp(2.2rem, 4vw, 3rem); }
.faq-item { border-bottom: 1px solid var(--line); }
.faq-item:first-child { border-top: 1px solid var(--line); }
.faq-item summary {
  list-style: none; cursor: pointer; position: relative;
  padding: 1.35rem 3rem 1.35rem 0;
  font-family: var(--serif); font-weight: 600; font-size: clamp(1.05rem, 1.7vw, 1.28rem);
  color: var(--ink); letter-spacing: -0.01em; transition: color 0.25s var(--ease);
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary:hover { color: var(--azure-600); }
.faq-item summary::before { content: ""; position: absolute; right: 4px; top: 50%; width: 15px; height: 2px; background: var(--azure-600); transform: translateY(-50%); border-radius: 2px; }
.faq-item summary::after { content: ""; position: absolute; right: 11px; top: 50%; width: 2px; height: 15px; background: var(--azure-600); transform: translateY(-50%); border-radius: 2px; transition: transform 0.3s var(--ease), opacity 0.3s var(--ease); }
.faq-item[open] summary::after { transform: translateY(-50%) rotate(90deg); opacity: 0; }
.faq-a { padding: 0 3rem 1.5rem 0; }
.faq-a p { font-size: 0.98rem; color: var(--muted); margin: 0; }
.faq-a p + p { margin-top: 0.8rem; }
.faq-item[open] .faq-a { animation: faqOpen 0.35s var(--ease); }
@keyframes faqOpen { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }

/* ----- Mobile click-to-call bar ----- */
.call-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
  display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem;
  padding: 0.6rem 0.8rem calc(0.6rem + env(safe-area-inset-bottom, 0px));
  background: rgba(8,21,37,0.94); backdrop-filter: saturate(180%) blur(16px); -webkit-backdrop-filter: saturate(180%) blur(16px);
  border-top: 1px solid var(--line-dark);
  transform: translateY(130%); transition: transform 0.4s var(--ease);
}
.call-bar.visible { transform: translateY(0); }
.call-bar-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5em;
  min-height: 52px; border-radius: 12px; font-weight: 600; font-size: 1rem; letter-spacing: 0.01em;
}
.call-bar-btn svg { width: 1.15em; height: 1.15em; flex: none; }
.call-bar-btn.call { background: rgba(255,255,255,0.1); color: #fff; border: 1.5px solid rgba(255,255,255,0.28); }
.call-bar-btn.req { background: var(--azure); color: #04101F; }
/* The bar exists to replace the phone number and Request-service button while
   they are folded away inside the nav drawer. The drawer takes over at 900px
   (see the mobile-nav block above), so the bar has to run to 900px too — at
   768px it left a 768-900px dead zone, iPad portrait included, with no visible
   number and no visible CTA anywhere on screen. Keep these two breakpoints in
   step: if the nav drawer's max-width changes, change these to match. */
@media (min-width: 901px) { .call-bar { display: none !important; } }
@media (max-width: 900px) { .site-footer { padding-bottom: 92px; } }
/* Past phone widths, two full-bleed buttons look stretched rather than
   thumb-friendly, so the bar centres on a comfortable measure instead. */
@media (min-width: 620px) and (max-width: 900px) {
  .call-bar {
    left: 50%; right: auto; transform: translateX(-50%) translateY(130%);
    width: min(560px, 100%); border-radius: 16px 16px 0 0;
    border-left: 1px solid var(--line-dark); border-right: 1px solid var(--line-dark);
  }
  .call-bar.visible { transform: translateX(-50%) translateY(0); }
}

/* ----- Legal / prose pages ----- */
.page-head { background: var(--navy-900); color: #fff; padding-block: clamp(120px, 18vh, 176px) clamp(40px, 6vw, 64px); border-bottom: 1px solid var(--line-dark); }
.page-head h1 { color: #fff; font-size: clamp(2rem, 4.4vw, 3.1rem); letter-spacing: -0.02em; }
.page-head p { color: rgba(255,255,255,0.72); margin-top: 0.8rem; max-width: 60ch; }
.prose { max-width: 760px; margin-inline: auto; }
.prose h2 { font-size: clamp(1.4rem, 2.4vw, 1.9rem); margin-top: 2.6rem; margin-bottom: 0.7rem; }
.prose h3 { font-size: 1.15rem; margin-top: 1.6rem; margin-bottom: 0.4rem; }
.prose p, .prose li { color: var(--muted); font-size: 1rem; }
.prose p + p { margin-top: 1rem; }
.prose ul { margin: 0.9rem 0 0 1.2rem; display: grid; gap: 0.5rem; }
.prose a { color: var(--azure-600); font-weight: 600; }
.prose a:hover { text-decoration: underline; }
.callout {
  border: 1px solid var(--line); border-left: 3px solid var(--azure); background: var(--mist);
  border-radius: var(--radius); padding: 1.2rem 1.4rem; margin-bottom: 2.2rem;
  font-size: 0.9rem; color: var(--muted);
}
.callout strong { color: var(--ink); }

/* ----- Footer additions — legal links + placeholder social row ----- */
.footer-legal { display: flex; gap: 1.2rem; flex-wrap: wrap; }
.footer-legal a { color: rgba(255,255,255,0.6); font-size: 0.82rem; transition: color 0.25s var(--ease); }
.footer-legal a:hover { color: #fff; }
/* Same reasoning as .footer-col li a — these sit in a horizontal row, so the
   padding is vertical only and the row's own gap already spaces them across. */
.footer-legal a, .footer-bottom .legal a { display: inline-block; padding: 0.45rem 0; }
.social-row { display: flex; align-items: center; gap: 0.55rem; margin-top: 1.4rem; flex-wrap: wrap; }
.social-dot { width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center; border: 1px dashed var(--line-dark); color: rgba(255,255,255,0.5); }
.social-dot svg { width: 18px; height: 18px; }
.social-note { font-size: 0.72rem; color: rgba(255,255,255,0.62); letter-spacing: 0.03em; }

/* ==========================================================================
   Conversion additions — hero trust proof, quick-contact dock, booking fields
   ========================================================================== */

/* ----- Hero trust proof (above the fold) ----- */
.hero-proof {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.7rem 1.1rem;
  margin-top: 2rem;
}
.hero-proof .proof-item {
  display: inline-flex; align-items: center; gap: 0.5em;
  font-size: 0.92rem; font-weight: 600; color: rgba(255,255,255,0.92);
}
.hero-proof .proof-item svg { width: 1.15em; height: 1.15em; color: var(--azure); flex: none; }
.hero-proof .proof-sep { width: 1px; height: 20px; background: rgba(255,255,255,0.22); }
.hero-proof .rating-badge {
  display: inline-flex; align-items: center; gap: 0.5em;
  padding: 0.45em 0.95em; border-radius: 999px;
  background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.3);
  font-size: 0.82rem; font-weight: 700; color: #fff;
}
.hero-proof .rating-badge .stars { display: inline-flex; gap: 1px; color: var(--brass); }
.hero-proof .rating-badge .stars svg { width: 0.95em; height: 0.95em; }
@media (max-width: 560px) { .hero-proof .proof-sep { display: none; } }

/* ---- Hero furniture scaled to the smaller headline ----
   The buttons, icons and badges were all sized against the old 5.1rem display
   type. With the headline down to 3.4rem they read oversized, so everything in
   the hero comes down proportionally. Scoped to .hero so the identical .btn-lg
   used in the closing CTA keeps its full size. */
.hero .hero-actions { gap: 0.7rem; margin-top: 1.9rem; }
.hero .hero-actions .btn-lg { padding: 0.82em 1.55em; font-size: 0.94rem; border-radius: 999px; }
.hero .hero-actions .btn svg { width: 1em; height: 1em; }
.hero .eyebrow { font-size: 0.71rem; letter-spacing: 0.16em; }
.hero .eyebrow::before { width: 20px; }
.hero .lead { font-size: clamp(0.98rem, 1.25vw, 1.12rem); margin-top: 1.15rem; }
.hero .hero-proof { gap: 0.6rem 0.9rem; margin-top: 1.6rem; }
.hero .hero-proof .proof-item { font-size: 0.83rem; }
.hero .hero-proof .proof-item svg { width: 1.05em; height: 1.05em; }
.hero .hero-proof .proof-sep { height: 16px; }
.hero .hero-proof .rating-badge { font-size: 0.75rem; padding: 0.4em 0.8em; }
.hero .hero-proof .rating-badge .stars svg { width: 0.9em; height: 0.9em; }

/* ----- Quick-contact floating dock (desktop; mobile uses the call bar) ----- */
.contact-dock {
  position: fixed; right: 20px; bottom: 20px; z-index: 95;
  display: flex; flex-direction: column; align-items: flex-end; gap: 0.7rem;
}
.contact-dock-panel {
  display: flex; flex-direction: column; gap: 0.5rem;
  opacity: 0; transform: translateY(10px) scale(0.96); transform-origin: bottom right;
  pointer-events: none; transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
.contact-dock.open .contact-dock-panel { opacity: 1; transform: none; pointer-events: auto; }
.contact-dock-panel a {
  display: inline-flex; align-items: center; gap: 0.6em; justify-content: flex-start;
  padding: 0.72em 1.15em; border-radius: 999px; font-weight: 600; font-size: 0.95rem;
  background: var(--paper); color: var(--ink); border: 1px solid var(--line); box-shadow: var(--shadow-sm);
  white-space: nowrap; transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.contact-dock-panel a:hover { transform: translateX(-3px); box-shadow: var(--shadow); }
.contact-dock-panel a svg { width: 1.1em; height: 1.1em; color: var(--azure-600); flex: none; }
.contact-dock-toggle {
  align-self: flex-end;
  width: 60px; height: 60px; border-radius: 50%; border: 0; cursor: pointer;
  background: var(--azure); color: #04101F; box-shadow: 0 16px 34px -12px var(--azure-glow);
  display: grid; place-items: center;
  transition: transform 0.3s var(--ease), background 0.3s var(--ease);
}
.contact-dock-toggle:hover { transform: translateY(-2px); background: #6FB6FF; }
.contact-dock-toggle svg { width: 26px; height: 26px; }
.contact-dock-toggle .ico-close { display: none; }
.contact-dock.open .contact-dock-toggle .ico-open { display: none; }
.contact-dock.open .contact-dock-toggle .ico-close { display: block; }
@media (max-width: 767px) { .contact-dock { display: none !important; } }

/* ----- Booking form extras ----- */
/* Bottom margin so the hint doesn't collide with the next field's label. */
.field-hint { font-size: 0.78rem; color: var(--muted); margin: 0.35rem 0 1.1rem; }
.form-confirm .confirm-sent { display: none; }
.form-card.sent .form-confirm .confirm-preview { display: none; }
.form-card.sent .form-confirm .confirm-sent { display: block; }

/* ----- "How it works" airflow section ----- */
.airflow-block { display: grid; grid-template-columns: 1fr; gap: clamp(2rem, 5vw, 4.5rem); align-items: center; }
@media (min-width: 900px) { .airflow-block { grid-template-columns: 1.05fr 0.95fr; } }
.video-frame {
  position: relative; border-radius: var(--radius-lg); overflow: hidden;
  border: 1px solid var(--line); background: linear-gradient(160deg, var(--navy-850), var(--navy-900));
  aspect-ratio: 16 / 11; box-shadow: var(--shadow); min-height: 240px;
}
.video-frame video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }

/* ----- Comfort-in-every-season cards (reuse .team-card visuals) ----- */
.comfort-grid { display: grid; grid-template-columns: 1fr; gap: 1.1rem; margin-top: clamp(2.2rem, 4vw, 3rem); }
@media (min-width: 760px) { .comfort-grid { grid-template-columns: repeat(3, 1fr); } }
/* Azure on photography only reached 3.44:1 here — white holds the label
   readable over any frame of the image. */
.comfort-grid .team-card figcaption span { color: rgba(255,255,255,0.92); font-weight: 600; font-size: 0.82rem; letter-spacing: 0.02em; }

/* ==========================================================================
   EXPERIMENT — floating section cards (home page only)

   Everything here is gated behind <body class="section-cards">, which is set
   only on index.html. Delete that one class and the page returns to its
   full-bleed bands with nothing else to undo — no markup or other CSS was
   changed to accommodate it. Pristine copies of index.html, site.css and
   site.js also sit in _backup-before-section-cards/.
   ========================================================================== */
body.section-cards { background: #F4F4F2; }
/* A fixed pseudo-element rather than background-attachment: fixed — the latter
   is unreliable on iOS Safari, where it stutters or silently falls back to
   scroll. This holds the backdrop steady while the cards travel over it.

   Warm graphite with fine film grain. Deliberately WARM rather than navy: a
   navy backdrop would swallow the .band-dark cards, which are navy themselves,
   whereas graphite separates them on hue as well as luminance. White cards get
   maximum lift against it.

   The grain is an inline feTurbulence SVG — no external request, and it kills
   the plastic look large flat gradients get on wide displays. */
body.section-cards::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background-color: #F4F4F2;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='0.30'/%3E%3C/svg%3E"),
    linear-gradient(180deg, #F8F8F7 0%, #EFEFEC 50%, #F6F6F4 100%);
  background-size: 200px 200px, auto;
  background-repeat: repeat, no-repeat;
  /* Multiply keeps the grain reading as tooth in the paper rather than a grey
     film laid over it — on a light ground a plain overlay just goes muddy. */
  background-blend-mode: multiply, normal;
}
/* Motion layer. The grain above stays fixed — like grain on a lens rather than
   crawling sand — and the light moves behind it instead, which is what makes
   the surface feel alive without ever pulling the eye off the cards.

   inset:-25% oversizes the layer so the drift and scale never expose an edge,
   and the whole thing animates on transform alone so it stays on the
   compositor and costs nothing per frame. */
body.section-cards::after {
  content: ""; position: fixed; inset: -25%; z-index: -1; pointer-events: none;
  background-image:
    radial-gradient(42% 46% at 22% 24%, rgba(120, 138, 168, 0.13) 0%, rgba(120, 138, 168, 0) 70%),
    radial-gradient(38% 42% at 78% 58%, rgba(150, 132, 106, 0.11) 0%, rgba(150, 132, 106, 0) 70%),
    radial-gradient(34% 38% at 48% 90%, rgba(110, 128, 150, 0.09) 0%, rgba(110, 128, 150, 0) 70%);
  will-change: transform;
  animation: bg-aurora 30s ease-in-out infinite alternate;
}
@keyframes bg-aurora {
  from { transform: translate3d(-7%, -5%, 0) scale(1); }
  to   { transform: translate3d(7%, 6%, 0) scale(1.24); }
}
@media (prefers-reduced-motion: reduce) {
  body.section-cards::after { animation: none; }
}
/* On a light ground the rim can't do the lifting — a white edge against a
   near-white backdrop is invisible — so the separation comes from the shadow,
   layered close-to-far and tinted navy rather than neutral grey. */
body.section-cards main > section:not(.hero) {
  box-shadow:
    0 1px 2px rgba(12, 29, 51, 0.06),
    0 10px 26px -10px rgba(12, 29, 51, 0.14),
    0 38px 76px -30px rgba(12, 29, 51, 0.20);
}
/* Depth for the cards that arrive on a 3D axis. */
/* Only for the superseded non-flat treatment, whose preset 5 arrives on a
   rotationX door-flop and needs the depth. It must NOT apply in flat mode:
   perspective makes <main> the containing block for position:fixed
   descendants, which silently breaks the pinned hero — ScrollTrigger sets the
   hero to fixed and it then resolves against <main> and scrolls away with the
   page instead of holding. Flat mode animates on y/scale only, so it has no
   use for the perspective anyway. */
body.section-cards:not(.flat) main { perspective: 1500px; }

body.section-cards main > section:not(.hero) {
  position: relative;
  width: min(1340px, calc(100% - clamp(1.25rem, 5vw, 4rem)));
  margin-inline: auto;
  margin-block: clamp(0.9rem, 2.2vw, 1.75rem);
  padding-block: clamp(2.75rem, 6vw, 5.5rem);
  border-radius: clamp(20px, 2.4vw, 34px);
  overflow: hidden;
  background: var(--paper);
  box-shadow:
    0 1px 2px rgba(8, 21, 37, 0.05),
    0 10px 24px -10px rgba(8, 21, 37, 0.16),
    0 36px 70px -28px rgba(8, 21, 37, 0.30);
}
/* Hairline catch-light along the edge — most of what sells "floating". */
body.section-cards main > section:not(.hero)::before {
  content: ""; position: absolute; inset: 0; z-index: 4;
  border: 1px solid rgba(12, 29, 51, 0.07);
  border-radius: inherit; pointer-events: none;
}

/* Services' Maintenance section was authored to continue the dark band above
   it, so it carried padding-top:0. As its own card it needs the padding back —
   but the original behaviour still has to hold if card mode is ever switched
   off, hence the rule rather than deleting it outright. */
body:not(.section-cards) #maintenance { padding-top: 0; }

/* The first card originally rode up over the hero on a negative margin. It
   read as the card covering the photo, so it now clears the hero outright. */
body.section-cards main > .hero + section {
  margin-top: clamp(1.25rem, 3vw, 2.5rem);
  z-index: 2;
}
/* The cue is pinned to bottom:26px of the hero — exactly the strip the first
   card now covers. The card cresting the hero is itself the "keep going"
   signal, so retire the cue rather than have it read as clipped. */
body.section-cards .scroll-cue { display: none; }

/* Card surfaces, carried over from the band each section already used. */
body.section-cards main > section.band-mist { background: var(--mist); }
body.section-cards main > section.band-dark { background: linear-gradient(158deg, var(--navy-800), var(--navy-900)); }
body.section-cards main > section.cta-band  { background: var(--navy-900); }
/* The dark cards invert it: against a near-white ground their own edge is
   already the strongest line on the page, so the rim goes light and faint just
   to soften the cut rather than draw it. */
body.section-cards main > section.band-dark::before,
body.section-cards main > section.cta-band::before { border-color: rgba(255, 255, 255, 0.10); }
body.section-cards main > section.band-dark,
body.section-cards main > section.cta-band {
  box-shadow:
    0 1px 2px rgba(8, 21, 37, 0.20),
    0 14px 30px -12px rgba(8, 21, 37, 0.40),
    0 46px 84px -30px rgba(8, 21, 37, 0.55);
}
/* .cta-band parks its photo and scrim at z-index -2/-1, which sat behind the
   page itself when the section had no background of its own. Now that the card
   paints a surface, they have to come forward or they vanish underneath it. */
body.section-cards main > section.cta-band .cta-media   { z-index: 0; }
body.section-cards main > section.cta-band .cta-overlay { z-index: 1; }
body.section-cards main > section.cta-band > .container { position: relative; z-index: 2; }

@media (prefers-reduced-motion: reduce) {
  body.section-cards main > section:not(.hero) { transform: none !important; opacity: 1 !important; }
}

/* ==========================================================================
   FLAT TREATMENT — <body class="section-cards flat">, and this is what ships.
   Live on index.html, services.html and about.html (the three pages that run
   card mode; the legal pages never did).

   Keeps the card SURFACE (graphite backdrop, shadow stack, catch-light rim —
   everything above is untouched) and drops the card CHOREOGRAPHY:

     1. One entrance for every section instead of a bespoke one per section,
        which also frees the per-element reveals to run again. See site.js.
     2. No cards inside cards. Where a section is already a card, its interior
        tiles lose their border/fill/radius/shadow and are held by spacing,
        the icon, and the occasional hairline instead.

   Deliberately NOT flattened, because these are not chrome:
     .team-card   — photographs; the radius is on the image itself.
     .form-card   — a white working surface on the navy CTA card. The contrast
                    is the point, and a form earns a discrete container.
     .area-map / .split-media — photographs, as above.
   ========================================================================== */

/* ----- Services ----- */
.flat .services-grid { gap: clamp(2rem, 3.6vw, 3.2rem) clamp(2rem, 3.6vw, 3.4rem); grid-auto-rows: auto; }
.flat .service-card {
  background: none; border: 0; border-radius: 0; box-shadow: none;
  padding: 0; overflow: visible;
}
/* The azure top-rule was a hover flourish for the card edge; with no edge to
   trace it has nothing to sit on. (This is .service-card's own ::after — the
   .card-link ::after stretched-link overlay is a different element and stays.) */
.flat .service-card::after { content: none; }
.flat .service-card:hover { transform: none; box-shadow: none; }
/* Hover affordance has to move somewhere, since these tiles are links. The
   heading takes it. */
.flat .service-card h3 a { transition: color 0.3s var(--ease); }
.flat .service-card:hover h3 a { color: var(--azure-600); }
/* The one tile that stays a card: the navy "Not sure what you need?" prompt,
   which carries inline styles and so keeps its own fill. Give its padding back
   — one accent tile among five flat ones reads as intent, not as leftovers.
   Tagged with a class in the markup rather than matched on [style], because
   GSAP writes inline styles onto every .reveal element and that selector would
   quietly re-pad the whole grid. */
.flat .service-card--accent { padding: 1.9rem 1.8rem 2rem; border-radius: var(--radius); }

/* ----- The 777 Standard ----- */
.flat .promise-grid { gap: clamp(1.9rem, 3.2vw, 2.8rem) clamp(2rem, 3.4vw, 3rem); }
.flat .promise {
  background: none; border: 0; border-radius: 0; box-shadow: none; padding: 0;
}
.flat .promise:hover { transform: none; box-shadow: none; }

/* ----- Brands strip ----- */
/* Plated tiles become plain wordmarks. Uniform weight and tracking already do
   the "one designed system" job the plates were added for. */
.flat .brands-row { gap: clamp(1.2rem, 2.6vw, 2.2rem); }
.flat .brand-name {
  border: 0; border-radius: 0; background: none; box-shadow: none;
  min-height: 0; padding: 0.7rem 0.4rem; opacity: 0.45;
}
.flat .brand-name:hover { opacity: 1; transform: none; box-shadow: none; border-color: transparent; }

/* ----- Services page: maintenance benefits (on a navy card) ----- */
/* The translucent white plate was separating these from the navy band behind
   them. Inside a navy card the azure icon already anchors each row, so the
   plate is doing nothing but adding a second edge. */
.flat .benefits { gap: 1.4rem clamp(1.8rem, 3vw, 2.6rem); }
.flat .benefit { background: none; border: 0; border-radius: 0; padding: 0; }

/* ----- About page: values ----- */
.flat .values-grid { gap: clamp(1.9rem, 3.2vw, 2.8rem) clamp(2rem, 3.4vw, 3rem); }
.flat .value { background: none; border: 0; border-radius: 0; box-shadow: none; padding: 0; }
.flat .value:hover { transform: none; box-shadow: none; }

/* ----- About page: credential facts (on a navy card) ----- */
/* The serif figure carries these on its own. Chips are left alone throughout —
   a pill reads as metadata, not as a card, so it isn't part of the nesting. */
.flat .fact-row { gap: 1.4rem clamp(1.8rem, 3vw, 2.6rem); }
.flat .fact { background: none; border: 0; border-radius: 0; padding: 0; }

/* ----- Reviews ----- */
/* The pull-quote holds its emphasis on scale, serif and centring alone. */
.flat .review-feature {
  background: none; border: 0; border-radius: 0;
  padding: clamp(1.2rem, 2.6vw, 1.8rem) 0 0;
}
/* Three unbordered quotes side by side do run together, so these keep one
   hairline each — a rule above, not a box around. */
.flat .reviews-grid { gap: clamp(1.7rem, 3vw, 2.4rem) clamp(2rem, 3.4vw, 3rem); margin-top: clamp(2.2rem, 4vw, 3.2rem); }
.flat .review-card {
  background: none; border: 0; border-radius: 0; box-shadow: none;
  border-top: 1px solid var(--line);
  padding: 1.5rem 0 0;
}

/* ==========================================================================
   Reduced motion
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
  .reveal { opacity: 1; transform: none; }
  .scroll-cue .line { animation: none; }
  /* site.js never initialises GSAP under reduced motion, but belt-and-braces:
     neutralise the drawn connector and drop the progress bar entirely. */
  html.gsap .step::after { transform: none; }
  .scroll-progress { display: none; }
  /* Skip the splash entirely — the rule above clamps duration but not the
     delay, which would otherwise leave a blank navy panel sitting there. */
  .preloader { display: none; }
}

/* ==========================================================================
   PHONE PASS — 700px and below only

   Everything in this block is gated at max-width:700px, which is below the
   768px tablet width and well below any desktop layout, so nothing here can
   reach them. Fixes are for problems that only exist once the multi-column
   layouts collapse to one column on a ~390px screen.
   ========================================================================== */
@media (max-width: 700px) {

  /* ----- 1. Hero copy was sitting unprotected on the busiest part of the shot.
     On wide screens the scrim is shaped to the copy COLUMN, which works because
     the copy occupies about half the frame. On a phone the copy goes full-width
     and drops over the roofline and the flowerbed, where the headline, the
     rating badge and "30 years of experience" all lost against the highlights.
     A bottom-weighted wash is the right tool at this width: the copy is bottom-
     anchored, so darkening the bottom covers all of it and leaves the top of
     the photograph — the part actually on show — alone.

     This also starts paying down the AA warning noted further up the file,
     which applies to the phone layout worst of all. */
  /* Interior heroes get the same bottom-weighted treatment on a phone — their
     copy is bottom-anchored too, and Services was the worst-measuring hero on
     the site at 1.54:1. */
  /* Interior phone scrim removed on request — photos shown as shot. */
  .hero-page .hero-overlay { background: none; }
  /* Homepage phone scrim removed on request — photo shown as shot. */
  .hero:not(.hero-page) .hero-overlay { background: none; }

  /* ----- 2. The intro statement is a display face sized for a two-column
     layout. Stacked on a phone it ran a single sentence to most of a screen. */
  .intro-statement { font-size: 1.24rem; line-height: 1.4; letter-spacing: -0.01em; }

  /* ----- 3. Centred section heads. Centring a display serif over ~22
     characters gives a bad rag — "Promises we make on / day one.", "We install
     and service / all major brands." — and it fights the body copy underneath,
     which is left-aligned. Left-align both and the column reads as one thing. */
  .section-head.center, .center { text-align: left; }
  .section-head.center { margin-inline: 0; }
  .rating-summary, .reviews-connect, .brands-note { text-align: left; justify-content: flex-start; }

  /* ----- 4. Brands. Eight grey words floating in a 2-up grid with 19px gutters
     read as debris rather than a list, and "Mitsubishi Electric" wrapped to two
     lines and knocked its row out of rhythm. Tighter, darker, and left-set so
     the column has an edge to line up on. */
  /* Selectors doubled to reach (0,2,0). The flat block above styles these as
     `.flat .brands-row` / `.flat .brand-name`, and a media query adds no
     specificity of its own — a single class here would silently lose. Doubling
     ties on specificity, and this block is last in the file, so it wins. */
  .brands-row.brands-row { grid-template-columns: 1fr 1fr; gap: 0.4rem 1rem; }
  .brand-name.brand-name {
    place-items: center start; text-align: left;
    padding: 0.3rem 0; font-size: 0.72rem; letter-spacing: 0.1em; opacity: 0.62;
  }

  /* ----- 5. Service-area neighbourhoods. Two CSS columns on a 390px screen
     staggered the rows against each other — "Salmon Creek" and "Camas" landing
     at different heights — so the ticks never lined up. One column. */
  .hood-list { columns: 1; }

  /* ----- 6. Icon-led lists. The 44px icon column plus a 16px gutter took 60px
     off a 390px screen and pushed every line of copy into a narrow well. */
  .diff-item { gap: 0.7rem; }
  .diff-item .tick { width: 34px; height: 34px; }
  .diff-item .tick svg { width: 17px; height: 17px; }

  /* ----- 7. The map's imagery credit ran under the caption pill and the two
     overlapped. Move it clear, below the pill's left edge. */
  .area-map .map-tag { right: 16px; bottom: 34px; }
  .area-map .map-credit { right: auto; left: 16px; bottom: 12px; }

  /* ----- 8. TYPE COMPACTION.
     The page ran 18.4 phone screens. Almost none of that was content — it was
     a desktop type scale surviving intact onto a 390px column, where every
     clamp() had already bottomed out at its minimum and so stopped responding
     to viewport at all. Headings set for a 1340px card were still 32px on a
     330px measure, wrapping to three lines each.

     Everything below only lowers those floors. The clamp maxima are untouched,
     so desktop is bit-for-bit unchanged; and this block is max-width:700px,
     which is under iPad mini (744) and iPad portrait (768), so tablets never
     see it either. */

  /* Headings. 2rem floor -> 1.55rem. Still the largest thing in the card and
     still the display serif; it just stops eating three lines to say four
     words. */
  h2.section-title { font-size: 1.55rem; line-height: 1.16; }
  h1 { font-size: clamp(1.75rem, 7.6vw, 2.4rem); line-height: 1.1; }
  h3 { font-size: 1.05rem; }

  /* Body. 1.55 leading is a desktop measure; on a ~40-character column it
     opens the paragraph up far more than it needs. 1.45 still clears the
     serif's descenders. */
  .lead { font-size: 1rem; line-height: 1.45; }
  .intro-statement { font-size: 1.12rem; line-height: 1.32; }
  p, li { line-height: 1.5; }
  .service-card p, .promise p, .value p, .review-text { font-size: 0.94rem; }

  /* Section rhythm. 44px of padding top and bottom on twelve stacked cards is
     over a full screen of nothing; the card's own edge and the gap between
     cards already separate them. */
  body.section-cards main > section:not(.hero) {
    padding-block: 2.1rem;
    margin-block: 0.6rem;
  }
  .section-head .eyebrow { margin-bottom: 0.6rem; }
  .section-head p { margin-top: 0.7rem; }

  /* Flat-mode grids: the de-carded tiles were spaced for a multi-column layout.
     Stacked one-up they only need enough gap to read as separate items. */
  .flat .services-grid { gap: 1.15rem; }
  .flat .promise-grid { gap: 1.05rem; }
  .flat .values-grid { gap: 1.05rem; }
  .flat .reviews-grid { gap: 1.1rem; margin-top: 1.4rem; }
  .flat .review-card { padding-top: 0.95rem; }
  .flat .review-feature { padding-top: 0.8rem; }
  .promise-grid, .values-grid { margin-top: 1.4rem; }

  /* ----- 9. Icon-led tiles become media objects.
     A service tile was 171px and 76px of that was the icon plate plus its
     margin — 45% of the tile, carrying no information, sixteen times over.
     Shrinking the plate recovered part of it; moving the text up alongside
     recovers the rest, because the icon no longer owns a row of its own.

     The icon sits in column 1 of row 1 and does NOT span — so the heading
     shares its row and the paragraph flows underneath it in column 2. Column 1
     is simply empty from row 2 down, which is what makes this a media object
     rather than a hanging indent.

     Cost is 44px of the 330px measure (32px plate + 12px gutter), leaving ~40
     characters a line. That is the trade this asks for: width for height. The
     .diff-item lists were already built this way and are left alone. */
  .service-icon { width: 28px; height: 28px; border-radius: 8px; margin-bottom: 0; }
  .service-icon svg { width: 15px; height: 15px; }

  .services-grid .service-card,
  .promise,
  .value {
    display: grid;
    grid-template-columns: 28px 1fr;
    column-gap: 0.7rem;
    row-gap: 0.3rem;
    align-items: start;
  }
  .services-grid .service-card > :not(.service-icon),
  .promise > :not(.service-icon),
  .value > :not(.service-icon) { grid-column: 2; }

  /* ALIGNMENT. These tiles come from three components whose headings each set
     their own size and bottom margin (.promise h3 is 1.14rem, .service-card h3
     inherits, .value h3 is 1.2rem). Centring each heading in its row therefore
     centred a different box every time, and the plates ended up on three
     different offsets down the page — services +1px, promises -8px. Read as a
     ragged left column.

     Fixed by removing the variables rather than compensating for them: one
     heading size and line-height for all three, margins zeroed so the row-gap
     is the only spacing, and both plate and heading anchored to the TOP of the
     row. The heading then gets a fixed nudge down to sit its first line on the
     plate's centre — (28px plate - 20.8px line) / 2 = 3.6px. A constant, not a
     centring rule, so a heading that wraps to two lines keeps its first line on
     the plate instead of drifting half a line down. */
  .services-grid .service-card > h3,
  .promise > h3,
  .value > h3 {
    grid-row: 1; align-self: start;
    font-size: 1rem; line-height: 1.3; letter-spacing: -0.01em;
    margin: 3.6px 0 0;
  }
  /* margin:0, not margin-top:0 — `.promise .service-icon` and `.value
     .service-icon` each carry their own 1.1rem bottom margin from the stacked
     layout. It ties this rule on specificity, so zeroing only the top left it
     in place, and since the plate sits in row 1 that margin inflated the row
     and opened a 26px hole between every promise heading and its paragraph
     while services sat at 8px. */
  .services-grid .service-card > .service-icon,
  .promise > .service-icon,
  .value > .service-icon { grid-column: 1; grid-row: 1; align-self: start; margin: 0; }

  /* The navy "Not sure what you need?" tile has no icon, so the 32px column
     would just indent it against nothing. Put it back to normal flow. */
  .service-card--accent { display: block; }

  /* ----- 10. Form controls must not be under 16px.
     iOS Safari zooms the whole page in when a focused control's text is smaller
     than 16px, and it does not zoom back out afterwards — the visitor is left
     scrolled sideways in a magnified form, mid-booking. The compact pass set
     these to 0.94rem (15.04px), which is invisible on desktop and breaks every
     iPhone. 16px exactly: the threshold, so nothing more is spent on it than
     the bug costs.

     This is also why the fix belongs here and not by editing the 0.94rem rule —
     that value is right on desktop, where no such zoom exists.

     Selectors match `.cta-form .field input` (0,2,0) rather than plain `.field`,
     which would lose to it silently; this block is last in the file so an equal
     specificity wins on order. */
  .cta-form .field input, .field input,
  .cta-form .field select, .field select,
  .cta-form .field textarea, .field textarea { font-size: 16px; }

  /* The rest of the form is deliberately left alone: `.cta-form .field` already
     ran a compact pass — 0.7rem gaps, 0.78rem labels, a 68px textarea — and
     those numbers are tighter than a fresh pass would pick. Its height is eight
     fields at a touch-sized target each, which is the point of it; shortening
     the form means removing fields, not leading. */

  /* ----- 11b. Room for the quick-capture form.
     The hero is a fixed-height box with bottom-anchored copy, so ~230px of
     form has to come from somewhere or the headline slides up under the fixed
     header. The lead paragraph pays for it: once someone has asked for an
     estimate they are past being persuaded by "Precise climate control, cleaner
     indoor air…" — the three fields are the more useful thing in that space. */
  .hero.quick-open .lead { display: none; }
  /* The proof row goes too. Dropping the lead alone still left the headline
     tucked under the fixed header (top 33px against a 77px header). The row's
     job — "5.0 on Google", the service area — is to earn the click that has
     just happened; the form carries its own reassurance in "No obligation, no
     salesperson". Both come straight back when the form is closed. */
  .hero.quick-open .hero-proof { display: none; }
  .hero.quick-open .hero-actions { margin-top: 1.4rem; }
  .hero-quick { max-width: none; padding: 1rem; }
  .quick-row { grid-template-columns: 1fr; gap: 0.5rem; }
  .quick-form select { margin-top: 0.5rem; }

  /* ----- 11a. Two-path CTAs go full width.
     Stacked, they sized to their own text — 218px and 243px — which read as
     two mismatched buttons rather than a choice between two equal options.
     Equal width also gives each a full-width thumb target. */
  .hero-paths { flex-direction: column; align-items: stretch; gap: 0.6rem; }
  .hero-paths .btn { width: 100%; justify-content: flex-start; padding-inline: 1.3em; }
  /* Label left, arrow pinned right. No row-reverse — the markup is already in
     reading order (label, then arrow), and reversing it put the arrow on the
     left pointing away from the text. */
  .hero-paths .path-planned { justify-content: space-between; }

  /* ----- 11. FOOTER.
     1202px — a screen and a half of footer, because a four-column desktop
     layout stacks into four full-width blocks on a phone: brand 174, Explore
     186, Services 225, Get in touch 157, plus 125px of gaps between them.

     Rebuilt as inline wrapped rows rather than stacked lists. Every link stays
     in the DOM and stays a link — nothing is display:none'd that carries an
     href, so internal linking and mobile-first indexing are unaffected. What
     goes is the scaffolding around them: the column headings (the groups are
     obvious from the link text at this size), the marketing blurb, and the
     placeholder social row, which is three dead spans and a "added at launch"
     note that no visitor needs. */

  .site-footer { padding-top: 1.75rem; }
  .footer-top { display: block; padding-bottom: 0.7rem; }

  /* Brand: mark only. The blurb restates the hero, at the very bottom of the
     page, to someone who has already read it. */
  .footer-brand p, .footer-brand .social-row { display: none; }
  .footer-brand .logo { height: 26px; margin-bottom: 0.9rem; }

  /* Column headings go; the links speak for themselves in a row. */
  .footer-col h4 { display: none; }
  .footer-col { margin-bottom: 0.3rem; }

  /* The lists become wrapped inline rows, separated by gap alone.
     Mid-dots via `li:not(:last-child)::after` were the obvious way to do this
     and they are wrong here: nothing in CSS knows where a flex line breaks, so
     the dot after the last item on a wrapped line dangles at the end of the row
     ("Indoor Air Quality ·"). Spacing separates them just as clearly with no
     artefact, and there is no pseudo-element for a screen reader to trip on. */
  .footer-col ul { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 1.15rem; }
  .footer-col li { display: inline-flex; align-items: baseline; }
  /* Padding kept vertical-only so the rows stay tight but each link still has
     a ~30px hit box — down from 40px, which is the real cost of this change. */
  .footer-col li a { display: inline-block; padding: 0.34rem 0; font-size: 0.9rem; }
  .footer-col span { font-size: 0.86rem; }

  /* ----- HIERARCHY.
     Flattening the columns into rows made the footer short but uniform: three
     rows of identically-weighted links where the phone number carried no more
     emphasis than "Maintenance". Short is not the same as clean. So the groups
     are ranked by what someone on a phone actually came here for.

     Targeted with :has() on the links' own hrefs rather than :nth-child. The
     columns are just sibling divs, so a positional selector would silently
     retarget the moment one is reordered or added — the same fragility the
     preset array in site.js is commented for. A tel: link is what makes the
     contact column the contact column. Where :has() is unsupported the whole
     block is skipped and the uniform layout above still stands. */

  /* Contact first — above the nav rows, directly under the mark.
     gap:0 is load-bearing. `.footer-top` carries gap:2.6rem from the stacked
     base layout, which `display:block` above ignored; switching to flex made it
     apply again and put 41.6px between all four blocks, adding 125px and
     undoing most of the compaction. Spacing is set per block instead.
     The brand needs an explicit order too — order:-1 alone sent contact above
     the logo, since the brand is a sibling in the same flex container. */
  .footer-top { display: flex; flex-direction: column; gap: 0; }
  .footer-brand { order: -2; }
  .footer-col:has(a[href^="tel:"]) { order: -1; margin-bottom: 0.9rem; }

  /* The phone number is the primary action. Also buys its tap target back:
     1.2rem + the shared vertical padding puts it near 40px. */
  .footer-col:has(a[href^="tel:"]) a[href^="tel:"] {
    font-size: 1.2rem; font-weight: 700; color: #fff; letter-spacing: -0.01em;
  }
  .footer-col:has(a[href^="tel:"]) ul { gap: 0 0.9rem; }
  .footer-col:has(a[href^="tel:"]) a[href^="mailto:"] { font-size: 0.84rem; }
  /* Address and hours are not links and shouldn't read as one. */
  .footer-col:has(a[href^="tel:"]) span {
    font-size: 0.78rem; color: rgba(255, 255, 255, 0.48);
  }

  /* Deep service links are secondary to the four top-level pages, so they sit
     back a step in size and value. Only the anchored ones match, which leaves
     "Services" in the nav row at full weight. */
  .footer-col:has(a[href*="services.html#"]) a {
    font-size: 0.84rem; color: rgba(255, 255, 255, 0.55);
  }
  .footer-col:has(a[href*="services.html#"]) { margin-top: 0.15rem; }

  /* ----- LINE STRUCTURE.
     What still read as messy was where the rows broke, not their weight.

     Contact was letting a link and a plain fact share a line — "hello@… |
     Serving Vancouver, WA" — and orphaning "Hours:" onto the next one. Each
     link now takes a full line; the two non-link facts share the last one,
     which is the grouping that matches the meaning. */
  .footer-col:has(a[href^="tel:"]) li:has(a) { flex: 0 0 100%; }
  .footer-col:has(a[href^="tel:"]) li:has(span) { margin-top: 0.15rem; }

  /* Five service links wrapped 3-then-2 with a ragged right edge. A two-column
     grid filled column-first gives two aligned stacks instead — the break is
     now a decision rather than wherever the text happened to run out. */
  .footer-col:has(a[href*="services.html#"]) ul {
    display: grid; grid-auto-flow: column; grid-template-rows: repeat(3, auto);
    justify-content: start; gap: 0 2rem;
  }
  /* Aligning them into three rows costs a row versus the ragged two-line wrap,
     so the secondary links give some of it back in padding. They are the least
     likely thing in the footer to be tapped, and still clear 26px. */
  .footer-col:has(a[href*="services.html#"]) a { padding: 0.22rem 0; }

  /* The bottom bar was three stacked lines of near-identical grey. The legal
     links are the only interactive thing in it, so they lead; the two static
     notices follow as one quieter block. */
  .footer-bottom { flex-direction: column; align-items: flex-start; }
  .footer-bottom .footer-legal, .footer-bottom .legal:has(a) { order: -1; }
  .footer-bottom > p, .footer-bottom > .legal:not(:has(a)) {
    font-size: 0.72rem; color: rgba(255, 255, 255, 0.38);
  }

  /* The bottom bar was three stacked rows — copyright, legal links, licence —
     at 96px. flex-wrap with a shared gap lets them share lines instead. */
  .footer-bottom { padding-top: 0.75rem; gap: 0.2rem 0.9rem; font-size: 0.75rem; }
  .footer-bottom .legal, .footer-legal { gap: 0 0.9rem; }
  .footer-legal a, .footer-bottom .legal a { padding: 0.25rem 0; }

  .site-footer { padding-top: 1.35rem; }
  .footer-brand .logo { margin-bottom: 0.6rem; }
  /* 92px was clearance for the 72px call bar plus a comfortable margin. 80px
     still clears it and the safe-area inset is added on top of the bar itself,
     not here, so this is not eating into the home-indicator gap. */
  .site-footer { padding-bottom: 80px; }
}

/* ----- FAQ page (faq.html) ----- */
/* The questions follow a page-head that already introduces them, so the
   section's full top padding plus the accordion's own margin left ~190px of
   blank space. Trimmed to a normal gap. */
.faq-page { padding-top: clamp(2.2rem, 4.5vw, 3.6rem); }
.faq-page .faq { margin-top: 0; }
.faq-more { max-width: 840px; margin: clamp(1.8rem, 3vw, 2.4rem) auto 0; color: var(--muted); font-size: 0.98rem; }
.faq-more a { color: var(--azure-600); font-weight: 600; }

/* ==========================================================================
   FULL-BLEED BANDS — <body class="flat bands"> (home page, Sept 2026)
   The floating cards were retired on the home page. Every section now runs
   edge to edge in the colour its card used to carry, so the page reads as one
   continuous surface instead of a stack of tiles. services/about still use
   section-cards and are untouched by anything here.
   ========================================================================== */
/* Tighter than the base --section-y (up to 140px): with no card edge doing the
   separating, the colour change does it, and 140px per band made the page
   feel empty. Matches the rhythm the cards had. */
body.bands main > section:not(.hero) { padding-block: clamp(3.25rem, 6.5vw, 6rem); }
/* The dark band keeps the navy gradient its card used. */
body.bands main > section.band-dark { background: linear-gradient(158deg, var(--navy-800), var(--navy-900)); }
/* Contact keeps its dusk photograph as a full-width background. isolation
   gives the band its own stacking context so the photo (z -2) and scrim (z -1)
   stay inside it rather than slipping behind the page. */
body.bands main > section.cta-band { isolation: isolate; background: var(--navy-900); }
/* Two white bands meet between Airflow and The 777 Standard. A hairline inside
   the content width separates them without inventing a colour. */
body.bands main > section:not([class*="band-"]):not(.cta-band):not(.hero) + section:not([class*="band-"]):not(.cta-band) > .container {
  border-top: 1px solid var(--line); padding-top: clamp(3.25rem, 6.5vw, 6rem);
}
body.bands main > section:not([class*="band-"]):not(.cta-band):not(.hero) + section:not([class*="band-"]):not(.cta-band) { padding-top: 0; }
@media (max-width: 700px) {
  body.bands main > section:not(.hero) { padding-block: 2.6rem; }
  body.bands main > section:not([class*="band-"]):not(.cta-band):not(.hero) + section:not([class*="band-"]):not(.cta-band) > .container { padding-top: 2.6rem; }
  body.bands main > section:not([class*="band-"]):not(.cta-band):not(.hero) + section:not([class*="band-"]):not(.cta-band) { padding-top: 0; }
}
/* The "Scroll" cue was hidden under the cards and never came back into the
   design; the hero is sized to its photo now, so the next band is already in
   view and the cue would only sit on top of the picture. */
body.bands .scroll-cue { display: none; }

/* ----- Band transitions (home page) -----
   Every colour change gets a transition instead of a hard cut:
   - light to light (white <-> mist): the mist band fades in and out over
     ~160px, so the tint arrives gradually.
   - into and out of dark bands: a slow asymmetric wave in the neighbouring
     colour, so the navy rolls in rather than starting on a ruler line.
   - contact photo into the footer: the photo fades down into the footer navy.
   The hero keeps a hard edge on purpose — anything laid over its bottom would
   put a layer back on the photograph. */
body.bands { --band-fade: clamp(90px, 11vw, 170px); --wave-h: clamp(26px, 5.2vw, 84px); }

/* Mist fades out into a white band below it... */
body.bands main > section.band-mist {
  background: linear-gradient(180deg,
    var(--mist) 0, var(--mist) calc(100% - var(--band-fade)), var(--paper) 100%);
}
/* ...and also fades in when a white band sits above it. */
body.bands main > section:not([class*="band-"]):not(.cta-band):not(.hero) + section.band-mist {
  background: linear-gradient(180deg,
    var(--paper) 0, var(--mist) var(--band-fade),
    var(--mist) calc(100% - var(--band-fade)), var(--paper) 100%);
}
/* White bands above mist fade toward it too, so the change is spread across
   both sides of the seam rather than happening in one band. */
body.bands main > section:not([class*="band-"]):not(.cta-band):not(.hero):has(+ section.band-mist) {
  background: linear-gradient(180deg, var(--paper) calc(100% - var(--band-fade)), #FAFBFD 100%);
}

/* Waves. Drawn as the NEIGHBOUR's colour laid over the band's own edge, with
   preserveAspectRatio="none" so one path stretches to any width. */
body.bands main > section.band-dark,
body.bands main > section.cta-band { position: relative; }
body.bands main > section.band-dark::before,
body.bands main > section.band-dark::after,
body.bands main > section.cta-band::before {
  content: ""; position: absolute; left: 0; right: 0; height: var(--wave-h);
  background-repeat: no-repeat; background-size: 100% 100%;
  pointer-events: none; z-index: 1;
}
/* Top of the dark band: white above, dipping into the navy off-centre. */
body.bands main > section.band-dark::before {
  top: -1px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V22C1250 70 1010 90 760 70 520 50 300 20 0 52Z' fill='%23FFFFFF'/%3E%3C/svg%3E");
}
/* Bottom of the dark band: the mist of Brands rising back in, mirrored so the
   two edges don't read as the same stamp. */
body.bands main > section.band-dark::after {
  bottom: -1px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0 90H1440V40C1180 12 940 4 680 26 440 46 220 70 0 44Z' fill='%23F4F7FB'/%3E%3C/svg%3E");
}
/* Top of the contact band: white from Reviews curving over the dusk photo. */
body.bands main > section.cta-band::before {
  top: -1px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V48C1160 24 900 22 660 44 420 66 200 78 0 30Z' fill='%23FFFFFF'/%3E%3C/svg%3E");
}
/* Contact photo melting into the footer navy. Sits above the photo and scrim
   (z -2 / -1) but below the copy and form. */
body.bands main > section.cta-band::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: var(--band-fade);
  background: linear-gradient(180deg, rgba(5,13,24,0) 0%, var(--navy-950) 100%);
  z-index: -1; pointer-events: none;
}
/* The copy must clear the wave, which is up to 84px tall. */
body.bands main > section.band-dark,
body.bands main > section.cta-band { padding-top: calc(clamp(3.25rem, 6.5vw, 6rem) + var(--wave-h) * 0.6); }
body.bands main > section.band-dark { padding-bottom: calc(clamp(3.25rem, 6.5vw, 6rem) + var(--wave-h) * 0.6); }

/* Soft shapes inside the dark band: two faint azure glows at opposite corners,
   echoing the wave's direction. Layered into the band's own background so they
   need no extra element. */
body.bands main > section.band-dark {
  background:
    radial-gradient(60% 70% at 92% 18%, rgba(77,163,255,0.14) 0%, rgba(77,163,255,0) 62%),
    radial-gradient(50% 60% at 4% 92%, rgba(77,163,255,0.08) 0%, rgba(77,163,255,0) 60%),
    linear-gradient(158deg, var(--navy-800), var(--navy-900));
}

@media (max-width: 700px) {
  body.bands main > section.band-dark,
  body.bands main > section.cta-band { padding-top: calc(2.6rem + var(--wave-h) * 0.6); }
  body.bands main > section.band-dark { padding-bottom: calc(2.6rem + var(--wave-h) * 0.6); }
}

/* ==========================================================================
   STRAIGHT-TO-THE-POINT PASS (home page, Sept 2026)
   ========================================================================== */
/* Hero buttons: one line each, slimmer. The ghost button gets a frosted fill
   so it holds up on the uncovered photo. */
.hero .hero-paths { gap: 0.6rem; margin-top: 1.6rem; }
.hero .hero-paths .btn {
  white-space: nowrap; gap: 0.5em; padding: 0.7em 1.3em; font-size: 0.95rem;
  font-weight: 600; line-height: 1.2;
}
.hero .hero-paths .btn svg { width: 1.05em; height: 1.05em; flex: none; }
.hero .hero-paths .path-planned {
  background: rgba(8, 21, 37, 0.28); border-color: rgba(255, 255, 255, 0.55);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
}
.hero .hero-paths .path-planned:hover { background: rgba(8, 21, 37, 0.42); border-color: #fff; }
.path-label { display: inline; }
@media (max-width: 700px) {
  /* Side by side when they fit, stacked on the narrowest phones. */
  .hero .hero-paths { flex-direction: row; flex-wrap: wrap; align-items: stretch; }
  .hero .hero-paths .btn { width: auto; flex: 1 1 auto; justify-content: center; padding-inline: 1em; font-size: 0.92rem; }
  .hero .hero-paths .path-planned { justify-content: center; }
}

/* Brands, folded into the services band as a single line. */
.brands-line { margin-top: clamp(1.6rem, 3vw, 2.4rem); font-size: 0.9rem; color: var(--muted); }

/* Optional-field marker on the booking form. */
.field .opt { font-weight: 400; color: var(--muted); }
/* Balanced wrap so a two-line headline breaks at the comma, not before a lone word. */
.hero h1 { text-wrap: balance; }
@media (max-width: 700px) {
  .hero .hero-paths .btn { min-height: 46px; }
}
.bands .section-title { text-wrap: balance; }
@media (max-width: 700px) {
  /* Eight towns stacked one per row was a screen of list; two columns halves it. */
  .bands .hood-list { columns: 2; column-gap: 1rem; }
}

/* "See all services" button + brands line under the home services grid. */
.services-more { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.6rem; margin-top: clamp(1.8rem, 3.4vw, 2.6rem); }
.services-more .btn { gap: 0.5em; padding: 0.72em 1.35em; font-size: 0.95rem; font-weight: 600; background: var(--paper); }
.services-more .btn svg { width: 1.05em; height: 1.05em; }
.services-more .brands-line { margin-top: 0; flex: 1 1 22rem; }
@media (max-width: 700px) {
  .services-more .btn { width: 100%; justify-content: center; min-height: 46px; }
}

/* ----- Review carousel (home) ----- */
.review-carousel { margin-top: clamp(1.8rem, 3.4vw, 2.6rem); }
.review-track {
  list-style: none; margin: 0; padding: 0.25rem 0 0.5rem;
  display: grid; grid-auto-flow: column;
  grid-auto-columns: calc((100% - 2 * 1.4rem) / 3); gap: 1.4rem;
  overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth;
  overscroll-behavior-x: contain; scrollbar-width: none;
}
.review-track::-webkit-scrollbar { display: none; }
.review-track:focus-visible { outline: 2px solid var(--azure-600); outline-offset: 4px; border-radius: 12px; }
.review-slide { scroll-snap-align: start; display: flex; }
.review-carousel .review-card {
  width: 100%; display: flex; flex-direction: column; gap: 0.9rem;
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.5rem 1.5rem 1.4rem; box-shadow: var(--shadow-sm);
}
.review-carousel .review-text { font-size: 0.96rem; line-height: 1.55; color: var(--ink); margin: 0; }
.carousel-controls { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-top: 1.4rem; }
.carousel-btn {
  width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center;
  border: 1px solid var(--line); background: var(--paper); color: var(--ink); cursor: pointer;
  transition: background 0.25s var(--ease), border-color 0.25s var(--ease), opacity 0.25s var(--ease);
}
.carousel-btn svg { width: 18px; height: 18px; }
.carousel-btn:hover:not(:disabled) { border-color: var(--navy-700); background: var(--mist); }
.carousel-btn:disabled { opacity: 0.35; cursor: default; }
.carousel-dots { display: flex; gap: 0.45rem; }
.carousel-dots button {
  width: 8px; height: 8px; padding: 0; border: 0; border-radius: 999px; cursor: pointer;
  background: var(--line); transition: width 0.3s var(--ease), background 0.3s var(--ease);
}
.carousel-dots button[aria-current="true"] { width: 22px; background: var(--azure-600); }
@media (max-width: 1000px) { .review-track { grid-auto-columns: calc((100% - 1.4rem) / 2); } }
@media (max-width: 700px) {
  /* One card with a peek of the next, so it reads as swipeable. */
  .review-track { grid-auto-columns: 86%; gap: 0.9rem; }
  .review-carousel .review-card { padding: 1.2rem 1.2rem 1.1rem; }
}
@media (prefers-reduced-motion: reduce) { .review-track { scroll-behavior: auto; } }
/* Room below the cards so their shadow isn't clipped by the scroll box. */
.review-track { padding: 0.4rem 0.3rem 1.4rem; margin: -0.4rem -0.3rem -1.4rem; scroll-padding-inline: 0.3rem; }

/* ==========================================================================
   DARK THEME — <body class="flat bands dark"> (home page only, Sept 2026)
   Done by re-pointing the colour tokens inside the content sections rather
   than restyling each component: headings (--ink), body copy (--muted),
   hairlines (--line) and band surfaces (--paper / --mist) all flip together.
   The hero, the contact band and the footer were already dark and keep their
   own styling; the booking form card stays light on purpose so it is the
   brightest thing on the page where it matters.
   ========================================================================== */
body.dark {
  --dk-base: #07121F;   /* was white */
  --dk-alt:  #0B1B2E;   /* was mist */
  --dk-deep: #040A13;   /* the "Why 777" band, darkest so it still stands apart */
  background: var(--dk-base);
}
body.dark main > section:not(.hero):not(.cta-band) {
  --paper: var(--dk-base);
  --mist:  var(--dk-alt);
  --ink:   #F3F7FC;
  --muted: rgba(226, 236, 248, 0.68);
  --line:  rgba(255, 255, 255, 0.10);
  --azure-600: #5FAEFF;          /* links, eyebrows, icons: brighter blue reads on dark */
  --shadow-sm: 0 14px 34px -18px rgba(0, 0, 0, 0.7);
  background-color: var(--dk-base);
}
body.dark .eyebrow { color: var(--azure-600); }

/* Band fades re-coloured (the light versions blended into white). */
body.dark main > section:not([class*="band-"]):not(.cta-band):not(.hero):has(+ section.band-mist) {
  background: linear-gradient(180deg, var(--dk-base) calc(100% - var(--band-fade)), #09172A 100%);
}
body.dark main > section.band-dark {
  background:
    radial-gradient(60% 70% at 92% 18%, rgba(77,163,255,0.13) 0%, rgba(77,163,255,0) 62%),
    radial-gradient(50% 60% at 4% 92%, rgba(77,163,255,0.07) 0%, rgba(77,163,255,0) 60%),
    var(--dk-deep);
}
/* Waves redrawn in the dark neighbour colours. */
body.dark main > section.band-dark::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V22C1250 70 1010 90 760 70 520 50 300 20 0 52Z' fill='%2307121F'/%3E%3C/svg%3E");
}
body.dark main > section.band-dark::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0 90H1440V40C1180 12 940 4 680 26 440 46 220 70 0 44Z' fill='%230B1B2E'/%3E%3C/svg%3E");
}
body.dark main > section.cta-band::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V48C1160 24 900 22 660 44 420 66 200 78 0 30Z' fill='%2307121F'/%3E%3C/svg%3E");
}
/* On dark, a wave in a near-identical colour is invisible; a faint azure rim
   along its edge keeps the shape. */
body.dark main > section.band-dark::before,
body.dark main > section.band-dark::after,
body.dark main > section.cta-band::before { filter: drop-shadow(0 1px 0 rgba(95, 174, 255, 0.22)); }
body.dark main > section.band-dark::after { filter: drop-shadow(0 -1px 0 rgba(95, 174, 255, 0.22)); }

/* Components that carried their own light colours. */
body.dark .service-icon { background: linear-gradient(150deg, rgba(95,174,255,0.20), rgba(45,134,230,0.08)); color: #7DBDFF; }
body.dark .diff-item .tick { background: rgba(95,174,255,0.16); color: #7DBDFF; }
body.dark .service-card--accent {
  background: linear-gradient(150deg, #123054, #0C2240) !important;
  border: 1px solid rgba(95,174,255,0.22) !important;
}
body.dark .review-carousel .review-card {
  background: rgba(255, 255, 255, 0.035); border-color: rgba(255, 255, 255, 0.09);
}
body.dark .carousel-btn { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.16); color: #F3F7FC; }
body.dark .carousel-btn:hover:not(:disabled) { background: rgba(255,255,255,0.10); border-color: rgba(255,255,255,0.4); }
body.dark .carousel-dots button { background: rgba(255,255,255,0.22); }
body.dark .carousel-dots button[aria-current="true"] { background: #5FAEFF; }
body.dark .services-more .btn { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.22); color: #F3F7FC; }
body.dark .services-more .btn:hover { background: rgba(255,255,255,0.10); border-color: rgba(255,255,255,0.5); }
body.dark .area-map { box-shadow: 0 24px 60px -24px rgba(0,0,0,0.8); border: 1px solid rgba(255,255,255,0.08); }
/* Contact → footer: the footer is navy-950 already, keep the join seamless. */
body.dark .site-footer { background: var(--navy-950); }

/* ----- Black instead of white (home, Sept 2026) -----
   Everything that was white is now pure black; the blue surfaces (services,
   service area, Why 777) keep their navy. Re-pointing --dk-base carries the
   band fades and the waves' fill colour with it. */
body.dark { --dk-base: #000000; }
body.dark main > section.band-dark::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V22C1250 70 1010 90 760 70 520 50 300 20 0 52Z' fill='%23000000'/%3E%3C/svg%3E");
}
body.dark main > section.cta-band::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V48C1160 24 900 22 660 44 420 66 200 78 0 30Z' fill='%23000000'/%3E%3C/svg%3E");
}
/* Services now sits under a hero that ends in black: fade in from black so
   there is no seam, then back out to black at its foot. */
body.dark main > .hero + section.band-mist {
  background: linear-gradient(180deg,
    #000 0, var(--dk-alt) var(--band-fade),
    var(--dk-alt) calc(100% - var(--band-fade)), #000 100%);
}
/* ----- Hero: fade to black at the foot of the photo -----
   Only the lower part of the picture is touched; the fade reaches solid black
   exactly at the hero's bottom edge, so it runs straight into the black page. */
body.dark .hero:not(.hero-page) .hero-overlay {
  background: linear-gradient(180deg,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0) 56%,
    rgba(0,0,0,0.35) 70%, rgba(0,0,0,0.78) 86%, #000 100%);
}
/* Rating / service-area row moves down into the fade. The copy block is
   bottom-anchored, so the row drops by trimming the bottom padding, and the
   extra margin above it keeps the headline and buttons where they were. */
body.dark .hero:not(.hero-page) .hero-inner { --hero-pb: clamp(18px, 2.4vw, 30px); }
body.dark .hero:not(.hero-page) .hero-proof { margin-top: clamp(2.6rem, 6vh, 4.4rem); }
@media (max-width: 700px) {
  body.dark .hero:not(.hero-page) .hero-inner { --hero-pt: 96px; --hero-pb: 16px; }
  body.dark .hero:not(.hero-page) .hero-proof { margin-top: 1.9rem; }
  body.dark .hero:not(.hero-page) .hero-overlay {
    background: linear-gradient(180deg,
      rgba(0,0,0,0) 0%, rgba(0,0,0,0) 60%,
      rgba(0,0,0,0.4) 74%, rgba(0,0,0,0.82) 88%, #000 100%);
  }
}
/* "Why 777" was a navy band originally, so it stays blue rather than going
   near-black with the white areas. */
body.dark main > section.band-dark {
  background:
    radial-gradient(60% 70% at 92% 18%, rgba(77,163,255,0.16) 0%, rgba(77,163,255,0) 62%),
    radial-gradient(50% 60% at 4% 92%, rgba(77,163,255,0.09) 0%, rgba(77,163,255,0) 60%),
    linear-gradient(158deg, var(--navy-800), var(--navy-900));
}

/* Services, Reviews and Where we work are solid black; Why 777 keeps its navy
   and Contact keeps its photo and white booking card. */
body.dark main > section.band-mist,
body.dark main > .hero + section.band-mist { background: #000; }
body.dark main > section.band-dark::after {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 90' preserveAspectRatio='none'%3E%3Cpath d='M0 90H1440V40C1180 12 940 4 680 26 440 46 220 70 0 44Z' fill='%23000000'/%3E%3C/svg%3E");
}
/* Hero ends on a hard edge: no fade over the photo (Sept 2026). */
body.dark .hero:not(.hero-page) .hero-overlay { background: none; }
@media (max-width: 700px) { body.dark .hero:not(.hero-page) .hero-overlay { background: none; } }

/* ==========================================================================
   ABOUT — credentials band + closing CTA, rebuilt to the Sept 2026 reference
   ========================================================================== */
/* Handwritten notes. System script faces only (no font download): Bradley Hand
   on Apple devices, Segoe Script / Ink Free on Windows, Android's cursive. */
.script-note {
  position: absolute; z-index: 3; margin: 0; pointer-events: none;
  font-family: "Bradley Hand", "Segoe Script", "Ink Free", "Snell Roundhand", cursive;
  font-weight: 600; font-size: clamp(1.05rem, 1.7vw, 1.45rem); line-height: 1.25;
  color: rgba(255, 255, 255, 0.9); transform: rotate(-9deg); text-align: left;
}
.script-note .script-swash { display: block; width: 7.5em; height: 1.1em; margin-top: 0.15em; margin-left: 0.6em; color: var(--azure); }

/* Photo backdrop shared by both bands. */
.cred-band .band-media { position: absolute; inset: 0; z-index: 0; }
.cred-band .band-media picture, .cred-band .band-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
.cred-band .band-shade {
  position: absolute; inset: 0; z-index: 1;
  background:
    linear-gradient(90deg, rgba(8,21,37,0.92) 0%, rgba(8,21,37,0.78) 38%, rgba(8,21,37,0.45) 70%, rgba(8,21,37,0.55) 100%),
    linear-gradient(180deg, rgba(8,21,37,0.15) 0%, rgba(8,21,37,0.55) 100%);
}
.cred-band > .container { position: relative; z-index: 2; }
body.section-cards main > section.cred-band { background: var(--navy-900); }

.cred-script { top: clamp(1.4rem, 3vw, 2.4rem); right: clamp(1.5rem, 5vw, 4.5rem); }
.cred-grid { display: grid; grid-template-columns: 1fr; gap: clamp(1.8rem, 3vw, 2.6rem); align-items: end; }
@media (min-width: 900px) { .cred-grid { grid-template-columns: 1fr 1.05fr; } }
.cred-copy .section-title { margin-top: 1rem; }
.cred-lead { color: rgba(255,255,255,0.8); margin-top: 1rem; max-width: 46ch; font-size: 1rem; }
.google-pill {
  display: inline-flex; align-items: center; gap: 0.6rem; margin-top: 1.6rem;
  padding: 0.6rem 1.1rem 0.6rem 0.8rem; border-radius: 999px;
  border: 1px solid rgba(255,255,255,0.3); background: rgba(8,21,37,0.35);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  color: #fff; font-weight: 600; font-size: 0.92rem; text-decoration: none;
  transition: border-color 0.25s var(--ease), background 0.25s var(--ease);
}
.google-pill:hover { border-color: #fff; background: rgba(8,21,37,0.55); }
.google-pill .g-mark { width: 20px; height: 20px; flex: none; }
.google-pill > svg:last-child { width: 16px; height: 16px; }

.cred-panel {
  list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, 1fr);
  border-radius: var(--radius); border: 1px solid rgba(255,255,255,0.14);
  background: rgba(8, 21, 37, 0.45); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  box-shadow: 0 20px 50px -24px rgba(0,0,0,0.6);
}
.cred-panel li {
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.45rem;
  padding: clamp(1.2rem, 2.2vw, 1.8rem) clamp(0.7rem, 1.4vw, 1.2rem);
}
.cred-panel li + li { border-left: 1px solid rgba(255,255,255,0.12); }
.cred-panel svg { width: 28px; height: 28px; color: var(--azure); margin-bottom: 0.35rem; }
.cred-panel strong { font-family: var(--serif); font-weight: 600; font-size: 1.02rem; line-height: 1.2; color: #fff; }
.cred-panel span { font-size: 0.78rem; line-height: 1.45; color: rgba(255,255,255,0.62); }

/* Closing CTA: photo shows through, darker in the middle behind the copy. */
.about-cta .cta-overlay {
  background:
    radial-gradient(60% 90% at 50% 50%, rgba(8,21,37,0.72) 0%, rgba(8,21,37,0.5) 60%, rgba(8,21,37,0.35) 100%),
    linear-gradient(180deg, rgba(8,21,37,0.25), rgba(8,21,37,0.45));
}
.about-cta > .container { position: relative; }
.cta-script { right: clamp(0.5rem, 3vw, 2.5rem); bottom: clamp(-0.4rem, 0.5vw, 0.6rem); transform: rotate(-12deg); }
.about-cta .cta-actions .btn-ghost { background: rgba(8,21,37,0.3); }

@media (max-width: 899px) {
  .cred-script, .cta-script { display: none; }
}
@media (max-width: 700px) {
  .cred-panel { grid-template-columns: 1fr; }
  .cred-panel li { flex-direction: row; flex-wrap: wrap; text-align: left; align-items: center; column-gap: 0.8rem; row-gap: 0.1rem; padding: 0.9rem 1rem; }
  .cred-panel li + li { border-left: 0; border-top: 1px solid rgba(255,255,255,0.12); }
  .cred-panel svg { margin: 0; width: 24px; height: 24px; }
  .cred-panel strong br { display: none; }
  .cred-panel span { flex-basis: 100%; padding-left: calc(24px + 0.8rem); }
}
/* Reference tweaks: note clears the panel, mountain reads through, CTA headline on one line. */
.cred-script { top: clamp(0.9rem, 1.6vw, 1.4rem); }
.cred-band .band-shade {
  background:
    linear-gradient(90deg, rgba(8,21,37,0.92) 0%, rgba(8,21,37,0.74) 38%, rgba(8,21,37,0.30) 72%, rgba(8,21,37,0.40) 100%),
    linear-gradient(180deg, rgba(8,21,37,0.05) 0%, rgba(8,21,37,0.5) 100%);
}
.about-cta h2 { max-width: none; }
.about-cta .cta-actions { margin-top: 1.7rem; }
.about-cta .cta-actions .btn-lg { padding: 0.8em 1.7em; font-size: 0.98rem; }
/* Fireplace backdrop: anchor lower so the whole firebox shows, and let the
   sides (plant, fire) come through brighter than the centre copy area. */
.about-cta .cta-media img { object-position: 50% 80%; }
.about-cta .cta-overlay {
  background:
    radial-gradient(46% 80% at 50% 45%, rgba(8,21,37,0.74) 0%, rgba(8,21,37,0.5) 55%, rgba(8,21,37,0.12) 100%),
    linear-gradient(180deg, rgba(8,21,37,0.2), rgba(8,21,37,0.3));
}
.cta-script { bottom: auto; top: clamp(1rem, 2vw, 1.8rem); }
/* Note sits up in the band's top padding, clear of the headline and above the fire. */
.cta-script { top: clamp(-3.6rem, -4.5vw, -2rem); right: clamp(0rem, 1vw, 1rem); }

/* ----- Footer alignment (Sept 2026) -----
   The link lists never had the browser's default 40px list indent removed, so
   every column's links sat 40px right of its own heading, and on phones right
   of the logo. Zeroing it lines everything up on one edge per column. */
.footer-col ul { padding: 0; margin: 0; }
/* The first link's tap padding pushed it below the heading's rhythm; pull the
   list up by that amount so heading and first link read as a pair. */
@media (min-width: 761px) {
  .footer-col ul { margin-top: -0.5rem; }
  .footer-top { align-items: start; }
}
@media (min-width: 761px) {
  /* Plain-text rows (service area, hours) get the same pitch as the link rows. */
  .footer-col li > span { display: block; padding: 0.5rem 0; }
}

/* Hero fade, contained (Sept 2026): darkens the lower part of the photo behind
   the rating row but stops short of solid black, so the photo still ends on a
   visible edge instead of dissolving into the black section below. */
body.dark .hero:not(.hero-page) .hero-overlay {
  background: linear-gradient(180deg,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0) 58%,
    rgba(0,0,0,0.28) 76%, rgba(0,0,0,0.62) 100%);
}
@media (max-width: 700px) {
  body.dark .hero:not(.hero-page) .hero-overlay {
    background: linear-gradient(180deg,
      rgba(0,0,0,0) 0%, rgba(0,0,0,0) 60%,
      rgba(0,0,0,0.32) 78%, rgba(0,0,0,0.66) 100%);
  }
}
/* Hero fade deepened to 75% at the foot (still short of solid black). */
body.dark .hero:not(.hero-page) .hero-overlay {
  background: linear-gradient(180deg,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0) 58%,
    rgba(0,0,0,0.34) 76%, rgba(0,0,0,0.75) 100%);
}
@media (max-width: 700px) {
  body.dark .hero:not(.hero-page) .hero-overlay {
    background: linear-gradient(180deg,
      rgba(0,0,0,0) 0%, rgba(0,0,0,0) 60%,
      rgba(0,0,0,0.36) 78%, rgba(0,0,0,0.75) 100%);
  }
}
/* Hero fade at 90% at the foot (still short of solid black). */
body.dark .hero:not(.hero-page) .hero-overlay {
  background: linear-gradient(180deg,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0) 56%,
    rgba(0,0,0,0.42) 75%, rgba(0,0,0,0.90) 100%);
}
@media (max-width: 700px) {
  body.dark .hero:not(.hero-page) .hero-overlay {
    background: linear-gradient(180deg,
      rgba(0,0,0,0) 0%, rgba(0,0,0,0) 58%,
      rgba(0,0,0,0.44) 77%, rgba(0,0,0,0.90) 100%);
  }
}

/* ==========================================================================
   MOBILE PASS (Sept 2026) — touch targets, legible minimums, safe areas.
   ========================================================================== */
@media (max-width: 900px) {
  /* Grey flash on tap reads as a bug on a dark site. */
  a, button, summary, [role="button"] { -webkit-tap-highlight-color: rgba(77, 163, 255, 0.18); }
  /* Menu rows were 32px tall — under the 44px a thumb needs. */
  .nav-links a { display: flex; align-items: center; min-height: 44px; }
}
@media (max-width: 700px) {
  /* Footer links were 22–27px. Padding grows the hit box without moving the
     type, so the compact footer stays compact. */
  .footer-col li a { padding: 0.6rem 0; }
  .footer-col:has(a[href*="services.html#"]) a { padding: 0.5rem 0; }
  .footer-legal a, .footer-bottom .legal a { padding: 0.55rem 0; }
  /* Carousel dots stay 8px to look at, but carry a 44px touch box:
     background-clip keeps the paint inside the content box. */
  .carousel-dots { gap: 0; }
  .carousel-dots button {
    padding: 18px 8px; background-clip: content-box; box-sizing: content-box;
  }
  /* Nothing under ~13px: 12px body text on a phone is where people start
     pinch-zooming, and zoom breaks the fixed call bar. */
  .eyebrow { font-size: 0.82rem; }
  .review-name span, .quick-note, .form-note, .area-fill, .brands-line,
  .reviews-connect, .cred-panel span, .field-hint { font-size: 0.82rem; }
  .footer-bottom, .footer-legal a, .footer-bottom .legal a { font-size: 0.8rem; }
  /* Home-indicator clearance under the fixed call bar (viewport-fit=cover). */
  .site-footer { padding-bottom: calc(92px + env(safe-area-inset-bottom, 0px)); }
}
@media (max-width: 900px) {
  /* 44px minimum on the things a thumb aims at. Inline links inside running
     paragraphs are left alone on purpose — padding there would overlap the
     lines above and below, and WCAG exempts inline text links. */
  .site-header .brand { display: inline-flex; align-items: center; min-height: 44px; }
  .nav-phone { min-height: 44px; }
  .textlink, .reviews-connect a { display: inline-flex; align-items: center; min-height: 44px; }
  .cta-form .field select, .cta-form .field input { min-height: 44px; }
}
@media (max-width: 700px) {
  .footer-col li a { padding: 0.75rem 0; }
  .footer-col:has(a[href*="services.html#"]) a { padding: 0.75rem 0; }
  .footer-legal a, .footer-bottom .legal a { padding: 0.75rem 0; }
}

/* ----- Mobile menu was unclickable (Sept 2026) -----
   .site-header is a fixed, z-index:100 element, so it forms a stacking
   context: the drawer's own z-index:110 only ranks it against its siblings
   INSIDE the header, not against .nav-scrim (z-index:105), which is a sibling
   of the header itself. The scrim therefore painted over the open drawer and
   swallowed every tap on a menu link. Lifting the header above the scrim fixes
   it for the whole subtree; the scrim still covers the page and the call bar
   (z-index:90) beneath it. */
@media (max-width: 900px) {
  .site-header { z-index: 120; }
}

/* ----- Vertical hero photos on phones: services + about (Sept 2026) -----
   Both pages now ship a 9:16 cut of their own hero scene, so the portrait
   layout can fill the screen instead of showing a short letterboxed band.
   Scoped to portrait screens — landscape phones and tablets keep the wide
   frame they were designed around. */
@media (max-aspect-ratio: 1/1) and (max-width: 700px) {
  /* Services: drop the fixed 16:9 contain frame and behave like the home hero —
     the box takes the portrait photo's own height, capped at the screen. */
  .hero.hero-fit {
    aspect-ratio: auto; min-height: min(calc(100vw * 16 / 9), 100svh);
    align-items: flex-end;
  }
  .hero-fit .hero-media { background: none; }
  .hero-fit .hero-media img { object-fit: cover; object-position: center; }
  /* The vw-based type on this page was sized for a 180px-tall band. With a full
     screen of photo the copy goes back to the normal page-hero scale. */
  .hero-fit .hero-inner { --hero-pt: 104px; --hero-pb: clamp(28px, 6vw, 56px); padding-block: var(--hero-pt) var(--hero-pb); }
  .hero-fit .eyebrow { display: inline-flex; font-size: 0.82rem; }
  .hero-fit h1 { font-size: clamp(1.9rem, 8.4vw, 2.6rem); line-height: 1.08; margin-top: 0.9rem; }
  .hero-fit .lead { font-size: 1rem; line-height: 1.5; margin-top: 1rem; }
  /* About already covers; it just needs the taller box to show the new cut. */
  .hero.hero-page { min-height: min(calc(100vw * 16 / 9), 92svh); }
}

/* Service-area page: inline town lists read as chips, not a stack of bullets. */
.prose ul.area-inline { list-style: none; margin-left: 0; display: flex; flex-wrap: wrap; gap: 0.5rem 0.6rem; }
.prose ul.area-inline li {
  padding: 0.34rem 0.8rem; border-radius: 999px; border: 1px solid var(--line);
  background: var(--mist); font-size: 0.9rem;
}
.prose ul.area-inline li a { color: var(--azure-600); font-weight: 600; }
