/*
  Atlanta Metro Airflow, page-wide stylesheet.
  Loaded by site/index.html as the single global stylesheet for Phase 1.
  Imports design tokens, declares layer order, ships reset + base + layout primitives,
  the skip-link component, the global focus-visible ring, and the prefers-reduced-motion guard.

  Phase boundary notes:
    Phase 1 (this file): foundation only. NO nav visual styling, NO hero, NO cards.
    Phase 2 owns the floating pill nav and corner wordmark.
    Phase 3 through Phase 7 layer component styles either here (additive in the components layer)
    or in their own stylesheets that respect the layer order declared below.
*/

/* tokens.css loaded directly via <link> in index.html (v1.6.1: lets dev cache-bust apply to both stylesheets). */

/*
  v2.0: the body multi-stop gradient was removed in favor of full-viewport
  cloud-sky coverage. The @property --bg-angle / --bg-shift registrations
  that fed the old gradient drift are gone with it; nothing else animated
  via those custom properties.
*/

/* Cascade ordering, declared first so every later rule lands in the intended layer. */
@layer reset, base, layout, components, utilities;

/* =====================================================================
   RESET
   Minimal modern reset. Opts back into bullets and link colors per component.
   ===================================================================== */
@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
  }

  body {
    margin: 0;
  }

  img,
  svg,
  video {
    display: block;
    max-width: 100%;
    height: auto;
  }

  button,
  input,
  select,
  textarea {
    font: inherit;
    color: inherit;
  }

  ul,
  ol {
    padding-left: 0;
    margin: 0;
    list-style: none;
  }

  h1,
  h2,
  h3,
  h4,
  p {
    margin: 0;
  }

  a {
    color: inherit;
    text-decoration: none;
  }
}

/* =====================================================================
   BASE
   Body typography, headings, anchors honor nav scroll offset, focus rings.
   ===================================================================== */
@layer base {
  :root {
    /* v1.6: enables transitions between length keywords (e.g. height: 0 -> auto), */
    /* which is required for the FAQ smooth open/close. Chrome 129+, Safari 18.4+; */
    /* older browsers gracefully degrade to instant open/close. */
    interpolate-size: allow-keywords;
  }

  html {
    scroll-behavior: smooth;
    /* v2.0: bumped from var(--nav-height) to add buffer space so section */
    /* headings land cleanly below the floating pill nav, not snuggled up */
    /* against it. Total = 84 + 24 = 108px below the pill before content. */
    scroll-padding-top: calc(var(--nav-height) + var(--space-5));
    /* v2.0: soft sky-blue fallback so if WebGL fails (or while it boots) the */
    /* site does not render against a default white. Matches the cloud */
    /* shader's lighter horizon color so the swap is invisible. */
    /* v2.2: shifted to the logo's ice-blue family so the cool palette matches */
    /* the snowman lockup's surface tones. */
    background: oklch(91% 0.05 220);
  }

  body {
    font-family: var(--font-body);
    font-size: var(--type-body);
    line-height: var(--leading-normal);
    color: var(--text-primary);
    /* v2.0: transparent body so the fixed cloud-sky WebGL canvas covers the */
    /* full site, not just the hero. The prior body multi-stop gradient (sky */
    /* blue at top, cream middle, terracotta-red at bottom) and its */
    /* --bg-angle/--bg-shift drift were removed in this iteration. */
    background: transparent;
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  /* v1.9: Cloud-sky background canvas. Fixed behind everything else, covers */
  /* the top portion of the viewport so the body gradient (cream middle, */
  /* red bottom) shows through the lower half. Aria-hidden + pointer-events */
  /* none so it never interferes with user interaction. */
  #cloud-sky {
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    z-index: -1;
    pointer-events: none;
    display: block;
  }

  h1,
  h2,
  h3,
  h4 {
    font-family: var(--font-display);
    /* v1.6: Inter Tight 600 reads trade-brand confident; was Fraunces 400 (serif). */
    font-weight: 600;
    line-height: var(--leading-tight);
    letter-spacing: -0.02em;
    color: var(--text-primary);
  }

  h2 { font-size: var(--type-h2); }
  h3 { font-size: var(--type-h3); }
  h4 { font-size: var(--type-h4); }

  p {
    margin-block-end: var(--space-4);
  }

  section {
    /* v2.0: bumped from var(--nav-height) to add buffer space so section */
    /* headings land cleanly below the floating pill nav on anchor jumps, */
    /* not snuggled up against it. */
    scroll-margin-top: calc(var(--nav-height) + var(--space-5));
    padding-block: var(--space-7);
  }

  main {
    display: block;
  }

  /* Suppress the default focus outline; we deliver a custom one via :focus-visible. */
  :focus {
    outline: none;
  }

  /* Global focus-visible ring. outline + outline-offset trace pill curves correctly
     (POLISH-01 prep): the ring follows border-radius via the inherited radius below. */
  :focus-visible {
    outline: 2px solid var(--text-accent);
    outline-offset: 3px;
    border-radius: inherit;
  }

  .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;
  }

  /* v3.3: was an inline style attribute on the sprite <svg>; moved here so
     the production CSP can stay strict (style-src 'self', no unsafe-inline).
     The width/height 0 attributes on the element keep it layout-free even
     if CSS ever fails to load. */
  .svg-sprite {
    position: absolute;
  }
}

/* =====================================================================
   LAYOUT
   Container primitives. Components opt in by adding the class.
   ===================================================================== */
@layer layout {
  .container {
    max-width: var(--content-max);
    margin-inline: auto;
    padding-inline: var(--space-5);
  }

  .container--narrow {
    max-width: var(--content-narrow);
  }
}

/* =====================================================================
   COMPONENTS
   Phase 1 ships only: skip link, generic pill button base, placeholder text utility.
   Phase 2 layers nav styles here. Phase 3 plus layers cards, brands, service area.

   Safari 18 note: backdrop-filter values must be LITERAL (e.g. blur(12px)),
   never a CSS custom property (var(--blur-md) fails silently in Safari 18).
   Applies to any future Phase 2 nav frosted-glass styling layered into this file.
   ===================================================================== */
@layer components {
  /*
    v2.3: Mascot widget replaces the v1.6 .quick-call pill.
    Floats persistently in the bottom-right of the viewport. Two parts:
      .mascot-widget__bubble: flame-orange speech bubble (tel: link)
      .mascot-widget__figure: snowman SVG (href #quote anchor)

    The bubble has a CSS-painted tail pointing DOWN at the snowman. On
    hover of the snowman, the figure shifts subtly upward and slightly
    rotates, the meditative "the bear notices you" pattern from the
    Zen Air reference the owner sent.
  */
  .mascot-widget {
    position: fixed;
    right: var(--space-4);
    bottom: var(--space-4);
    z-index: 50;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* v2.6: gap bumped from 4px to 14px so the bubble's tail clears the
       snowman's head even at the peak of the snowman's idle float-up
       (-6px) and hover lift (-6px additional). Without breathing room
       the snowman would cover the tail mid-animation. */
    gap: 14px;
    pointer-events: none;  /* children opt back in */
  }

  .mascot-widget__bubble {
    pointer-events: auto;
    /* v2.6: explicit z-index so the bubble paints on top of the snowman
       figure whenever the snowman's hover/idle transforms bring it up
       into the bubble's footprint. The figure is a flex sibling that
       previously painted on top by virtue of source order; that order
       was visible to the owner as "bubble clipped by the moving
       snowman." Bumping bubble z and giving the figure a lower z
       guarantees the bubble always wins the overlap. */
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: var(--space-3) var(--space-5);
    background: var(--clay-button);
    color: var(--cream);
    text-decoration: none;
    border-radius: 18px;
    font-family: var(--font-body);
    text-align: center;
    box-shadow:
      0 10px 32px -14px color-mix(in oklch, var(--navy) 40%, transparent),
      0 1px 0 color-mix(in oklch, white 24%, transparent) inset;
    position: relative;
    transform-origin: bottom center;
  }
  .mascot-widget__bubble-label {
    font-weight: 700;
    font-size: 0.9375rem;
    line-height: 1.1;
    letter-spacing: 0;
  }
  .mascot-widget__bubble-sub {
    font-weight: 500;
    font-size: 0.75rem;
    letter-spacing: 0.04em;
    opacity: 0.9;
  }
  /*
    v2.4: down-pointing tail as a real CSS triangle.
    Previous rotated-square approach had a z-index: -1 trick (so the
    bubble's border-radius would mask the diamond's top edges) which
    sometimes painted the tail behind the page background, leaving
    the bubble visually clipped with no connection to the snowman.
    The classic transparent-border triangle has no overlap issue and
    paints reliably. Sits at top: 100% so it hugs the bubble's bottom.
  */
  .mascot-widget__bubble::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 50%;
    margin-left: -9px;
    width: 0;
    height: 0;
    border-left: 9px solid transparent;
    border-right: 9px solid transparent;
    border-top: 12px solid var(--clay-button);
    pointer-events: none;
  }

  .mascot-widget__figure {
    pointer-events: auto;
    /* v2.6: z-index 1 (below the bubble's z 2) so any overlap during
       hover/idle animations paints the bubble on top. */
    position: relative;
    z-index: 1;
    display: block;
    /* v2.6: aspect-ratio updated to match the new waving-snowman SVG
       (Group 1587, 230x294 = 0.782). Was 165:250 = 0.66 for the
       standing snowman. Width range unchanged. */
    width: clamp(90px, 11vw, 130px);
    aspect-ratio: 230 / 294;
    text-decoration: none;
    filter: drop-shadow(0 12px 28px color-mix(in oklch, var(--navy) 32%, transparent));
    transform-origin: bottom center;
  }
  /* v3.0: per-img sizing moved to .mascot-pose (two stacked pose imgs
     crossfade inside the figure; see the v3.0 components section). */

  @media (prefers-reduced-motion: no-preference) {
    /* Idle: bubble bobs gently. Snowman has a slow float. The two move
       at different rates so the widget reads as alive rather than rigid. */
    .mascot-widget__bubble {
      animation: mascot-bubble-bob 4.6s ease-in-out infinite;
      transition:
        transform 400ms var(--ease-out),
        box-shadow 400ms var(--ease-out);
    }
    .mascot-widget__figure {
      animation: mascot-figure-float 6s ease-in-out infinite;
      transition: transform 480ms var(--ease-out);
    }
    .mascot-widget__bubble:hover,
    .mascot-widget__bubble:focus-visible {
      animation-play-state: paused;
      transform: translateY(-4px) scale(1.02);
      box-shadow:
        0 14px 40px -14px color-mix(in oklch, var(--navy) 50%, transparent),
        0 1px 0 color-mix(in oklch, white 30%, transparent) inset;
    }
    .mascot-widget__figure:hover,
    .mascot-widget__figure:focus-visible {
      animation-play-state: paused;
      transform: translateY(-6px) rotate(-3deg);
    }
    @keyframes mascot-bubble-bob {
      0%, 100% { transform: translateY(0); }
      50%      { transform: translateY(-3px); }
    }
    @keyframes mascot-figure-float {
      0%, 100% { transform: translateY(0) rotate(0); }
      50%      { transform: translateY(-6px) rotate(-1.5deg); }
    }
  }

  /* Mobile: smaller widget, no sub-label on the bubble, tighter spacing. */
  @media (max-width: 480px) {
    .mascot-widget {
      right: var(--space-3);
      bottom: var(--space-3);
    }
    .mascot-widget__bubble {
      padding: var(--space-2) var(--space-4);
    }
    .mascot-widget__bubble-sub { display: none; }
    .mascot-widget__bubble-label { font-size: 0.875rem; }
    .mascot-widget__figure { width: 84px; }
  }

  /* v2.0: scroll-tracker step indicator. Fixed right edge, vertically */
  /* centered. Each bar represents one section; active bar expands taller */
  /* and fills as you scroll through that section's content. Hidden under */
  /* 720px to save room. JS in script.js (initScrollTracker) toggles */
  /* .is-active and sets the inline fill height. */
  .scroll-tracker {
    position: fixed;
    right: var(--space-4);
    top: 50%;
    transform: translateY(-50%);
    z-index: 40;
    pointer-events: none;
  }

  .scroll-tracker__list {
    list-style: none;
    margin: 0;
    padding: var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    background: color-mix(in oklch, var(--cream) 60%, transparent);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-radius: var(--radius-pill);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
  }

  .scroll-tracker__item {
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .scroll-tracker__bar {
    display: block;
    position: relative;
    width: 6px;
    height: 8px;
    border-radius: 3px;
    background: color-mix(in oklch, var(--ink) 18%, transparent);
    overflow: hidden;
  }

  .scroll-tracker__fill {
    display: block;
    position: absolute;
    inset: 0;
    height: 0%;
    background: var(--clay);
    border-radius: 3px;
  }

  @media (prefers-reduced-motion: no-preference) {
    .scroll-tracker__bar {
      transition: height 320ms cubic-bezier(0.22, 1, 0.36, 1);
    }
    .scroll-tracker__fill {
      transition: height 120ms linear;
    }
  }

  .scroll-tracker__item.is-active .scroll-tracker__bar {
    height: 40px;
  }

  /* Hide on small viewports to save room */
  @media (max-width: 720px) {
    .scroll-tracker { display: none; }
  }

  /*
    Skip-to-main link (D-08).
    Visually hidden by default (off-canvas via translateY), slides into the top-left
    on :focus as a clay-button pill with cream text. Uses --clay-button (not --clay)
    so cream text passes WCAG AA at body weight (5.5:1 vs 3.9:1 on plain --clay).
    Tap target meets 44px minimum via min-height.
  */
  .skip-link {
    position: absolute;
    top: var(--space-3);
    left: var(--space-3);
    z-index: 100;
    display: inline-flex;
    align-items: center;
    min-height: var(--tap-target-min);
    padding: var(--space-3) var(--space-5);
    border-radius: var(--radius-pill);
    background: var(--surface-clay);
    color: var(--text-on-clay);
    font-family: var(--font-body);
    font-size: var(--type-body);
    font-weight: 600;
    text-decoration: none;
    transform: translateY(-200%);
  }

  .skip-link:focus,
  .skip-link:focus-visible {
    transform: translateY(0);
  }

  /*
    Reserved pill button base. Currently consumed by 404.html's "Back to home" link.
    Phase 2 hero CTA will extend this base class.
  */
  .btn-pill {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap-target-min);
    padding: var(--space-3) var(--space-6);
    border: 0;
    border-radius: var(--radius-pill);
    background: var(--surface-clay);
    color: var(--text-on-clay);
    font-family: var(--font-body);
    font-size: var(--type-body);
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
  }

  /* v2.7: .placeholder removed. The Phase 1 section stubs it styled were
     all replaced with real markup by Phase 7; no consumers remain. */

  /*
    Floating pill nav (NAV-01, D-01..D-04). Fixed at top-center, frosted glass,
    backs the Phase 1 structural <nav aria-label="Primary"> markup. Backdrop
    filter values are literal: Safari 18 silently fails when blur() is given
    a CSS custom property as its value.
  */
  nav[aria-label="Primary"] {
    position: fixed;
    top: var(--space-3);
    left: 50%;
    transform: translateX(-50%);
    z-index: 90;
    width: calc(100% - var(--space-5) * 2);
    max-width: 640px;
    padding: var(--space-2) var(--space-4);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-radius: var(--radius-pill);
    background: color-mix(in oklch, var(--cream) 70%, transparent);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    box-shadow:
      0 1px 0 color-mix(in oklch, white 60%, transparent) inset,
      0 8px 24px -8px color-mix(in oklch, var(--ink) 20%, transparent);
  }

  nav[aria-label="Primary"] .nav-list {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  nav[aria-label="Primary"] .nav-list a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap-target-min);
    padding-block: var(--space-3);
    padding-inline: var(--space-3);
    font-family: var(--font-body);
    font-size: 0.9375rem; /* 15px desktop */
    font-weight: 500;
    color: var(--text-primary);
    opacity: 0.78;
    text-decoration: none;
    border-radius: var(--radius-pill);
    white-space: nowrap;
  }

  nav[aria-label="Primary"] .nav-list a:hover,
  nav[aria-label="Primary"] .nav-list a:focus-visible {
    opacity: 1;
  }

  /* Active section, hooked by Phase 1 scroll-spy (initScrollSpy sets aria-current). */
  nav[aria-label="Primary"] .nav-list a[aria-current="true"] {
    opacity: 1;
    font-weight: 600;
  }

  /*
    Corner wordmark (NAV-02, D-05..D-08). Stationary upper-left. Same backdrop
    blur as the pill, different radius (card, not pill) so it reads as a logo
    plate rather than a button. Composition is Buck Mason / Los Angeles:
    small tracked location above large dominant brand.
  */
  .wordmark {
    position: fixed;
    top: var(--space-3);
    left: var(--space-3);
    z-index: 91;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-2) var(--space-3);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-radius: var(--radius-card);
    background: color-mix(in oklch, var(--cream) 70%, transparent);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    box-shadow:
      0 1px 0 color-mix(in oklch, white 60%, transparent) inset,
      0 8px 24px -8px color-mix(in oklch, var(--ink) 20%, transparent);
    text-decoration: none;
    color: var(--text-primary);
  }

  .wordmark__img {
    display: block;
    /* v2.2: bumped to 52px to give the new flame-M + snowman lockup */
    /* presence (was 44px when the mark was just the wordmark + snowman tile). */
    height: 52px;
    width: auto;
    max-width: 100%;
  }

  /*
    Hero layout (HERO-01). Text-left, visual-right desktop grid; stacked mobile.
    min-height: 100svh with 100vh fallback (older browsers honor vh first; newer
    Safari iOS uses svh and avoids URL-bar height shift). Padding-top accounts
    for the floating pill nav set by Plan 02-01 (--nav-height = 84px).
  */
  #hero {
    /* Override the base section padding; hero owns its vertical rhythm. */
    padding-block: 0;
  }

  .hero {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-7);
    min-height: 100vh;
    min-height: 100svh;
    padding-block: calc(var(--nav-height) + var(--space-6)) var(--space-9);
    padding-inline: var(--space-5);
    max-width: var(--content-max);
    margin-inline: auto;
    /* v2.4: align-items shifted from center to start so the hero photo
       tracks the TOP of the text column (eyebrow line). Previous center
       alignment made the photo float visually mid-text-column, which
       read as "uncentered" once the text column gained the v2.4 logo
       lockup and grew taller. start-alignment keeps the photo and the
       eyebrow on the same horizontal line for a clean top edge. */
    align-items: start;
  }

  @media (min-width: 1024px) {
    .hero {
      grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
      gap: var(--space-8);
    }
  }

  .hero__text {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    max-width: 36rem;
  }

  .hero__eyebrow {
    margin: 0;
    font-family: var(--font-body);
    font-size: 0.75rem; /* 12px */
    font-weight: 500;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  /*
    v2.4: hero logo lockup. Decorative (the h1 below carries the brand
    semantically). Sized at clamp(180px, 22vw, 280px) so it scales with
    the viewport, big enough to read but not so big it competes with
    the headline. Negative-top margin pulls it tight under the eyebrow
    without disrupting the .hero__text flex gap.
  */
  .hero__logo {
    display: block;
    width: clamp(180px, 22vw, 280px);
    height: auto;
    margin: calc(var(--space-2) * -1) 0 calc(var(--space-2) * -1) 0;
  }

  .hero__headline {
    margin: 0;
    font-family: var(--font-display);
    /* v3.0: trimmed ~12% for Bricolage Grotesque, which runs wider than
       the Inter Tight it replaced (was clamp(5rem, 8.8vw, 6.5rem)). Keeps
       the headline at three lines in the 36rem text column. */
    font-size: clamp(4.25rem, 7.6vw, 5.6rem);
    font-weight: 650;
    line-height: 0.98;
    letter-spacing: -0.025em;
    color: var(--text-primary);
  }

  .hero__subhead {
    margin: 0;
    font-family: var(--font-body);
    font-size: 1.25rem; /* 20px */
    font-weight: 400;
    line-height: 1.45;
    color: var(--text-muted);
    max-width: 50ch;
  }

  /*
    Trust bar (TRUST-01). Three items separated by a 4px round dot in
    --border-strong. Items wrap to multiple lines on narrow screens; the dot
    sits between items via the ::before on each non-first li so wrapped
    rows do not inherit a dangling dot.
  */
  .hero__trust {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
    font-family: var(--font-body);
    font-size: 0.875rem; /* 14px */
    font-weight: 500;
    color: var(--text-muted);
  }

  .hero__trust li {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
  }

  .hero__trust li + li::before {
    content: "";
    display: inline-block;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--border-strong);
  }

  /*
    CTA cluster (CONV-06). One primary clay-button pill, secondary actions in
    a quieter text line below. Tap target floor 56px on the primary button
    (oversized vs the 44 minimum, reads confident).
  */
  .hero__cta {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin-top: var(--space-2);
  }

  .hero__cta-primary {
    align-self: flex-start;
    min-height: 56px;
    padding: var(--space-4) var(--space-6);
    font-family: var(--font-body);
    font-size: 1rem; /* 16px */
    font-weight: 600;
    letter-spacing: 0;
    background: var(--surface-clay);
    color: var(--text-on-clay);
    border: 0;
    border-radius: var(--radius-pill);
    cursor: pointer;
  }

  .hero__cta-secondary {
    margin: 0;
    font-family: var(--font-body);
    font-size: 0.9375rem; /* 15px */
    color: var(--text-muted);
  }

  .hero__cta-secondary a {
    color: var(--text-muted);
    text-decoration: none;
    border-bottom: 1px solid color-mix(in oklch, var(--text-muted) 30%, transparent);
  }

  .hero__cta-secondary a:hover,
  .hero__cta-secondary a:focus-visible {
    color: var(--text-primary);
    border-bottom-color: var(--text-primary);
  }

  /*
    Visual panel container (HERO-03, D-16 layer 1). The atmospheric base is a
    static three-stop radial gradient: a warm clay-tinted highlight in the
    upper-left fading through cream and settling into a slightly stonier linen
    at the lower-right. The gradient sells "warm interior light" without
    requiring real photography. Static by design. No keyframes, no animation,
    no WebGL. The subtle 1px inner border (D-17) defines the panel edge against
    the cream surface behind. Aspect ratio 4/5 on desktop relaxes to 5/4 on
    mobile so the stacked layout does not push the CTA off-screen.
  */
  .hero__visual {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius-card);
    aspect-ratio: 4 / 5;
    width: 100%;
    max-width: 36rem;
    justify-self: center;
    /* v1.2: top-of-page-aligned water-blue radial with a subtle warm */
    /* accent in the lower right. Uses oklch literals so the panel reads */
    /* as the same real sky-blue as the top of the body gradient (mixing */
    /* through warm cream previously produced a green undertone). */
    background:
      radial-gradient(
        ellipse 50% 60% at 78% 88%,
        oklch(85% 0.10 30) 0%,
        transparent 65%
      ),
      radial-gradient(
        ellipse 80% 100% at 28% 18%,
        oklch(86% 0.09 230) 0%,
        oklch(92% 0.05 228) 40%,
        oklch(96% 0.02 220) 75%,
        var(--cream) 100%
      );
    border: 1px solid color-mix(in oklch, var(--ink) 6%, transparent);
  }

  /*
    Atmospheric grain (HERO-03, D-16 layer 2). Inline SVG <feTurbulence>
    converted to a URL-encoded data URI (NOT base64; URL-encoded is smaller
    for SVG payloads). feColorMatrix tints the noise to a warm low-saturation
    brown so multiplying with cream darkens warmly instead of cooling it off.
    Static; no animation. No WebGL. No random decorative shapes. This is the
    only approved warmth mechanism (HANDOFF.md section 6 lists the rejections).
  */
  .hero__visual::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.15 0 0 0 0 0.13 0 0 0 0 0.11 0 0 0 0.5 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
    background-repeat: repeat;
    background-size: 200px 200px;
    opacity: 0.04;
    mix-blend-mode: multiply;
  }

  @media (max-width: 1023px) {
    .hero__visual {
      aspect-ratio: 5 / 4;
      max-width: 100%;
    }
  }

  .hero__picture,
  .hero__picture img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
  }

  /* Empty src: picture is invisible until a real photo lands post-launch. */
  .hero__picture img[src=""] {
    opacity: 0;
  }

  /* v2.3: .hero__mascot removed. The snowman moved to .mascot-widget */
  /* (defined above), a persistent floating widget anchored to the */
  /* bottom-right of the viewport with a flame-orange speech bubble */
  /* and a small float/bob animation. */

  /* =====================================================================
     SERVICES SECTION (Phase 3 plan 01)
     Three vertically stacked category groups (Cooling, Heating, Home
     Performance) per CONTEXT D-01. Each group has eyebrow + Fraunces h3 +
     thin clay divider per D-02, then a responsive card grid per D-03.
     Cards are informational only, no CTA inside (D-05), warm-cream surface
     with ink/muted text only (no clay backgrounds behind body text).
     ===================================================================== */

  .services-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-9);            /* 96px between category groups */
    padding-block: var(--space-6);  /* 32px breathing inside the section */
  }

  .service-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);            /* 32px between header and grid */
    margin: 0;                       /* reset <article> defaults */
    /* v1.6: per-category anchor jumps (#cooling / #heating / #home-performance) */
    /* land below the floating pill nav instead of being clipped beneath it. */
    /* v2.0: matched the section-level bump for consistent landing buffer. */
    scroll-margin-top: calc(var(--nav-height) + var(--space-5));
  }

  .service-group__header {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding-bottom: var(--space-5);
    /* Thin clay hairline below each group header (D-02). Clay at 30% mixed
       with transparent reads as a warm rule, not the saturated brand clay.
       No body text ever sits on this. */
    border-bottom: 1px solid color-mix(in oklch, var(--clay) 30%, transparent);
  }

  .service-group__eyebrow {
    margin: 0;
    font-family: var(--font-body);
    font-size: var(--type-micro);   /* 13px */
    font-weight: 500;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    /* muted-on-cream is 7.1:1 (AAA at body); clay-on-cream at 13px would sit
       below AA-large 4.9:1. Visible accent is the clay border-bottom under
       the h3, not the eyebrow color. */
    color: var(--text-muted);
  }

  .service-group__title {
    margin: 0;
    font-family: var(--font-display);
    /* v1.6: bumped 10% (2-3rem -> 2.2-3.3rem) for Inter Tight (narrower than Fraunces) */
    font-size: clamp(2.2rem, 4.4vw, 3.3rem);
    font-weight: 600;
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--text-primary);
  }

  .service-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));  /* D-03 */
    gap: var(--space-5);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .service-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);                       /* 12px between icon, title, desc */
    padding: var(--space-5) var(--space-6);    /* 24px vertical, 32px horizontal (D-03) */
    /* v1.6: frosted-glass surface unifies with the pill nav language. Cream */
    /* at 64% with the body gradient bleeding through; ink still passes AAA */
    /* against the translucent cream wash because the gradient stays soft. */
    background: color-mix(in oklch, var(--cream) 64%, transparent);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-radius: var(--radius-card);          /* 24px */
    /* No shadow at rest; shadow appears on hover inside the motion guard. */
  }

  .service-card__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: clamp(1.375rem, 2.2vw, 1.625rem);  /* 22-26px per D-04 */
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: -0.01em;
    color: var(--text-primary);
  }

  .service-card__desc {
    margin: 0;
    font-family: var(--font-body);
    font-size: var(--type-body);   /* 18px body floor (D-04, FOUND-04) */
    font-weight: 400;
    line-height: 1.55;
    /* muted-on-cream verified AAA at body in tokens.css. */
    color: var(--text-muted);
    max-width: 38ch;
  }

  .icon {
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    /* Clay stroke against cream. The sprite paths use stroke="currentColor"
       so this paints the icon clay. Decorative stroke only, no text on clay. */
    color: var(--text-accent);
  }

  /* =====================================================================
     PROCESS BLOCK (Phase 3 plan 02)
     "What happens when you text us" 3-step block per CONTEXT D-08, plus the
     pricing posture sentence per D-09. Sits inside #services > .container
     below the three category groups. Step cards mirror the service-card
     visual family (cream surface, ink border, 24px radius, hover lift) so
     the two card systems read as siblings. The ONLY clay background in the
     entire block is the 48px round numeral badge (.process-step__num),
     which holds 24px Fraunces 700 (large-text threshold) so cream-on-clay
     stays AA-large safe per the tokens.css contrast comment.
     ===================================================================== */

  .process-block {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);              /* 32px between header and step list */
    margin-top: var(--space-9);       /* 96px breathing above */
    padding-block: var(--space-7);    /* 48px top/bottom */
  }

  .process-block__header {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    max-width: 56ch;
    /* Intentionally no border-bottom; the process block stands alone visually,
       no divider needed (deliberate contrast with .service-group__header). */
  }

  .process-block__eyebrow {
    margin: 0;
    font-family: var(--font-body);
    font-size: var(--type-micro);     /* 13px */
    font-weight: 500;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    /* muted-on-cream is 7.1:1 (AAA at body) per tokens.css. */
    color: var(--text-muted);
  }

  .process-block__title {
    margin: 0;
    font-family: var(--font-display);
    /* v1.6: bumped 10% (2-3rem -> 2.2-3.3rem) for Inter Tight */
    font-size: clamp(2.2rem, 4.4vw, 3.3rem);
    font-weight: 600;
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--text-primary);
  }

  /*
    v3.0: the three numbered process cards are now a real SMS conversation.
    The thread is the page's strongest proof device: the entire brand is
    "a real text back", so the section SHOWS the text back instead of
    describing it. Customer messages sit right in navy (their own outgoing
    bubble, phone convention); our replies sit left on cream with a
    snowman avatar chip. The three concrete promises render as a pill row
    beneath the thread.
  */
  .sms-thread-wrap {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
    width: 100%;
    max-width: 640px;
    margin-inline: auto;
  }

  .sms-thread {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .sms-msg {
    max-width: 86%;
  }
  .sms-msg--out {
    align-self: flex-end;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: var(--space-1);
  }
  .sms-msg--in {
    align-self: flex-start;
    display: grid;
    grid-template-columns: 36px minmax(0, 1fr);
    gap: var(--space-3);
    align-items: end;
  }
  .sms-msg__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
  }

  .sms-msg__bubble {
    margin: 0;
    padding: var(--space-3) var(--space-4);
    border-radius: 18px;
    font-family: var(--font-body);
    font-size: 1.0625rem;
    line-height: 1.45;
    font-weight: 500;
  }
  .sms-msg--out .sms-msg__bubble {
    background: var(--navy);
    color: var(--ice);
    border-bottom-right-radius: 6px;
  }
  .sms-msg--in .sms-msg__bubble {
    background: color-mix(in oklch, white 65%, var(--cream));
    color: var(--text-primary);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-bottom-left-radius: 6px;
  }

  .sms-msg__meta {
    margin: 0;
    font-family: var(--font-body);
    font-size: 0.75rem;
    color: var(--text-muted);
    padding-inline: var(--space-2);
  }

  /* Snowman avatar chip. The standing-pose SVG is full body (165x250);
     object-position top + cover inside the 36px circle crops to the head. */
  .sms-msg__avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    overflow: hidden;
    background: var(--ice);
    border: 1px solid color-mix(in oklch, var(--ink) 10%, transparent);
    display: block;
    flex-shrink: 0;
  }
  .sms-msg__avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 50% 4%;
    transform: scale(1.5);
    transform-origin: 50% 12%;
  }
  .sms-msg__avatar--spacer {
    background: transparent;
    border: 0;
  }

  /* Bubble stagger: keyed off the parent .reveal's .is-visible so the
     conversation plays out in order as it scrolls into view. */
  .js-reveal .sms-thread .sms-msg {
    opacity: 0;
    transform: translateY(14px);
  }
  .js-reveal .sms-thread.is-visible .sms-msg {
    opacity: 1;
    transform: none;
  }
  @media (prefers-reduced-motion: no-preference) {
    .js-reveal .sms-thread.is-visible .sms-msg {
      transition:
        opacity 620ms cubic-bezier(0.22, 1, 0.36, 1),
        transform 620ms cubic-bezier(0.22, 1, 0.36, 1);
    }
    .js-reveal .sms-thread.is-visible .sms-msg:nth-child(1) { transition-delay: 0ms; }
    .js-reveal .sms-thread.is-visible .sms-msg:nth-child(2) { transition-delay: 380ms; }
    .js-reveal .sms-thread.is-visible .sms-msg:nth-child(3) { transition-delay: 760ms; }
    .js-reveal .sms-thread.is-visible .sms-msg:nth-child(4) { transition-delay: 1140ms; }
  }
  @media (prefers-reduced-motion: reduce) {
    .js-reveal .sms-thread .sms-msg {
      opacity: 1;
      transform: none;
      transition: none;
    }
  }

  .sms-promises {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .sms-promise {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 40px;
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-pill);
    background: color-mix(in oklch, var(--cream) 70%, transparent);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    font-family: var(--font-body);
    font-size: 0.9375rem;
    font-weight: 500;
    color: var(--text-primary);
  }
  .sms-promise .icon {
    width: 18px;
    height: 18px;
    color: var(--flame);
  }

  /* v2.7: .pricing-posture removed with its markup (owner request). The
     promise lives on in process step 03 and the quotes FAQ answer. */

  /* =====================================================================
     BRANDS GRID (Phase 4 plan 01)
     12-slot import-ready grid of brand tiles inside #brands. Each tile is a
     square cream surface with a centered placeholder label in Plus Jakarta
     Sans 500 muted uppercase. When real SVG logos arrive (Phase 8 / v1.x),
     the <span class="brand-tile__label"> is swapped for an <img> sized to
     max-height 50% / max-width 70% inside the same tile (D-02, D-03).

     Section header pattern (.section__eyebrow + .section__title +
     .section__lede) is fresh for Phase 4 but uses the same type sizes and
     color tokens as .service-group__* so the brands section reads as a
     sibling component.
     ===================================================================== */

  .section__eyebrow {
    margin: 0;
    font-family: var(--font-body);
    font-size: var(--type-micro);   /* 13px */
    font-weight: 500;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .section__title {
    margin: var(--space-3) 0 0 0;
    font-family: var(--font-display);
    /* v1.6: bumped 10% (2-3rem -> 2.2-3.3rem) for Inter Tight */
    font-size: clamp(2.2rem, 4.4vw, 3.3rem);
    font-weight: 600;
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--text-primary);
  }

  .section__lede {
    margin: var(--space-4) 0 0 0;
    max-width: 56ch;
    font-family: var(--font-body);
    font-size: var(--type-body);    /* 18px body floor */
    font-weight: 400;
    line-height: 1.55;
    color: var(--text-muted);
  }

  /*
    v1.6: brand grid is now an infinite right-to-left marquee. The track
    holds two identical 12-tile sets side by side; the keyframe translates
    -50% so the second set seamlessly replaces the first. Mask-image on the
    container fades both edges so tiles enter and exit softly rather than
    chopping at the container edge.
  */
  .brand-marquee {
    margin-top: var(--space-7);
    overflow: hidden;
    mask-image: linear-gradient(90deg, transparent 0%, black 8%, black 92%, transparent 100%);
    -webkit-mask-image: linear-gradient(90deg, transparent 0%, black 8%, black 92%, transparent 100%);
  }
  .brand-marquee__track {
    display: flex;
    gap: var(--space-4);
    width: max-content;
  }
  .brand-marquee__set {
    display: flex;
    gap: var(--space-4);
    list-style: none;
    margin: 0;
    padding: 0;
  }

  .brand-tile {
    /* v1.9: no background, no border, no glass. Just the logo floating in */
    /* the marquee. Width holds the slot size; height is auto so logos */
    /* center vertically. */
    flex-shrink: 0;
    width: 200px;
    height: 100px;
    aspect-ratio: unset;
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border: none;
    border-radius: 0;
    display: grid;
    place-items: center;
    padding: var(--space-3);
    overflow: hidden;
  }

  .brand-tile__img {
    /* v1.9: bigger logos since no frame, cleaner presence */
    max-width: 100%;
    max-height: 70px;
    width: auto;
    height: auto;
    object-fit: contain;
    display: block;
    /* Optional muted grayscale at rest, full color on marquee scroll. */
    /* If you prefer color always-on, remove the filter line. */
  }

  @media (prefers-reduced-motion: no-preference) {
    /* v1.6: marquee scroll. -50% lands the second set exactly where the */
    /* first started, producing a seamless loop. */
    /* v2.0: hover-pause removed; the marquee runs continuously with no */
    /* interaction by owner request. */
    @keyframes brand-scroll {
      from { transform: translateX(0); }
      to   { transform: translateX(calc(-50% - var(--space-2))); }
    }
    .brand-marquee__track {
      animation: brand-scroll 50s linear infinite;
    }
  }

  /* v2.7: .brand-tile__label removed. Real SVG logos shipped in v1.9; no
     text-label placeholder spans remain in the markup. */

  /* =====================================================================
     SERVICE AREA MAP (Phase 5, v1.3 redesign)
     Real OSM Atlanta basemap with a clay service-area polygon overlay on the
     left, neighborhood chevron list on the right. Stacks on mobile. Replaces
     the v1 stylized inline SVG approach with a recognisable map image so
     metro-Atlanta visitors get instant geographic context. Polygon SVG sits
     in absolute position above the image inside .service-area__map-frame.
     ===================================================================== */
  .service-area {
    display: grid;
    gap: var(--space-7);
    margin-top: var(--space-7);
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }
  @media (min-width: 1024px) {
    .service-area {
      grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.9fr);
      gap: var(--space-7);
    }
  }

  .service-area__map { margin: 0; }
  /* v1.4: keep the map in view while the visitor scrolls the neighborhood */
  /* list on desktop. The two columns are deliberately unbalanced (12 list */
  /* rows are taller than a 1:1 map), so anchoring the map removes the */
  /* awkward empty space below it. */
  @media (min-width: 1024px) {
    .service-area__map {
      position: sticky;
      top: calc(var(--nav-height) + var(--space-4));
    }
  }
  .service-area__map-frame {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    border-radius: var(--radius-card);
    overflow: hidden;
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    background: var(--surface);
  }
  .service-area__map-img,
  .service-area__overlay {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
  }
  .service-area__map-img { object-fit: cover; }
  .service-area__overlay { pointer-events: none; }
  .service-area__attribution {
    margin-top: var(--space-3);
    font-family: var(--font-body);
    font-size: 0.75rem;
    color: var(--text-muted);
  }
  .service-area__attribution a {
    color: inherit;
    text-decoration: underline;
  }

  /* Display heading: big italic-accent display. Reused across sections. */
  /* v1.6: Inter Tight 700 replaces Fraunces 600. Bumped 10% (2.5-4rem -> */
  /* 2.75-4.4rem) since Inter Tight is narrower than Fraunces, italic em is */
  /* now Inter italic 400 to preserve the calligraphic emphasis device. */
  /* v2.0: weight relationship inverted. Base text drops to 400 and the <em> */
  /* words become 700 italic so the accent words pop on both axes (weight */
  /* contrast plus italic) instead of italics alone. Applies sitewide. */
  .display-heading {
    font-family: var(--font-display);
    font-weight: 400;
    font-size: clamp(2.75rem, 5.5vw, 4.4rem);
    line-height: 1.04;
    letter-spacing: -0.03em;
    color: var(--text-primary);
    text-transform: none;
    margin: var(--space-4) 0 var(--space-6) 0;
  }
  .display-heading__line { display: block; }
  .display-heading em {
    font-family: var(--font-body);
    font-style: italic;
    font-weight: 700;
    color: var(--text-primary);
    letter-spacing: -0.02em;
  }

  /* v1.6: replaced the chevron-row neighborhood list with a flowing pill */
  /* cloud. Same 12 cities, more compact, rhymes with the pill nav and */
  /* quick-call. Each pill carries the frosted glass treatment so the */
  /* cards-and-buttons language stays unified across the page. */
  .neighborhood-pills {
    list-style: none;
    margin: 0 0 var(--space-5) 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
  }
  .neighborhood-pill {
    display: inline-flex;
    align-items: center;
    padding: var(--space-3) var(--space-4);
    background: color-mix(in oklch, var(--cream) 64%, transparent);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-radius: var(--radius-pill);
    color: var(--text-primary);
    text-decoration: none;
    font-family: var(--font-body);
    font-weight: 500;
    font-size: 0.9375rem;
    min-height: 40px;
  }
  @media (prefers-reduced-motion: no-preference) {
    .neighborhood-pill {
      transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
    }
    .neighborhood-pill:hover {
      /* v2.2: hover tint is --accent-soft-red (coral), not --flame (orange).
         The flame is the primary brand accent (buttons, headings); the city
         pills get a softer, distinct red so they read as a different
         interaction class from the call-to-action language. */
      transform: translateY(-2px);
      border-color: color-mix(in oklch, var(--accent-soft-red) 50%, transparent);
      background: color-mix(in oklch, var(--accent-soft-red) 12%, var(--cream));
    }
  }

  .service-area__disclaimer {
    font-family: var(--font-body);
    font-size: 0.9375rem;
    color: var(--text-muted);
    line-height: 1.55;
    max-width: 50ch;
  }

  /* =====================================================================
     ABOUT SECTION (Phase 6)
     Two-column layout per ABOUT-01/02/03: we-voice text block on the left
     (eyebrow + Fraunces section title + lede + body + credentials card),
     photo slot on the right with a 4/5 aspect-ratio gradient backdrop so
     the design holds whether or not a real Atlanta-context photo has been
     supplied. Stacks on mobile. Credentials card is the ONE cream-surface
     panel inside this block; body text never sits on clay.
     ===================================================================== */
  .about {
    display: grid;
    gap: var(--space-7);
    margin-top: var(--space-7);
    grid-template-columns: minmax(0, 1fr);
    /* v1.7: image no longer stretches to match text-column height; centered */
    /* alignment lets the photo stay compact on the right while the text owns */
    /* more of the column ratio. */
    align-items: center;
  }

  @media (min-width: 1024px) {
    .about {
      /* v1.7: text column wider, photo column narrower (was 1.2 / 1) so the */
      /* photo frame reads as a side accent, not a co-equal slab. */
      grid-template-columns: minmax(0, 1.45fr) minmax(0, 0.75fr);
      gap: var(--space-8);
    }
  }

  .about__lede {
    font-family: var(--font-body);
    font-weight: 500;
    font-size: 1.25rem;
    line-height: 1.5;
    color: var(--text-primary);
    margin-bottom: var(--space-5);
    max-width: 50ch;
  }

  .about__body {
    font-family: var(--font-body);
    font-size: 1.0625rem;
    line-height: 1.65;
    color: var(--text-muted);
    margin-bottom: var(--space-4);
    max-width: 60ch;
  }

  /* v2.3: about__credentials replaced by about__stats, a 4-up grid of
     proof points (15+, 12, 100%, 2hr) plus two wide rows for licensure
     and "how we work." Each tile is its own frosted glass card so the
     whole block reads as a scannable trust deck instead of a 3-line list. */
  .about__stats {
    list-style: none;
    margin: var(--space-6) 0 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3);
  }
  /* v2.7: wide tiles span the full 2-col row on mobile too. They previously
     only spanned at 640px+, which crammed the long LICENSED / HOW WE WORK
     captions into single half-width cells on phones. */
  .about__stat--wide { grid-column: span 2; }
  @media (min-width: 640px) {
    .about__stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  }
  @media (min-width: 1024px) {
    /* On wide desktops keep 4-up for the numeric tiles and span the two
       wide rows across all 4 columns each. */
    .about__stat--wide { grid-column: span 4; }
  }
  .about__stat {
    padding: var(--space-4) var(--space-5);
    background: color-mix(in oklch, var(--cream) 70%, transparent);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-radius: var(--radius-card);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }
  .about__stat-figure {
    font-family: var(--font-display);
    font-weight: 700;
    font-size: clamp(2rem, 3.4vw, 2.6rem);
    line-height: 1;
    color: var(--flame);
    letter-spacing: -0.02em;
    margin: 0;
  }
  .about__stat-tag {
    font-family: var(--font-body);
    font-weight: 700;
    font-size: 0.75rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--flame);
    margin: 0;
  }
  .about__stat-caption {
    font-family: var(--font-body);
    font-size: 0.9375rem;
    line-height: 1.45;
    color: var(--text-primary);
    margin: 0;
  }

  .about__signoff {
    margin: var(--space-6) 0 0 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4);
    font-family: var(--font-body);
  }
  .about__signoff-aside {
    font-size: 0.9375rem;
    color: var(--text-muted);
  }
  .about__signoff-aside a {
    color: var(--ink);
    text-decoration: underline;
    text-underline-offset: 3px;
  }

  .about__photo { margin: 0; }
  .about__photo-frame {
    /* v1.7: photo stays at its natural 4/5 ratio, capped at 380px wide and */
    /* centered in the narrower right column. Removed height: 100% and the */
    /* 400px min-height floor that were stretching the image too tall. */
    aspect-ratio: 4 / 5;
    max-width: 380px;
    margin-inline: auto;
    background:
      radial-gradient(
        ellipse 80% 100% at 70% 30%,
        color-mix(in oklch, var(--clay) 12%, var(--cream)) 0%,
        var(--cream) 60%,
        color-mix(in oklch, var(--linen) 75%, var(--cream)) 100%
      );
    border: 1px solid color-mix(in oklch, var(--ink) 6%, transparent);
    border-radius: var(--radius-card);
    position: relative;
    overflow: hidden;
  }
  .about__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }
  .about__img:not([src=""]):not([src*="data:"]) { z-index: 1; }

  .about__caption {
    margin-top: var(--space-3);
    font-family: var(--font-body);
    font-size: 0.8125rem;
    color: var(--text-muted);
    text-align: center;
  }

  /* =====================================================================
     QUOTE BUILDER + CONTACT (Phase 7, two-panel layout per HANDOFF §7)
     Desktop >=1024px: 2 columns, contact on the LEFT (~40%), form on the
     RIGHT (~60%). Mobile: stacked, contact above form. The form is a
     standard 2-column grid that collapses to 1 column below 640px. The
     full-width fields (textarea, actions, legal) span both columns via
     grid-column: 1 / -1.

     SMS preview <dialog> styling lives at the bottom of this block.
     Phase 1 shipped the dialog with no chrome; Phase 7 owns the final
     visual: cream surface, blurred backdrop, output text in a recessed
     card, action row of pill + bordered-text buttons.
     ===================================================================== */
  .quote {
    display: grid;
    gap: var(--space-7);
    margin-top: var(--space-7);
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }

  @media (min-width: 1024px) {
    .quote {
      grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
      gap: var(--space-8);
    }
  }

  .quote__contact { padding: 0; }
  .quote__intro {
    font-family: var(--font-body);
    font-size: 1.0625rem;
    line-height: 1.6;
    color: var(--text-muted);
    margin: var(--space-5) 0 var(--space-6) 0;
    max-width: 50ch;
  }

  .quote__info {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-5);
  }
  .quote__info-row {
    display: grid;
    grid-template-columns: 24px 1fr;
    gap: var(--space-4);
    align-items: start;
  }
  .quote__info-row .icon {
    width: 24px;
    height: 24px;
    color: var(--clay);
  }
  .quote__info-label {
    margin: 0 0 var(--space-1) 0;
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 0.8125rem;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
  }
  .quote__info-value {
    font-family: var(--font-body);
    font-size: 1rem;
    color: var(--text-primary);
    text-decoration: none;
  }
  a.quote__info-value:hover { text-decoration: underline; }

  .quote__form {
    /* v1.6: frosted glass matches site-wide card language. */
    background: color-mix(in oklch, var(--cream) 64%, transparent);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-radius: var(--radius-section);
    padding: var(--space-6);
    display: grid;
    gap: var(--space-5);
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
  @media (min-width: 1024px) {
    .quote__form { padding: var(--space-7); }
  }

  .form-field {
    display: grid;
    gap: var(--space-2);
  }
  .form-field--wide { grid-column: 1 / -1; }
  @media (max-width: 639px) {
    .quote__form { grid-template-columns: 1fr; }
    .form-field--wide { grid-column: auto; }
  }

  .form-field label {
    font-family: var(--font-body);
    font-weight: 500;
    font-size: 0.875rem;
    color: var(--text-primary);
  }
  .form-field input,
  .form-field select,
  .form-field textarea {
    width: 100%;
    padding: var(--space-3) var(--space-4);
    min-height: 44px;
    font-family: var(--font-body);
    font-size: 1rem;
    color: var(--text-primary);
    background: var(--cream);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-card);
    box-shadow: none;
  }
  .form-field textarea {
    min-height: 96px;
    resize: vertical;
    line-height: 1.5;
  }
  /* v1.7: native <select> arrow indicator crowded the right edge. Strip the */
  /* UA chrome with appearance: none, then paint our own warm-ink chevron via */
  /* an inline data-URI SVG positioned in the right gutter, with extra */
  /* padding-right so user-typed text never collides with the chevron. */
  .form-field select {
    padding-right: var(--space-7);
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1l5 5 5-5' stroke='%23262220' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-repeat: no-repeat;
    background-position: right var(--space-4) center;
    appearance: none;
    -webkit-appearance: none;
  }
  .form-field input:focus-visible,
  .form-field select:focus-visible,
  .form-field textarea:focus-visible {
    outline: 2px solid var(--text-accent);
    outline-offset: 2px;
    border-color: var(--clay);
  }
  .form-field [aria-invalid="true"] {
    border-color: var(--clay);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--clay) 14%, transparent);
  }

  .quote__actions {
    grid-column: 1 / -1;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
    margin-top: var(--space-3);
  }
  .btn-pill--quiet {
    background: transparent;
    color: var(--text-primary);
    border: 1px solid var(--border-subtle);
    font-family: var(--font-body);
    font-weight: 500;
    font-size: 1rem;
    padding: var(--space-4) var(--space-6);
    min-height: 56px;
    border-radius: var(--radius-pill);
    cursor: pointer;
  }
  .btn-pill--quiet:hover { border-color: color-mix(in oklch, var(--ink) 24%, transparent); }

  .quote__legal {
    grid-column: 1 / -1;
    margin: var(--space-2) 0 0 0;
    font-family: var(--font-body);
    font-size: 0.8125rem;
    color: var(--text-muted);
    line-height: 1.55;
  }

  /*
    SMS preview dialog (Phase 1 markup, Phase 7 chrome). The native <dialog>
    must NOT have display:none in custom CSS (research §4): the UA stylesheet
    handles dialog visibility via the open attribute. We style the open state
    only. ::backdrop is inert and styled separately. backdrop-filter values
    are literal blur(4px), not CSS custom properties, due to Safari 18 bug.
  */
  #sms-preview {
    position: relative;
    border: none;
    border-radius: var(--radius-card);
    padding: var(--space-6);
    max-width: 540px;
    width: calc(100% - var(--space-6));
    background: var(--cream);
    color: var(--text-primary);
    box-shadow: 0 24px 60px -20px color-mix(in oklch, var(--ink) 40%, transparent);
  }
  #sms-preview::backdrop {
    background: color-mix(in oklch, var(--ink) 60%, transparent);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
  }
  #sms-preview h2 { margin: 0 0 var(--space-3) 0; font-family: var(--font-display); font-size: 1.5rem; }
  #sms-preview output {
    display: block;
    white-space: pre-wrap;
    font-family: var(--font-body);
    font-size: 0.9375rem;
    line-height: 1.5;
    color: var(--text-primary);
    background: var(--surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-card);
    padding: var(--space-4);
    margin: var(--space-3) 0;
    max-height: 50vh;
    overflow: auto;
  }
  .sms-preview__close {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    margin: 0;
  }
  .sms-preview__close button {
    background: transparent;
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
    color: var(--text-muted);
    /* WCAG AA tap target floor is 44x44 (POLISH-01). Phase 8 audit raised the
       previous 36x36 to the minimum so keyboard and touch hit the same target. */
    width: var(--tap-target-min);
    height: var(--tap-target-min);
    min-width: var(--tap-target-min);
    min-height: var(--tap-target-min);
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
  }
  .sms-preview__close button:hover { background: var(--surface); color: var(--text-primary); }
  .sms-preview__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-4);
  }
  .sms-preview__actions .btn-pill {
    background: var(--surface-clay);
    color: var(--text-on-clay);
    text-decoration: none;
    border: none;
    padding: var(--space-3) var(--space-5);
    border-radius: var(--radius-pill);
    font-family: var(--font-body);
    font-weight: 600;
    cursor: pointer;
    /* WCAG AA tap-target floor (POLISH-01); padding alone left these ~42px. */
    min-height: var(--tap-target-min);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .sms-preview__actions a:not(.btn-pill),
  .sms-preview__actions button:not(.btn-pill) {
    background: transparent;
    border: 1px solid var(--border-subtle);
    color: var(--text-primary);
    padding: var(--space-3) var(--space-5);
    border-radius: var(--radius-pill);
    text-decoration: none;
    cursor: pointer;
    font-family: var(--font-body);
    /* WCAG AA tap-target floor (POLISH-01); same fix as the primary .btn-pill above. */
    min-height: var(--tap-target-min);
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* =====================================================================
     SITE FOOTER (v1.6 redesign)
     Three-zone composition over the body gradient's terracotta endpoint:
       1. Hero zone: eyebrow + big display CTA + cream pill button
       2. 3-column block: Contact NAP, Navigate, Service area
       3. Legal: small inline wordmark and copyright
     All text is cream because the deep red endpoint of the body gradient
     bleeds through the transparent footer surface. The wordmark--footer
     variant clears the fixed positioning so the brand mark can render
     inline in the legal row.
     ===================================================================== */
  .site-footer {
    margin-top: 0;
    background: transparent;
    border-top: none;
    padding-block: var(--space-9) var(--space-6);
    color: var(--ink);
  }

  .site-footer__hero {
    text-align: center;
    margin-bottom: var(--space-9);
    max-width: 720px;
    margin-inline: auto;
  }
  .site-footer__eyebrow {
    font-family: var(--font-body);
    font-weight: 500;
    font-size: 0.8125rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    /* v2.2: footer now sits on the cloud-sky (was the deep red gradient end).
       Eyebrow shifted to flame-on-ice for brand cohesion. */
    color: var(--flame);
    margin: 0 0 var(--space-4) 0;
  }
  .site-footer__big-cta {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: clamp(2rem, 4vw, 3rem);
    line-height: 1.1;
    color: var(--ink);
    margin: 0 0 var(--space-6) 0;
    letter-spacing: -0.02em;
  }
  .site-footer__cta {
    background: var(--cream);
    color: var(--ink);
  }
  /* v1.9: site-footer__cta background flip removed; ink-fill ::before handles it. */

  .site-footer__cols {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-6);
    margin-bottom: var(--space-8);
    padding-block: var(--space-7);
    /* v2.2: dividers tinted navy (footer on cloud-sky, not the old red gradient). */
    border-top: 1px solid color-mix(in oklch, var(--ink) 16%, transparent);
    border-bottom: 1px solid color-mix(in oklch, var(--ink) 16%, transparent);
  }
  @media (min-width: 768px) {
    .site-footer__cols { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-7); }
  }

  .site-footer__col-title {
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    /* v2.2: column titles use the same muted-on-cream pattern as the rest of the site. */
    color: var(--muted);
    margin: 0 0 var(--space-4) 0;
  }
  .site-footer__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-3);
    font-family: var(--font-body);
  }
  .site-footer__list a,
  .site-footer__list a:visited {
    color: var(--ink);
    text-decoration: none;
    font-weight: 500;
  }
  .site-footer__list a:hover {
    text-decoration: underline;
  }
  .site-footer__license {
    font-size: 0.875rem;
    color: var(--muted);
  }
  .site-footer__area-text {
    font-family: var(--font-body);
    font-size: 0.9375rem;
    line-height: 1.6;
    color: var(--ink);
    margin: 0;
  }

  .site-footer__legal {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    align-items: center;
    text-align: center;
    font-family: var(--font-body);
    font-size: 0.8125rem;
    color: var(--muted);
  }
  /* v2.3: --centered modifier keeps the legal row in column flex on all
     viewport widths so the lockup sits literally centered at the bottom
     of the page, with the copyright line stacked beneath it. Without
     this override the 640px+ media query below would push them apart
     into a row. */
  .site-footer__legal--centered {
    flex-direction: column;
    gap: var(--space-4);
    margin-top: var(--space-6);
  }
  .site-footer__legal--centered .site-footer__copyright {
    font-size: 0.8125rem;
    color: var(--muted);
    letter-spacing: 0.04em;
  }
  @media (min-width: 640px) {
    .site-footer__legal:not(.site-footer__legal--centered) {
      flex-direction: row;
      justify-content: space-between;
    }
  }
  .site-footer__legal p { margin: 0; }

  /*
    Footer-variant wordmark. Clears the base .wordmark fixed positioning so
    the same brand block can render inline inside the legal row. Cream-tone
    text replaces the cream-on-cream base treatment since the footer sits on
    the deep terracotta gradient endpoint.
  */
  .wordmark--footer {
    position: static;
    top: auto;
    left: auto;
    z-index: auto;
    display: block;
    flex-direction: initial;
    gap: 0;
    padding: 0;
    border: 0;
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    box-shadow: none;
    margin: 0;
  }
  .wordmark--footer .wordmark__img {
    /* v2.2: bumped from 56px to 110px because Group 1581 lockup is the */
    /* big horizontal mark (wordmark + snowman + ATLANTA caption). Footer */
    /* should feel like a definitive brand signoff, not a small icon. */
    height: 110px;
    width: auto;
    /* v2.2: footer no longer has a dark gradient background (the cloud-sky */
    /* canvas covers the whole site); the lockup's own navy + orange + ice */
    /* palette reads cleanly without any filter. */
    filter: none;
  }
  @media (max-width: 640px) {
    .wordmark--footer .wordmark__img {
      height: 84px;
    }
  }

  /* =====================================================================
     SCROLL-DRIVEN REVEAL (v1.2)
     Below-fold content blocks tagged .reveal fade up when an
     IntersectionObserver in site/js/script.js adds .is-visible. JS-driven
     instead of CSS animation-timeline because Safari 18 ships the latter
     with intermittent triggering (elements past the entry threshold on
     initial load sometimes never fire). IntersectionObserver is 97%+
     supported and behaves identically across modern browsers.

     Resting state (opacity 0, translateY 28px) is unguarded so the
     observer always has a starting state to transition from. The
     transition itself is wrapped in prefers-reduced-motion: no-preference
     so reduce-motion users skip the transition entirely; the reduce
     branch below also force-collapses opacity/transform to the final
     state so unrevealed nodes never get stuck invisible.
     ===================================================================== */
  /* Scroll-driven reveal: JS-armed pattern. Content is visible by default. */
  /* When JS confirms it can handle the reveal (sets .js-reveal on <html>), */
  /* .reveal becomes hidden and then revealed via IntersectionObserver. */
  /* If JS fails for any reason, .reveal elements stay visible (no FOUC, */
  /* no permanently-hidden content). */
  .reveal { opacity: 1; transform: none; }

  /* v1.7: even bigger travel (48 to 72px), scale-in from 0.98, and longer */
  /* duration (900 to 1100ms) so the scroll reveal is unmistakably felt. */
  .js-reveal .reveal {
    opacity: 0;
    transform: translateY(72px) scale(0.98);
  }
  .js-reveal .reveal.is-visible {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
  @media (prefers-reduced-motion: no-preference) {
    .js-reveal .reveal {
      transition:
        opacity 1100ms cubic-bezier(0.22, 1, 0.36, 1),
        transform 1100ms cubic-bezier(0.22, 1, 0.36, 1);
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .js-reveal .reveal {
      opacity: 1;
      transform: none;
      transition: none;
    }
  }

  /* =====================================================================
     HERO LOAD-STAGGER (v1.2)
     Hero non-LCP children fade up on first paint so the page visibly
     "comes to life" instead of popping in fully formed. The hero
     headline (.hero__headline, the <h1>) is the LCP element and is
     intentionally NOT in this list; it stays at opacity 1 from the very
     first paint so the LCP timing is unaffected.

     Wrapped in prefers-reduced-motion: no-preference so reduce-motion
     users get the still page with no animation; the global reduce
     branch at the bottom of this file collapses any leaked animation
     duration to ~0ms as a safety net.
     ===================================================================== */
  @media (prefers-reduced-motion: no-preference) {
    /* v1.7: travel pushed to 40px and duration to 1000ms so the hero load-in */
    /* reads as a clear staged entrance, not a quick fade. */
    .hero__eyebrow,
    .hero__subhead,
    .hero__trust,
    .hero__cta,
    .hero__visual {
      opacity: 0;
      transform: translateY(40px);
      animation: hero-load-fade 1000ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
    }
    .hero__eyebrow      { animation-delay:  80ms; }
    .hero__subhead      { animation-delay: 260ms; }
    .hero__trust        { animation-delay: 400ms; }
    .hero__cta          { animation-delay: 540ms; }
    .hero__visual       { animation-delay: 220ms; }
    @keyframes hero-load-fade {
      from { opacity: 0; transform: translateY(40px); }
      to   { opacity: 1; transform: translateY(0); }
    }
  }

  /* =====================================================================
     CATEGORIES SECTION (v1.3 new)
     Four image-style cards between hero and services as a visual TL;DR
     of what we do. Three category cards (Cooling, Heating, Home performance)
     each anchor to #services for the detailed catalog; a fourth dark anchor
     card carries a quote-builder CTA.
     ===================================================================== */
  #categories {
    padding-block: var(--space-7);
  }
  .display-heading--center {
    text-align: center;
    max-width: 22ch;
    margin-inline: auto;
  }

  .category-grid {
    margin-top: var(--space-7);
    display: grid;
    gap: var(--space-4);
    grid-template-columns: minmax(0, 1fr);
  }
  @media (min-width: 640px) {
    .category-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }
  @media (min-width: 1024px) {
    .category-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  }

  .category-card {
    position: relative;
    aspect-ratio: 3 / 4;
    border-radius: var(--radius-card);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-3);
    padding: var(--space-5);
    padding-bottom: var(--space-6);
    text-decoration: none;
    color: var(--text-on-clay);
    border: 1px solid color-mix(in oklch, var(--ink) 6%, transparent);
  }

  /*
    v2.3: category-card photo layer is now grayscale across all three
    cards. The v2.2 color-tinted overlays (ice/navy/flame per card)
    were dropped because the owner felt the commercial card read as
    "too blue" and preferred the residential look (which appeared
    desaturated under its overlay). The new treatment is photo →
    grayscale filter → uniform dark-navy gradient overlay → cream label
    + badge. Card identity comes from the icon and caption, not the
    photo tint.
  */
  .category-card__photo {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
    z-index: 0;
    filter: grayscale(1) contrast(1.04);
  }

  .category-card__bg {
    position: absolute;
    inset: 0;
    z-index: 1;
    /* v2.3: uniform dark-navy overlay across all three cards. Top is
       lightly veiled; bottom is heavy so the cream label reads at AA
       contrast against any photo. */
    background:
      linear-gradient(180deg,
        color-mix(in oklch, var(--navy) 12%, transparent) 0%,
        color-mix(in oklch, var(--navy) 30%, transparent) 50%,
        color-mix(in oklch, var(--navy) 78%, transparent) 100%);
  }
  /* v2.3: per-card .category-card--cool / --warm / --neutral overrides
     are removed; all three cards share the same overlay. The category
     modifiers stay on the element in case future iterations want to
     reintroduce per-card tinting. */

  .category-card__badge {
    /* In-flow positioning. Was absolutely positioned and overlapping the label (v1.4 fix). */
    position: relative;
    z-index: 2;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--cream);
    color: var(--ink);
    display: grid;
    place-items: center;
    box-shadow: 0 4px 12px -4px color-mix(in oklch, var(--ink) 35%, transparent);
  }

  .category-card__label {
    position: relative;
    z-index: 2;
    margin: 0; /* removed margin-top hack; gap on the flex parent handles spacing */
    text-align: center;
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 1rem;
    color: var(--cream);
    letter-spacing: 0;
  }

  /* Dark anchor card (4th slot) */
  .category-card--dark {
    /* Override the centered badge+label flex layout for the cta-copy + cta-btn pair. */
    align-items: stretch;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-6);
    background: var(--navy); /* v2.2: logo navy (was warm ink). Matches the wordmark color and pulls the dark anchor card into the flame palette. */
    color: var(--cream);
    text-align: left;
  }
  .category-card__cta-copy {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: clamp(1.375rem, 2.2vw, 1.65rem);
    line-height: 1.25;
    letter-spacing: -0.02em;
    color: var(--cream);
    margin: 0;
  }
  .category-card__cta-btn {
    align-self: flex-start;
    background: transparent;
    color: var(--cream);
    border: 1px solid color-mix(in oklch, var(--cream) 70%, transparent);
    padding: var(--space-3) var(--space-5);
    border-radius: var(--radius-pill);
    text-decoration: none;
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 0.9375rem;
    min-height: 48px;
    display: inline-flex;
    align-items: center;
  }

  @media (prefers-reduced-motion: no-preference) {
    /* v2.3: hover feel slowed and softened. Was 220ms ease, felt snappy.
       Now 540ms with a long ease-out tail so the lift glides. Also added
       a subtle scale (1.012) so the card "breathes" instead of just
       sliding up. Photo gets a gentle saturation lift on hover too,
       hinting at the underlying color the grayscale filter is hiding. */
    .category-card {
      transition:
        transform 540ms cubic-bezier(0.22, 1, 0.36, 1),
        box-shadow 540ms cubic-bezier(0.22, 1, 0.36, 1);
    }
    .category-card:hover {
      transform: translateY(-6px) scale(1.012);
      box-shadow: 0 18px 36px -16px color-mix(in oklch, var(--navy) 35%, transparent);
    }
    .category-card__photo {
      transition: filter 540ms cubic-bezier(0.22, 1, 0.36, 1),
                  transform 700ms cubic-bezier(0.22, 1, 0.36, 1);
    }
    .category-card:hover .category-card__photo {
      filter: grayscale(0.35) contrast(1.04);
      transform: scale(1.04);
    }
    .category-card__cta-btn { transition: border-color 360ms cubic-bezier(0.22, 1, 0.36, 1); }
    .category-card__cta-btn:hover {
      /* v1.9: background flip removed; ink-fill ::before handles it. */
      border-color: var(--ink);
    }
  }

  /* =====================================================================
     FAQ (v1.3 new)
     Native <details>/<summary> collapsible Q&A list. Chevron rotates on
     open via [open] selector. Sits inside .container--narrow to keep
     line length comfortable. All motion guarded by prefers-reduced-motion.
     ===================================================================== */
  #faq { padding-block: var(--space-7); }

  .display-heading--narrow {
    /* v1.6: bumped 10% (2-3rem -> 2.2-3.3rem) for Inter Tight */
    font-size: clamp(2.2rem, 4.4vw, 3.3rem);
    text-transform: none;
    max-width: 28ch;
    margin: var(--space-4) 0 var(--space-7) 0;
  }
  .display-heading--narrow em {
    text-transform: none;
  }

  .faq-list {
    /* v1.6: removed the gap:1px + parent-bg divider trick so the per-item */
    /* frosted glass shows through. Items separate via their own borders + gap. */
    display: grid;
    gap: var(--space-3);
    background: transparent;
    border-radius: 0;
    overflow: visible;
  }
  .faq-item {
    background: color-mix(in oklch, var(--cream) 64%, transparent);
    backdrop-filter: blur(12px) saturate(140%);
    -webkit-backdrop-filter: blur(12px) saturate(140%);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-radius: var(--radius-card);
  }
  .faq-item__q {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-5) var(--space-5);
    cursor: pointer;
    font-family: var(--font-body);
    font-weight: 600;
    font-size: 1.0625rem;
    color: var(--text-primary);
    list-style: none;
    min-height: 56px;
  }
  .faq-item__q::-webkit-details-marker { display: none; }
  .faq-item__chev {
    flex-shrink: 0;
    color: var(--text-muted);
  }
  /* v1.7: FAQ smooth open AND close via the ::details-content pseudo with */
  /* transition-behavior: allow-discrete. The prior v1.6 approach (height: 0 */
  /* on .faq-item__a) animated open but snapped closed because removing */
  /* [open] flips display: none on the content discretely. ::details-content */
  /* + allow-discrete lets both directions tween. Modern Chrome 131+ and */
  /* Safari 18.4+; older browsers fall through the @supports gate to instant */
  /* open/close, no FOUC. */
  @supports (transition-behavior: allow-discrete) {
    .faq-item {
      interpolate-size: allow-keywords;
    }
    .faq-item::details-content {
      overflow: hidden;
      height: 0;
      opacity: 0;
      transition:
        height 360ms cubic-bezier(0.22, 1, 0.36, 1),
        opacity 200ms ease,
        padding 360ms cubic-bezier(0.22, 1, 0.36, 1),
        content-visibility 360ms allow-discrete;
    }
    .faq-item[open]::details-content {
      height: auto;
      opacity: 1;
    }
  }

  /* Answer body styling remains on .faq-item__a so padding, type, and color */
  /* are consistent whether or not ::details-content support is present. */
  .faq-item__a {
    padding: 0 var(--space-5) var(--space-5);
    font-family: var(--font-body);
    font-size: 1rem;
    line-height: 1.6;
    color: var(--text-muted);
  }
  .faq-item__a p {
    margin: 0;
    max-width: 60ch;
  }
  @media (prefers-reduced-motion: no-preference) {
    .faq-item__chev {
      transition: transform 180ms ease, color 180ms ease;
    }
    .faq-item[open] .faq-item__chev {
      transform: rotate(180deg);
      color: var(--clay);
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .faq-item[open] .faq-item__chev {
      transform: rotate(180deg);
    }
  }

  /* =====================================================================
     v3.0 COMPONENTS
     The "site that texts back" layer: seasonal badge, headline underline,
     hero SMS teaser, editorial word band, live quote composer, footer
     ghost wordmark, two-pose mascot.
     ===================================================================== */

  /*
    Seasonal copy switching. season.js sets html[data-season] pre-paint.
    All variants ship in the DOM; only the matching one displays. The
    shoulder variant doubles as the no-JS fallback via :not([data-season]).
    v3.1: the hero seasonal badge was removed (owner call); the footer
    headline below is now the only seasonal copy surface. The engine also
    still drives the WebGL sky temperature in clouds.js.
  */
  .site-footer__big-line {
    display: none;
  }
  html[data-season="cooling"]  .site-footer__big-line[data-season-show="cooling"],
  html[data-season="heating"]  .site-footer__big-line[data-season-show="heating"],
  html[data-season="shoulder"] .site-footer__big-line[data-season-show="shoulder"],
  html:not([data-season])      .site-footer__big-line[data-season-show="shoulder"] {
    display: block;
  }

  /* v3.1: .hl-stroke flame underline removed (owner: felt corny). */

  /*
    Hero SMS teaser. A small frosted conversation card pinned to the
    photo's lower-left corner. The typing indicator and the reply share
    grid row 2, so when the dots hand off to the reply the card's height
    never jumps. All entrance animations live in the no-preference branch
    with fill-mode both; the base state is fully visible, so reduced
    motion (and no-JS) shows the finished conversation, minus the dots.
  */
  .sms-teaser {
    position: absolute;
    left: var(--space-4);
    bottom: var(--space-4);
    width: min(310px, calc(100% - var(--space-4) * 2));
    display: grid;
    gap: var(--space-2);
    padding: var(--space-4);
    border-radius: 20px;
    background: color-mix(in oklch, var(--cream) 80%, transparent);
    backdrop-filter: blur(14px) saturate(150%);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    box-shadow: 0 18px 44px -18px color-mix(in oklch, var(--navy) 38%, transparent);
  }
  .sms-teaser__msg {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border-radius: 14px;
    font-family: var(--font-body);
    font-size: 0.9375rem;
    line-height: 1.4;
    font-weight: 500;
    max-width: 94%;
  }
  .sms-teaser__msg--out {
    grid-row: 1;
    grid-column: 1;
    justify-self: end;
    background: var(--navy);
    color: var(--ice);
    border-bottom-right-radius: 5px;
  }
  .sms-teaser__msg--in {
    grid-row: 2;
    grid-column: 1;
    justify-self: start;
    background: color-mix(in oklch, white 65%, var(--cream));
    color: var(--text-primary);
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
    border-bottom-left-radius: 5px;
  }
  .sms-teaser__typing {
    grid-row: 2;
    grid-column: 1;
    justify-self: start;
    align-self: start;
    display: none; /* shown only inside the no-preference motion branch */
    gap: 4px;
    align-items: center;
    margin: 0;
    padding: var(--space-3) var(--space-3);
    border-radius: 14px;
    border-bottom-left-radius: 5px;
    background: color-mix(in oklch, white 65%, var(--cream));
    border: 1px solid color-mix(in oklch, var(--ink) 8%, transparent);
  }
  .sms-teaser__typing span {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: color-mix(in oklch, var(--ink) 38%, transparent);
  }
  .sms-teaser__meta {
    grid-row: 3;
    grid-column: 1;
    margin: 0;
    padding-inline: var(--space-1);
    font-family: var(--font-body);
    font-size: 0.71rem;
    color: var(--text-muted);
  }

  @media (prefers-reduced-motion: no-preference) {
    .sms-teaser__msg--out {
      animation: teaser-in 540ms cubic-bezier(0.22, 1, 0.36, 1) 700ms both;
    }
    .sms-teaser__typing {
      display: inline-flex;
      animation: teaser-typing-window 1900ms linear 1300ms both;
    }
    .sms-teaser__typing span {
      animation: teaser-dot 900ms ease-in-out infinite;
    }
    .sms-teaser__typing span:nth-child(2) { animation-delay: 150ms; }
    .sms-teaser__typing span:nth-child(3) { animation-delay: 300ms; }
    .sms-teaser__msg--in {
      animation: teaser-in 540ms cubic-bezier(0.22, 1, 0.36, 1) 3150ms both;
    }
    .sms-teaser__meta {
      animation: teaser-in 540ms cubic-bezier(0.22, 1, 0.36, 1) 3450ms both;
    }
    @keyframes teaser-in {
      from { opacity: 0; transform: translateY(10px); }
      to   { opacity: 1; transform: none; }
    }
    @keyframes teaser-typing-window {
      0%       { opacity: 0; transform: translateY(10px); }
      10%      { opacity: 1; transform: none; }
      88%      { opacity: 1; }
      100%     { opacity: 0; }
    }
    @keyframes teaser-dot {
      0%, 100% { transform: translateY(0);    opacity: 0.5; }
      50%      { transform: translateY(-3px); opacity: 1; }
    }
  }

  /*
    Editorial word band. Outlined service words drift right-to-left in a
    masked strip between the hero and the categories grid. Same duplicate
    set + translate(-50%) loop as the brand marquee.
  */
  .word-band {
    overflow: hidden;
    padding-block: var(--space-4);
    mask-image: linear-gradient(90deg, transparent 0%, black 7%, black 93%, transparent 100%);
    -webkit-mask-image: linear-gradient(90deg, transparent 0%, black 7%, black 93%, transparent 100%);
  }
  .word-band__track {
    display: flex;
    gap: var(--space-6);
    width: max-content;
  }
  .word-band__set {
    display: flex;
    align-items: center;
    gap: var(--space-6);
    margin: 0;
    /* v3.1: Archivo Black, solid fill at low opacity. The v3.0 outline
       stroke treatment read as broken type to the owner; solid chunky
       caps at 16% navy read as a deliberate editorial watermark. */
    font-family: 'Archivo Black', 'Bricolage Grotesque', sans-serif;
    font-weight: 400; /* Archivo Black ships one weight; 400 maps to it */
    font-size: clamp(1.9rem, 4.2vw, 3.2rem);
    letter-spacing: 0.01em;
    text-transform: uppercase;
    white-space: nowrap;
    color: color-mix(in oklch, var(--navy) 16%, transparent);
  }
  .word-band__accent {
    color: color-mix(in oklch, var(--flame) 78%, transparent);
  }
  .word-band__dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: color-mix(in oklch, var(--navy) 28%, transparent);
    flex-shrink: 0;
  }
  @media (prefers-reduced-motion: no-preference) {
    @keyframes word-band-scroll {
      from { transform: translateX(0); }
      to   { transform: translateX(calc(-50% - var(--space-4))); }
    }
    .word-band__track {
      animation: word-band-scroll 46s linear infinite;
    }
  }

  /* v3.1: live quote composer preview removed (owner: not needed). The
     SMS preview dialog is the single compose-review surface again. */

  /* v3.2: .site-footer__ghost removed by owner request (with the
     position/overflow wrapper rules that existed only to crop it).
     Archivo Black stays loaded for the word band. */

  /*
    Two-pose mascot. Standing is the idle pose; the waving pose flashes in
    on a 12s cycle (and instantly on hover/focus). Reduced motion shows
    the waving pose statically, which has been the owner's chosen pose
    since v2.6.
  */
  .mascot-pose {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    object-position: bottom center;
    display: block;
    pointer-events: none;
  }
  .mascot-pose--stand { opacity: 0; }
  .mascot-pose--wave  { opacity: 1; }
  @media (prefers-reduced-motion: no-preference) {
    .mascot-pose--stand {
      opacity: 1;
      animation: mascot-stand-window 12s linear infinite;
    }
    .mascot-pose--wave {
      opacity: 0;
      animation: mascot-wave-peek 12s linear infinite;
    }
    @keyframes mascot-wave-peek {
      0%, 80%       { opacity: 0; }
      84%, 96%      { opacity: 1; }
      100%          { opacity: 0; }
    }
    @keyframes mascot-stand-window {
      0%, 80%       { opacity: 1; }
      84%, 96%      { opacity: 0; }
      100%          { opacity: 1; }
    }
    .mascot-widget__figure:hover .mascot-pose--wave,
    .mascot-widget__figure:focus-visible .mascot-pose--wave {
      opacity: 1;
      animation: none;
    }
    .mascot-widget__figure:hover .mascot-pose--stand,
    .mascot-widget__figure:focus-visible .mascot-pose--stand {
      opacity: 0;
      animation: none;
    }
  }

  /* v2.0: Origin Button hover (replaces v1.9.3 inset box-shadow ink-fill). */
  /* Cursor entry position becomes the origin of a circular bloom that scales */
  /* from 0 to 1 with a subtle spring; reverses on exit from the exit cursor */
  /* position. JS (initOriginButtons) wraps inline text in .btn-label so the */
  /* text rides above the bloom via z-index, and sets --origin-x / --origin-y */
  /* custom properties on mouseenter / mouseleave. */
  .btn-pill,
  .site-footer__cta,
  .category-card__cta-btn {
    position: relative;
    overflow: hidden;
    isolation: isolate;
    --origin-x: 50%;
    --origin-y: 100%;
  }
  /* v2.3: .quick-call removed (replaced by .mascot-widget which uses */
  /* a different hover language, a float-and-rotate, not a circular */
  /* bloom). The Origin Button selectors below no longer include */
  /* .quick-call. */

  .btn-pill::before,
  .site-footer__cta::before,
  .category-card__cta-btn::before {
    content: "";
    position: absolute;
    left: var(--origin-x);
    top: var(--origin-y);
    width: 600px;
    height: 600px;
    border-radius: 50%;
    transform: translate(-50%, -50%) scale(0);
    transform-origin: center;
    pointer-events: none;
    z-index: 0;
  }

  /* Text / icon wrapper sits above the bloom via z-index 1. JS wraps any */
  /* inline children of these buttons in a .btn-label span on init. */
  .btn-label {
    position: relative;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    gap: inherit;
  }

  /* Per-button bloom colors (v2.2: flame-button blooms to pure navy, not a
     navy-flame oklch mix, because that mix passes through a muddy magenta
     midpoint visible during the scale transition). */
  .btn-pill--primary::before,
  .hero__cta-primary::before,
  .site-footer__cta::before {
    background: var(--navy);
  }
  .btn-pill--quiet::before {
    background: var(--surface-clay);
  }
  .category-card__cta-btn::before {
    background: var(--cream);
  }

  @media (prefers-reduced-motion: no-preference) {
    .btn-pill::before,
    .site-footer__cta::before,
    .category-card__cta-btn::before {
      /* v2.3: hover feel softened. Was 360ms ease-out. Bumped to 520ms
         with a longer ease-out tail so the bloom glides instead of
         snapping. Matches the new category-card 540ms transform timing
         so the page-wide hover language is consistent. */
      transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1);
    }
    .btn-pill,
    .site-footer__cta,
    .category-card__cta-btn {
      /* v2.3: color/border crossfades match the bloom timing so the text
         color flip happens with the bloom, not after it. */
      transition: color 520ms cubic-bezier(0.22, 1, 0.36, 1),
                  border-color 520ms cubic-bezier(0.22, 1, 0.36, 1);
    }

    .btn-pill:hover::before,
    .btn-pill:focus-visible::before,
    .site-footer__cta:hover::before,
    .site-footer__cta:focus-visible::before,
    .category-card__cta-btn:hover::before,
    .category-card__cta-btn:focus-visible::before {
      transform: translate(-50%, -50%) scale(1);
    }

    /* Text color flips on hover for contrast against the bloom. */
    .btn-pill--quiet:hover,
    .btn-pill--quiet:focus-visible {
      color: var(--text-on-clay);
      border-color: var(--surface-clay);
    }
    .category-card__cta-btn:hover,
    .category-card__cta-btn:focus-visible {
      color: var(--ink);
      border-color: var(--ink);
    }
    /*
      v2.3: site-footer CTA text was invisible on hover because the
      button base is cream + ink-text, the bloom paints navy on top, and
      no text-color flip existed. The text-color was sitting at ink
      (navy-ish) over a navy bloom = invisible. Flip to cream on hover
      so the label reads against the bloom.
    */
    .site-footer__cta:hover,
    .site-footer__cta:focus-visible {
      color: var(--cream);
    }
    /* v2.3: .quick-call hover rules removed (widget replaced). */
  }
}

/* =====================================================================
   UTILITIES
   Single-purpose helpers. Kept intentionally small at Phase 1.
   ===================================================================== */
@layer utilities {
  .text-muted { color: var(--text-muted); }
  .text-accent { color: var(--text-accent); }
}

/* =====================================================================
   MOBILE-FIRST OVERRIDES (v2.7)
   Most visitors reach this site from the QR code on the truck, so the
   phone rendering is the canonical experience, not a degraded desktop.
   This section is deliberately OUTSIDE the @layer blocks: unlayered
   styles win over layered ones, so these overrides beat the component
   definitions above without specificity games.

   Breakpoints: ≤1023px is where the centered nav pill began colliding
   with the corner wordmark (tablets included). ≤640px is the
   single-column phone layout. ≤380px is small phones.
   ===================================================================== */

/* ---- Header: compact wordmark + right-anchored nav (≤1023px) ----
   The desktop composition centers the nav pill (max-width 640px). Below
   ~975px the pill's left edge ran underneath the fixed corner wordmark
   and buried the Services link. The pill now anchors to the right edge
   and shrinks to fit its links; the wordmark compacts. They never meet. */
@media (max-width: 1023px) {
  .wordmark {
    padding: var(--space-1) var(--space-2);
  }
  .wordmark__img {
    /* v3.1: 44px everywhere below 1024px (was 40 tablet / 32 phone).
       The owner flagged the phone logo as undersized next to the nav
       pill; a 44px logo + 4px pill padding lands both pills at ~54px
       tall so they read as one header system. */
    height: 44px;
  }
  nav[aria-label="Primary"] {
    left: auto;
    right: var(--space-3);
    transform: none;
    width: auto;
    max-width: none;
    padding: var(--space-1) var(--space-2);
  }
  nav[aria-label="Primary"] .nav-list a {
    font-size: 0.875rem; /* 14px */
    padding-inline: var(--space-2);
  }
}

@media (max-width: 640px) {
  /* v3.1: Brands, Map, AND About drop out of the pill on phones (About
     used to survive until 380px; the bigger 44px logo needs the room).
     Services / Quote / Call remain: catalog, money, phone. Everything
     else stays reachable by scroll and the footer Navigate column.
     Link order in index.html: 1 Services, 2 Brands, 3 Map, 4 About,
     5 Quote, 6 Call. Keep nth-child indexes in sync if links reorder. */
  nav[aria-label="Primary"] .nav-list li:nth-child(2),
  nav[aria-label="Primary"] .nav-list li:nth-child(3),
  nav[aria-label="Primary"] .nav-list li:nth-child(4) { display: none; }
}

@media (max-width: 360px) {
  /* Tiny phones: compact slightly so all three links still clear the logo. */
  .wordmark__img { height: 38px; }
  nav[aria-label="Primary"] .nav-list a { font-size: 0.8125rem; }
}

/* ---- Hero ---- */
@media (max-width: 1023px) {
  /* The 100svh floor exists for the desktop two-column composition. In
     the stacked mobile layout it only inserted dead space between the
     CTA and the photo. Natural height instead. */
  .hero {
    min-height: auto;
    /* v3.0: +8px over v2.7 so the seasonal badge clears the nav pill. */
    padding-block: calc(var(--nav-height) + var(--space-6)) var(--space-7);
  }
}
@media (max-width: 640px) {
  .hero { gap: var(--space-6); }
  .hero__headline {
    /* The desktop clamp() floor is 5rem = 80px, which rendered four
       enormous lines on a 390px phone. 12vw is ~47px at 390px. */
    font-size: clamp(2.75rem, 12vw, 4rem);
  }
  .hero__subhead { font-size: 1.125rem; }
  .hero__logo { width: clamp(160px, 44vw, 220px); }
  .hero__cta-primary {
    align-self: stretch;
    width: 100%;
  }
}

/* ---- Section rhythm ----
   Desktop sections breathe at 48-96px scales. On a phone those gaps
   read as blank screens between content. Tighter vertical rhythm.
   :not(#hero) because #hero owns its own padding (layered rules would
   otherwise lose to this unlayered one regardless of specificity). */
@media (max-width: 640px) {
  section:not(#hero) { padding-block: var(--space-6); }
  .services-section { gap: var(--space-7); padding-block: var(--space-3); }
  .process-block { margin-top: var(--space-7); padding-block: var(--space-3); }
  .service-grid { gap: var(--space-4); }
  .category-grid { margin-top: var(--space-6); }
  .brand-marquee { margin-top: var(--space-6); }
  .service-area,
  .about,
  .quote { margin-top: var(--space-5); gap: var(--space-6); }

  /* Display typography: the desktop floors (35-44px) broke the two-line
     heading compositions at phone width. Scale down, keep the shape. */
  .display-heading { font-size: clamp(2.1rem, 9vw, 2.75rem); }
  .display-heading--narrow,
  .section__title,
  .service-group__title,
  .process-block__title { font-size: clamp(1.9rem, 8.2vw, 2.2rem); }
  .display-heading--narrow { margin-bottom: var(--space-6); }

  .quote__form { padding: var(--space-5); }
  .quote__actions { flex-direction: column; align-items: stretch; }
  .about__signoff-cta { width: 100%; justify-content: center; }

  .brand-tile { width: 156px; height: 84px; }
  .brand-tile__img { max-height: 56px; }

  /* v3.0 components at phone width */
  .sms-teaser {
    left: var(--space-3);
    bottom: var(--space-3);
    width: min(280px, calc(100% - var(--space-3) * 2));
    padding: var(--space-3);
  }
  .sms-teaser__msg { font-size: 0.875rem; }
  .sms-msg { max-width: 94%; }
  .sms-msg__bubble { font-size: 1rem; }
  .word-band { padding-block: var(--space-3); }
}

/* ---- Category cards ---- */
@media (max-width: 639px) {
  /* The 3/4 portrait ratio is a four-across desktop grid decision; in
     the single-column phone layout it produced ~460px-tall cards, and
     the dark CTA card rendered as a huge empty navy slab (heading
     pinned top, button pinned bottom, dead space between). Photo cards
     crop to a 16/10 landscape band; the dark card takes its natural
     content height. */
  .category-card { aspect-ratio: 16 / 10; }
  .category-card--dark {
    aspect-ratio: auto;
    justify-content: flex-start;
    gap: var(--space-5);
  }
  .category-card__cta-btn {
    align-self: stretch;
    justify-content: center;
  }
}

/* ---- Footer ---- */
@media (max-width: 640px) {
  .site-footer { padding-block: var(--space-7) var(--space-5); }
  .site-footer__hero { margin-bottom: var(--space-7); }
  /* The hard <br> was tuned for desktop line lengths; at phone width it
     stacked with natural wraps into ragged four-line output. */
  .site-footer__big-cta br { display: none; }
  .site-footer__big-cta { font-size: clamp(1.75rem, 7.6vw, 2.25rem); }
  .site-footer__cols {
    margin-bottom: var(--space-6);
    padding-block: var(--space-5);
    gap: var(--space-5);
  }
  .site-footer__legal--centered { margin-top: var(--space-4); }
}

/* =====================================================================
   MOTION GUARDS
   All animation and transition rules are declared inside the
   prefers-reduced-motion: no-preference branch so the default state respects
   reduced-motion. The reduce branch below also force-collapses any third-party
   or future-component motion to ~0ms as a safety net.
   ===================================================================== */
@media (prefers-reduced-motion: no-preference) {
  /* v2.0: bg-drift keyframes (and the body animation that consumed them) */
  /* were removed alongside the body gradient. The cloud-sky WebGL canvas */
  /* is now the only ambient motion behind content. */

  .skip-link {
    transition: transform var(--motion-base) var(--ease-out);
  }

  .btn-pill {
    transition:
      background-color var(--motion-base) var(--ease-out),
      transform var(--motion-fast) var(--ease-out);
  }

  /* v1.9: background change removed; ink-fill ::before handles the hover visual. */
  /* Keep the rule slot empty so this is a deliberate intent, not a missing rule. */

  nav[aria-label="Primary"] .nav-list a {
    transition: opacity var(--motion-fast) var(--ease-out);
  }

  .wordmark {
    transition: transform var(--motion-base) var(--ease-out);
  }

  .wordmark:hover {
    transform: scale(1.02);
  }

  .hero__cta-primary {
    transition:
      background-color var(--motion-base) var(--ease-out),
      transform var(--motion-fast) var(--ease-out);
  }

  .hero__cta-primary:hover {
    /* v1.9: background-color flip removed; ink-fill ::before handles it. */
    transform: translateY(-1px);
  }

  /*
    Service cards (Phase 3 D-05). Subtle lift plus shadow intensification on
    hover; no color flip. Cards remain warm-cream surface throughout. Border
    darkens slightly to anchor the card edge against the deeper shadow.
    focus-within mirrors hover border so keyboard tab order is visible.
  */
  .service-card {
    transition:
      transform var(--motion-base) var(--ease-out),
      box-shadow var(--motion-base) var(--ease-out),
      border-color var(--motion-base) var(--ease-out);
  }

  .service-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 14px -10px color-mix(in oklch, var(--ink) 20%, transparent);
    border-color: color-mix(in oklch, var(--ink) 12%, transparent);
  }

  .service-card:focus-within {
    border-color: color-mix(in oklch, var(--ink) 18%, transparent);
  }

  /* v3.0: .process-step hover rules removed with the numbered cards;
     the SMS thread that replaced them animates via its own stagger. */

  /*
    Brand tiles (v1.9): no hover interaction. Logos float in the marquee
    with no frame, no background, no lift. Owner request to drop the
    "this is alive" hover cue in favor of pure presence.
  */

}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .skip-link {
    transition: none;
  }
}
