/* ==========================================================================
   Color Transformations Painting — stylesheet entrypoint
   --------------------------------------------------------------------------
   This is the ONLY <link rel="stylesheet"> on a page.
   Import order is load-bearing: tokens -> base -> layout -> components ->
   utilities. Utilities are last so they win without !important.

   Stage 2: this import list becomes the theme's enqueue order. tokens.css
   maps onto theme.json (settings.color.palette / settings.typography).
   ========================================================================== */


/* ---- fonts.css ---- */
/*
  Self-hosted Archivo and Instrument Serif. Both are SIL Open Font License,
  which permits redistribution — the licence text ships in assets/fonts/.

  Generated from the Google Fonts css2 API, then the URLs pointed at local
  files. The unicode-range declarations are kept verbatim and matter: a
  browser downloads only the subsets the page actually needs, so the
  latin-ext and vietnamese files cost returning visitors nothing unless a
  character from them appears. Archivo is a variable font, so one file per
  subset serves every weight from 400 to 700.

  font-display: swap is inherited from the generated CSS — text paints
  immediately in the fallback and reflows once the webfont lands, which is
  the right trade for a lead-generation page.
*/

/* vietnamese */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-vietnamese.woff2") format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin-ext.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* vietnamese */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-vietnamese.woff2") format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin-ext.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* vietnamese */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-vietnamese.woff2") format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin-ext.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* vietnamese */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-vietnamese.woff2") format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

/* latin-ext */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin-ext.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url("../fonts/archivo-latin.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* latin-ext */
@font-face {
  font-family: 'Instrument Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/instrument-serif-italic-latin-ext.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Instrument Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/instrument-serif-italic-latin.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* latin-ext */
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/instrument-serif-latin-ext.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/instrument-serif-latin.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}



/* ---- tokens.css ---- */
/* ==========================================================================
   TOKENS — Color Transformations Painting
   --------------------------------------------------------------------------
   Two layers, deliberately.

   1. PRIMITIVES  — the client palette verbatim, named as the handoff names
                    them. A palette revision is a one-line-per-colour edit here.
   2. SEMANTIC    — what every component actually consumes. No component file
                    may reference a primitive directly.

   Stage 2: the primitive block maps 1:1 onto theme.json
   settings.color.palette; the type scale maps onto settings.typography.
   ========================================================================== */

:root {

  /* ---- Primitives: client palette ------------------------------------ */
  --ink:              #0E1116;  /* Ink / near-black — base dark bg, body text */
  --ink-deep:         #04121C;  /* Text on cyan fills */
  --navy-900:         #101A2C;  /* Hero base */
  --navy-800:         #111E31;  /* Quick-answer cells */
  --navy-700:         #132238;  /* Section transitions */
  --navy-600:         #1B3157;  /* Transformation mid */
  --brand-blue:       #1E3C8F;  /* Process bg, links, eyebrows on light */
  --brand-blue-alt:   #1B3478;  /* Process lower */
  --cyan:             #16A2E4;  /* PRIMARY ACCENT — buttons, rules, marks */
  --cyan-light:       #8FD3F6;  /* Eyebrows on dark, footer tagline */
  --paper:            #F6F4F0;  /* Light section base */
  --paper-warm:       #F1EEE8;  /* Light section end */
  --paper-warmer:     #EFECE6;  /* FAQ end / footer start */
  --white:            #FFFFFF;
  --body-grey:        #4A5058;  /* Body copy + notes on light (6.9:1 on paper) */
  --google-gold:      #FBBC04;
  --google-blue:      #4285F4;
  --google-green:     #34A853;
  --google-yellow:    #FBBC05;
  --google-red:       #EA4335;
  --avatar-1:         #1A5C8A;
  --avatar-2:         #0E1116;
  --avatar-3:         #16A2E4;

  /* Derived: hover partner for the cyan fill on light ground. */
  --cyan-deep:        #0D7DB4;

  /* ---- Semantic: surfaces -------------------------------------------- */
  --color-surface-page:       var(--ink);
  --color-surface-white:      var(--white);
  --color-surface-paper:      var(--paper);
  --color-surface-paper-warm: var(--paper-warm);
  --color-surface-navy:       var(--navy-800);
  --color-surface-ink:        var(--ink);

  /* ---- Semantic: text ------------------------------------------------- */
  --color-text-primary:        var(--ink);
  --color-text-muted:          var(--body-grey);
  --color-text-inverse:        var(--white);
  /* Alpha steps used for inverse copy. Every one of these is contrast-checked
     against the darkest ground it sits on — do not lighten them further. */
  --color-text-inverse-strong: rgba(255, 255, 255, 0.88);
  --color-text-inverse-body:   rgba(255, 255, 255, 0.78);
  --color-text-inverse-muted:  rgba(255, 255, 255, 0.68);
  --color-text-link:           var(--brand-blue);
  --color-text-link-hover:     var(--cyan-deep);

  /* ---- Semantic: accents ---------------------------------------------- */
  --color-accent:             var(--cyan);
  --color-accent-on-dark:     var(--cyan-light);
  --color-accent-on-light:    var(--brand-blue);
  --color-accent-foreground:  var(--ink-deep);

  /* ---- Semantic: borders ----------------------------------------------
     1.4.11 — an interactive control's boundary needs >= 3:1. The subtle
     tints below are for decorative dividers and non-interactive card edges
     ONLY. Form inputs, chips and outline buttons use the -strong pair. */
  --color-border-subtle:         rgba(14, 17, 22, 0.10);
  --color-border-subtle-inverse: rgba(255, 255, 255, 0.14);
  --color-border-strong:         #767C86;  /* 3.1:1 on white, 3.0:1 on paper */
  --color-border-strong-inverse: rgba(255, 255, 255, 0.55);
  --color-border-hairline:       rgba(14, 17, 22, 0.14);

  /* Focus ring — visibly distinct from every hover state on the page. */
  --color-focus-ring: var(--cyan);

  /* ---- Typography ------------------------------------------------------ */
  --font-display: "Instrument Serif", Georgia, "Times New Roman", serif;
  --font-sans:    Archivo, Helvetica, Arial, sans-serif;
  --font-mono:    ui-monospace, Menlo, Consolas, monospace;

  --text-h1:        clamp(3.25rem, 9vw, 8.25rem);       /* 52 -> 132 */
  --text-h2-large:  clamp(2.375rem, 4.8vw, 4.25rem);    /* 38 -> 68 */
  --text-h2:        clamp(2.25rem, 4.2vw, 3.75rem);     /* 36 -> 60 */
  --text-h2-estimate: clamp(2.375rem, 4.4vw, 4rem);     /* 38 -> 64 */
  --text-statement: clamp(2.625rem, 6.4vw, 6rem);       /* 42 -> 96 */
  --text-statement-setup: clamp(1.0625rem, 1.5vw, 1.3125rem);
  --text-service-lede: clamp(1.625rem, 2.6vw, 2.25rem); /* 26 -> 36 */
  --text-figure:    3.5rem;      /* 56 — rating, step numeral */
  --text-phone:     clamp(2.25rem, 3.4vw, 2.75rem);     /* 36 -> 44 */
  --text-h3:        1.5rem;      /* 24 — process */
  --text-h3-card:   1.3125rem;   /* 21 — cards */
  --text-tab:       1.4375rem;   /* 23 */
  --text-lead:      1.15625rem;  /* 18.5 */
  --text-body-lg:   1.125rem;    /* 18 */
  --text-quote:     1.03125rem;  /* 16.5 */
  --text-body:      1rem;        /* 16 */
  --text-body-sm:   0.96875rem;  /* 15.5 */
  --text-meta:      0.90625rem;  /* 14.5 */
  --text-sm:        0.875rem;    /* 14 */
  --text-chip:      0.84375rem;  /* 13.5 */
  --text-xs:        0.8125rem;   /* 13 */
  --text-eyebrow:   0.78125rem;  /* 12.5 */
  --text-label:     0.75rem;     /* 12 — field labels */
  --text-eyebrow-sm: 0.75rem;    /* 12 */
  --text-micro:     0.71875rem;  /* 11.5 */

  --leading-display: 1.05;
  --leading-snug:    1.35;
  --leading-body:    1.65;

  --tracking-display: -0.015em;
  --tracking-tight:   -0.01em;
  --tracking-button:  0.07em;
  --tracking-nav:     0.08em;
  --tracking-field:   0.12em;
  --tracking-eyebrow: 0.16em;
  --tracking-eyebrow-wide: 0.18em;

  /* ---- Spacing --------------------------------------------------------
     The page's vertical rhythm is unusually large by design: the deep
     paddings are what the background gradient travels through. They scale
     down on small screens but never collapse — see layout.css. */
  --space-3xs: 0.375rem;   /*  6 */
  --space-2xs: 0.5rem;     /*  8 */
  --space-xs:  0.75rem;    /* 12 */
  --space-sm:  0.875rem;   /* 14 */
  --space-md:  1.25rem;    /* 20 */
  --space-lg:  1.625rem;   /* 26 */
  --space-xl:  2rem;       /* 32 */
  --space-2xl: 2.75rem;    /* 44 */
  --space-3xl: 3.5rem;     /* 56 */
  --space-4xl: 4.5rem;     /* 72 */

  --gutter: clamp(1.25rem, 4vw, 2rem);           /* 20 -> 32 */
  --container-max: 87.5rem;   /* 1400 */
  --container-mask: 77.5rem;  /* 1240 */
  --container-faq: 67.5rem;   /* 1080 */
  --container-narrow: 68.75rem; /* 1100 */

  /* Section rhythm — the paired values are the "hard hand-off" paddings the
     background journey depends on. */
  --section-pad-sm:   clamp(3.75rem, 8vw, 6.25rem);   /*  60 -> 100 */
  --section-pad-md:   clamp(4.5rem,  9vw, 7.375rem);  /*  72 -> 118 */
  --section-pad-lg:   clamp(6rem,   13vw, 12.5rem);   /*  96 -> 200 */
  /* Also the length of the process section's colour ramp — at the old 120px
     floor a full paper -> deep-blue descent banded visibly on a phone. */
  --section-pad-xl:   clamp(13rem, 22vw, 20rem);      /* 208 -> 320 */
  /* The footer's top padding IS the length of its colour ramp (the ramp is
     bound to it — see site-footer.css). Deliberately the deepest padding on
     the page: a paper -> navy descent needs room or it reads as a band. */
  --section-pad-footer: clamp(11.875rem, 24vw, 21.875rem); /* 190 -> 350 */

  /* ---- Radius — deliberately near-square. No soft card language. ------- */
  --radius-control: 2px;   /* buttons, photo frames, inputs */
  --radius-card:    3px;   /* cards, badges */
  --radius-round:   50%;   /* avatars */

  /* ---- Shadow ---------------------------------------------------------- */
  --shadow-card:       0 1px 2px rgba(14, 17, 22, 0.06);
  --shadow-card-hover: 0 10px 30px rgba(14, 17, 22, 0.10);

  /* ---- Motion ---------------------------------------------------------- */
  --ease-entrance: cubic-bezier(0.2, 0.7, 0.2, 1);
  --ease-word:     cubic-bezier(0.2, 0.75, 0.2, 1);
  --ease-wipe:     cubic-bezier(0.3, 0.8, 0.2, 1);
  --ease-reveal:   cubic-bezier(0.22, 0.75, 0.16, 1);
  --duration-fast: 0.25s;
  --duration-base: 0.4s;
  --duration-slow: 1.5s;

  /* ---- Z-index --------------------------------------------------------- */
  --z-mobile-cta: 70;
  --z-header: 60;
  --z-skip: 100;
}



/* ---- base.css ---- */
/* ==========================================================================
   BASE — reset, element defaults, focus, the reflow traps
   ========================================================================== */

*,
*::before,
*::after { box-sizing: border-box; }

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchor targets must clear the fixed header. */
  scroll-padding-top: 6rem;
}

body {
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--color-text-primary);
  background-color: var(--color-surface-page);
  -webkit-font-smoothing: antialiased;

  /*
    `clip`, NOT `hidden`. This one keyword is load-bearing for the whole
    entrance animation system.

    `overflow-x: hidden` forces the other axis to compute to `overflow-y: auto`
    (CSS Overflow 3: a `visible` axis becomes `auto` when its partner is
    hidden/scroll/auto). That makes <body> a scroll container — and every
    `animation-timeline: view()` then resolves against the BODY's scrollport
    rather than the viewport. The body's scrollport is the full height of the
    document, so no element ever enters or leaves it, the timeline never
    advances (overallProgress stays null), and every scroll-driven animation
    on the page silently does nothing.

    `clip` clips overflow WITHOUT creating a scroll container, so the timelines
    keep resolving against the viewport. Browsers too old for `overflow: clip`
    just ignore it — which is harmless here, because no element actually
    overflows at any breakpoint (verified at 320/768/1024/1440); this is a
    guard against future content, not a fix for present overflow.

    Do not "simplify" this back to `hidden`.
  */
  overflow-x: clip;
}

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

img { height: auto; }

h1, h2, h3, h4, h5, h6 {
  font-weight: 400;
  line-height: var(--leading-display);
  color: inherit;
  text-wrap: balance;
}

p { text-wrap: pretty; }

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

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

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

/* --------------------------------------------------------------------------
   REFLOW TRAP (WCAG 1.4.10)
   Browsers default fieldset to `min-inline-size: min-content`, so a fieldset
   refuses to shrink below its widest child and drags the page into horizontal
   scroll at 320px. The min-width:0 companions do the same job for grid and
   flex children, which otherwise refuse to shrink below their content.
   -------------------------------------------------------------------------- */
fieldset {
  min-inline-size: 0;
  border: 0;
  padding: 0;
}

legend { padding: 0; }

.l-grid > *,
.l-flex > * { min-width: 0; }

/* --------------------------------------------------------------------------
   Focus — visible on EVERY interactive element, `summary` included.
   -------------------------------------------------------------------------- */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--color-focus-ring);
  outline-offset: 3px;
}

/* The chip toggles hide their radio; the focus ring has to move to the label. */
.chip-toggle__input:focus-visible + .chip-toggle__label {
  outline: 3px solid var(--color-focus-ring);
  outline-offset: 3px;
}

/* Skip link — the first focusable element on the page. */
.skip-link {
  position: absolute;
  top: -4rem;
  left: var(--gutter);
  z-index: var(--z-skip);
  background: var(--cyan);
  color: var(--ink-deep);
  padding: 0.75rem 1.25rem;
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: var(--tracking-button);
  text-transform: uppercase;
  border-radius: var(--radius-control);
  transition: top var(--duration-fast) var(--ease-entrance);
}

.skip-link:focus { top: 0.75rem; }

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

/* ==========================================================================
   ANIMATION SYSTEM

   Keyframes are shared; what drives them is defined below the keyframe block.

   One rule from the design handoff still applies and is easy to undo by
   accident: NEVER NEST TWO ANIMATED WRAPPERS. Nested fades multiply their
   opacities and the inner element never reaches full strength. Put the
   data-attribute on the element that should animate, not on a parent that
   already carries one.
   ========================================================================== */

@keyframes ct-rise      { from { opacity: 0; transform: translateY(26px); }            to { opacity: 1; transform: none; } }
@keyframes ct-card-in   { from { opacity: 0; transform: translateY(40px) rotate(0.5deg); } to { opacity: 1; transform: none; } }
@keyframes ct-photo-in  { from { opacity: 0; transform: scale(1.06); }                  to { opacity: 1; transform: none; } }
@keyframes ct-slide-in  { from { opacity: 0; transform: translateX(-22px); }            to { opacity: 1; transform: none; } }
@keyframes ct-wipe-in   { from { clip-path: inset(0 100% 0 0); }                        to { clip-path: inset(0 0 0 0); } }
@keyframes ct-reveal-up { from { clip-path: inset(0 0 108% 0); transform: translateY(16px); } to { clip-path: inset(-20% -20% -20% -20%); transform: none; } }
@keyframes ct-slow-zoom { from { transform: scale(1.12) translateY(-2.6%); }            to { transform: scale(1) translateY(2.6%); } }
@keyframes ct-word-up   { from { opacity: 0; transform: translateY(0.62em) rotate(1.6deg); } to { opacity: 1; transform: none; } }
@keyframes ct-kenburns  { from { transform: scale(1); }                                 to { transform: scale(1.09); } }
@keyframes ct-rise-soft { from { opacity: 0; transform: translateY(30px); }             to { opacity: 1; transform: none; } }
@keyframes ct-soft-fade { from { opacity: 0; transform: scale(1.02); }                  to { opacity: 1; transform: none; } }
@keyframes ct-rule-grow { from { transform: scaleX(0); }                                to { transform: scaleX(1); } }
/* The answers strip is one continuous hairline band, not a row of floating
   cards, so it draws across from the left rather than rising and settling.
   Clip only — no opacity — so the 1px grid gaps never flash. */
@keyframes ct-cell-in   { from { clip-path: inset(0 100% 0 0); }                        to { clip-path: inset(0 0 0 0); } }

/* --------------------------------------------------------------------------
   ENTRANCE ANIMATIONS — one path, every browser.

   This was originally built on CSS scroll-driven animations
   (`animation-timeline: view()`). That is the more elegant mechanism, and it
   is gone on purpose. Two separate failures killed it:

   1. `body { overflow-x: hidden }` made <body> a scroll container, so every
      view() timeline resolved against a scrollport that never scrolls and no
      animation ever advanced. Silent, and invisible to a CSS review.
   2. Feature detection cannot be trusted here. Some browsers parse
      `animation-timeline` — so `CSS.supports()` answers true — without
      driving view() timelines correctly. A fallback gated on that check
      stands aside precisely when it is needed most.

   Both failures shared one property: the page looked static and nothing
   reported an error. So the mechanism is now an IntersectionObserver, which
   works identically everywhere, fails loudly if it fails at all, and can be
   verified by reading a class rather than by trusting a timeline.

   `js-anim` is added by main.js, so the hidden pre-state can only ever exist
   when the script that removes it is running. Scripting off: nothing hidden.
   -------------------------------------------------------------------------- */

/*
  PRE-STATE. Every element listed here must be revealed by a keyframe that
  actually animates the property being pre-set, or it stays invisible forever.

  That is not hypothetical: [data-reveal] was in this opacity list while its
  keyframe (ct-reveal-up) only animates clip-path and transform — so every
  section heading on the page animated its clip-path while sitting at opacity
  0, permanently invisible. If you add an attribute here, check its keyframe.

  Animates opacity: ct-rise, ct-card-in, ct-photo-in, ct-slide-in,
  ct-rise-soft, ct-word-up.
  Does NOT: ct-reveal-up, ct-wipe-in, ct-rule-grow — those get a shape
  pre-state instead.
*/
.js-anim [data-rise],
.js-anim [data-card],
.js-anim [data-photo],
.js-anim [data-slide],
.js-anim [data-col],
.js-anim [data-word-scroll] {
  opacity: 0;
}

/* Shape pre-states — these keyframes never touch opacity. */
.js-anim [data-reveal] { clip-path: inset(0 0 108% 0); transform: translateY(16px); }
.js-anim [data-cell]   { clip-path: inset(0 100% 0 0); }
.js-anim [data-wipe]   { clip-path: inset(0 100% 0 0); }
.js-anim [data-rule]   { transform: scaleX(0); transform-origin: left center; }

.js-anim [data-rise].is-in   { animation: ct-rise      0.85s var(--ease-entrance) both; }
.js-anim [data-card].is-in   { animation: ct-card-in   0.9s  var(--ease-entrance) both; }
.js-anim [data-photo].is-in  { animation: ct-photo-in  1s    var(--ease-entrance) both; }
.js-anim [data-slide].is-in  { animation: ct-slide-in  0.8s  var(--ease-entrance) both; }
.js-anim [data-reveal].is-in { animation: ct-reveal-up 1s    var(--ease-reveal)   both; }
.js-anim [data-col].is-in    { animation: ct-rise-soft 0.85s var(--ease-entrance) both; }
.js-anim [data-wipe].is-in   { animation: ct-wipe-in   0.9s  var(--ease-wipe)     both; }
.js-anim [data-rule].is-in   { animation: ct-rule-grow 0.8s  var(--ease-wipe)     both; }
.js-anim [data-cell].is-in   { animation: ct-cell-in   0.65s var(--ease-wipe)     both; }

/* Grid children stagger by position so a row does not land all at once.
   --n is set by the observer. */
.js-anim [data-card].is-in { animation-delay: calc(var(--n, 0) * 90ms); }
.js-anim [data-cell].is-in { animation-delay: calc(var(--n, 0) * 110ms); }

/* Words keep their per-word stagger; --i comes from the .w-N index class. */
.js-anim [data-word-scroll].is-in {
  animation: ct-word-up 0.8s var(--ease-word) both;
  animation-delay: calc(var(--i, 0) * 70ms);
}

/* The continuous zoom on the work photos is the one effect an observer cannot
   reproduce — it is scroll-linked across the element's whole pass, not a
   one-shot. It stays on the scroll-driven timeline and simply does not run
   where that is unsupported, which costs nothing: the photo is just static.
   NOTE: `animation-timeline` must come AFTER the `animation` shorthand — the
   shorthand resets it to `auto`. */
[data-zoom] {
  animation: ct-slow-zoom both linear;
  animation-timeline: view();
  animation-range: cover 0% cover 100%;
}

/*
  STAGGER INDEX. Sets --i, which every staggered sequence on the page reads:
  word-by-word headlines, the hero load-in order, cards within a grid.

  Classes rather than inline custom properties, so the pages carry no style
  attributes and stage 2 inherits none.

  These are load-bearing — without them every --i falls back to 0 and each
  sequence fires all at once instead of stepping. They were silently deleted
  once by a careless block edit; if staggering ever looks "off", check here
  first.
*/
.i-0 { --i: 0; }
.i-1 { --i: 1; }
.i-2 { --i: 2; }
.i-3 { --i: 3; }
.i-4 { --i: 4; }
.i-5 { --i: 5; }
.i-6 { --i: 6; }
.i-7 { --i: 7; }
.i-8 { --i: 8; }
.i-9 { --i: 9; }
.i-10 { --i: 10; }
.i-11 { --i: 11; }
.i-12 { --i: 12; }
.i-13 { --i: 13; }
.i-14 { --i: 14; }
.i-15 { --i: 15; }

[data-word] { display: inline-block; }

[data-word-load] {
  animation: ct-word-up 0.85s var(--ease-word) both;
  animation-delay: calc(240ms + var(--i, 0) * 90ms);
}

/*
  Hero load-in. The hero is on screen the instant the page opens, so it runs on
  a TIME base rather than waiting for a scroll that has not happened — and it
  is plain CSS, so it plays whether or not the script runs. Order is set by the
  .i-N stagger class on each element.
*/
[data-load] {
  animation: ct-rise 0.8s var(--ease-entrance) both;
  animation-delay: calc(var(--i, 0) * 90ms);
}

/* The photo gets the scale-in rather than the rise — it is a frame, not a
   line of type. */
[data-load-photo] {
  animation: ct-photo-in 1.1s var(--ease-entrance) both;
  animation-delay: calc(var(--i, 0) * 90ms);
}

[data-hero-slide] { animation: ct-kenburns 22s ease-out both alternate infinite; }

/* WCAG 2.3.3 — reveal everything immediately rather than leaving content
   behind an effect that will not play. */
@media (prefers-reduced-motion: reduce) {
  .js-anim [data-rise],
  .js-anim [data-card],
  .js-anim [data-photo],
  .js-anim [data-slide],
  .js-anim [data-reveal],
  .js-anim [data-col],
  .js-anim [data-word-scroll],
  .js-anim [data-wipe],
  .js-anim [data-rule] {
    opacity: 1;
    clip-path: none;
    transform: none;
    animation: none !important;
  }

  [data-zoom], [data-word-load], [data-hero-slide],
  [data-load], [data-load-photo] { animation: none !important; opacity: 1; transform: none; }
}

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

  [data-rise], [data-card], [data-photo], [data-slide], [data-wipe],
  [data-reveal], [data-zoom], [data-rule], [data-col], [data-fade],
  [data-word-scroll], [data-word-load], [data-hero-slide] {
    animation: none !important;
  }

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



/* ---- layout.css ---- */
/* ==========================================================================
   LAYOUT — sections, containers, the background journey
   --------------------------------------------------------------------------
   THE BACKGROUND JOURNEY is the core concept of this page. Every section owns
   a `linear-gradient(180deg, …)` whose FIRST stop equals the previous
   section's LAST stop, forming one continuous ramp from near-black to navy to
   paper to white and back to black, with no visible seam anywhere.

   Two things it depends on — do not "simplify" either:

   1. Multi-stop eased ramps. A two-stop light->dark transition bands visibly.
      The process section needs 8 stops, the footer 11.
   2. Deep padding at the hard hand-offs. The empty space is what the colour
      change travels through, so it lands BEFORE the next heading rather than
      behind it.
   ========================================================================== */

.section { padding-inline: var(--gutter); }

.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
}

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

/* --- Section grounds, in document order --------------------------------- */

.section--hero {
  background: linear-gradient(180deg, #0E1116 0%, #0E1116 44%, #0D1320 66%, #101A2C 100%);
  color: var(--color-text-inverse);
}

.section--answers {
  background: linear-gradient(180deg, #101A2C 0%, #132238 100%);
  color: var(--color-text-inverse);
  padding-block: 0.625rem var(--section-pad-md);
}

.section--transformation {
  /* Paper arrives at 84%, then holds, so the CTA row below the mask sits on a
     settled paper ground rather than mid-transition. Measured: the button fill
     read 1.59:1 against the mid-grey it previously landed on. */
  background: linear-gradient(180deg, #132238 0%, #1B3157 34%, #3D5E85 52%, #8398AE 66%, #CFCEC8 76%, #F6F4F0 84%, #F6F4F0 100%);
  padding-block: var(--section-pad-sm) var(--section-pad-lg);
  position: relative;
}

.section--services {
  background: linear-gradient(180deg, #F6F4F0 0%, #FFFFFF 40%, #FFFFFF 100%);
  color: var(--color-text-primary);
  padding-block: 1.25rem var(--section-pad-md);
}

.section--values {
  background: linear-gradient(180deg, #FFFFFF 0%, #F6F3EE 52%, #F1EEE8 100%);
  color: var(--color-text-primary);
  /* 260px bottom is load-bearing: it is the runway for the light -> deep-blue
     ramp that begins in the process section. */
  padding-block: var(--section-pad-sm) var(--section-pad-xl);
}

/*
  The process section's light -> deep-blue ramp.

  The handoff expressed this ramp in PERCENTAGES of the section, with the deep
  blue arriving at 40%. That only lands before the heading if the section
  happens to be the right height — and it is not. At 1440px the ramp was still
  mid-transition behind the eyebrow, which measured 3.54:1 and failed 1.4.3;
  on a taller mobile layout it is worse.

  So the ramp is bound to the TOP PADDING instead, in a fixed-height layer.
  The empty space above the heading is exactly the distance the colour travels,
  at every viewport, which is what the design intended all along. The stops are
  the handoff's own, rescaled from the 0-40% band to 0-100% of the ramp.
*/
.section--process {
  --process-ramp: var(--section-pad-xl);
  background: linear-gradient(180deg, #1E3C8F 0%, #1E3C8F 18%, #1B3478 78%, #132238 100%);
  color: var(--color-text-inverse);
  padding-block: var(--section-pad-xl) var(--section-pad-lg);
  position: relative;
  overflow: hidden;
}

.section--process::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--process-ramp);
  pointer-events: none;
  /* 21 stops. The handoff's own six colours are all still here; the rest are
     Lab-interpolated along the same path.

     Positions are chosen so PERCEIVED lightness follows a smootherstep across
     the ramp — gentle out of the paper, gentle into the blue, the change
     carried through the middle. Evenly spaced stops are not enough on their
     own: a ramp that goes from "not changing" to "changing at full rate" in
     one step draws a visible line where it leaves the flat colour above it.
     Easing both ends is what removes that line. Same method as the reviews
     ramp. */
  background: linear-gradient(180deg,
    #F1EEE8 0%,
    #ECEAE8 13.7%,
    #E7E6E7 17.8%,
    #E2E3E7 20.2%,
    #DDDFE6 22.7%,
    #D0D4E1 28.1%,
    #C3C9DB 32.6%,
    #B5BED6 36.6%,
    #A8B3D0 40.2%,
    #96A3C9 45.1%,
    #8393C2 49.9%,
    #7083BB 54.7%,
    #5B74B4 59.6%,
    #5069AE 63.3%,
    #455FA7 67%,
    #3854A1 71.7%,
    #2A4A9B 77.2%,
    #274698 79.9%,
    #244395 82.5%,
    #213F92 86.9%,
    #1E3C8F 100%);
}

.section--work {
  background: linear-gradient(180deg, #132238 0%, #11192A 30%, #0E1116 58%, #0E1116 100%);
  color: var(--color-text-inverse);
  padding-block: var(--section-pad-lg) var(--section-pad-sm);
}

.section--statement {
  background: var(--ink);
  color: var(--color-text-inverse);
  padding-block: clamp(5rem, 10vw, 8.25rem) clamp(5.25rem, 10vw, 8.625rem);
  overflow: hidden;
}

.section--reviews {
  /* The ramp lives on .reviews__ramp (see reviews.css); the section itself is
     the paper the cards and CTA sit on. */
  background: var(--paper);
  padding-block: 0 var(--section-pad-md);
}

.section--estimate {
  background: linear-gradient(180deg, #F6F4F0 0%, #FFFFFF 45%, #FFFFFF 100%);
  color: var(--color-text-primary);
  padding-block: var(--section-pad-sm) var(--section-pad-md);
}

.section--faq {
  background: linear-gradient(180deg, #FFFFFF 0%, #F6F4F0 34%, #EFECE6 100%);
  color: var(--color-text-primary);
  padding-block: var(--section-pad-sm) clamp(4.5rem, 9vw, 7.5rem);
}

/* --- Shared section furniture --------------------------------------------

   .section-head is a WRAPPER class — a div holding the eyebrow rule, the
   eyebrow and the headline. Never put it on a heading.

   Putting it on an <h2> also puts `.headline` on the same element, and the two
   collide: both are single-class selectors (0,1,0), so the later one in this
   file wins and `.headline { margin: 0 }` silently cancels the wrapper's
   margin-bottom. That produced a 0px gap under two section headings. A
   standalone heading gets its spacing from its own component class instead —
   see .values__title, .faq__title, .work__title.
   -------------------------------------------------------------------------- */

.section-head { max-width: 48.75rem; margin-bottom: clamp(2rem, 4vw, 3.125rem); }
.section-head--narrow { max-width: 45rem; margin-bottom: clamp(2.25rem, 4.5vw, 3.625rem); }
.section-head--wide { max-width: 51.25rem; }

.eyebrow {
  font-size: var(--text-eyebrow-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-eyebrow-wide);
  text-transform: uppercase;
  margin-bottom: 1.125rem;
}

.eyebrow--on-dark  { color: var(--color-accent-on-dark); }
.eyebrow--on-light { color: var(--color-accent-on-light); }

.headline {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--text-h2);
  line-height: var(--leading-display);
  margin: 0;
}

.headline--large { font-size: var(--text-h2-large); }

.section-lede {
  font-size: var(--text-body-lg);
  line-height: var(--leading-body);
  max-width: 37.5rem;
}

/* --- Responsive collapse -------------------------------------------------
   Breakpoints: 1100 (nav + 4-col), 900 (two-column bodies), 720 (mobile bar),
   560 (single column everywhere).
   -------------------------------------------------------------------------- */

@media (max-width: 68.75rem) {   /* 1100 */
  .section--process,
  .section--values { --grid-cols: 2; }
}

@media (max-width: 45rem) {      /* 720 — the sticky CTA bar appears here, so
                                    the document needs room beneath it. */
  body { padding-bottom: 3.75rem; }
}




/* ---- button.css ---- */
/* ==========================================================================
   COMPONENT: button
   One block, four modifiers. Every CTA on the page is this component — if a
   new call to action appears, add a modifier here rather than a new block.
   Targets are well over the 24x24 minimum (2.5.8) at every size.
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 1.1875rem 1.875rem;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: var(--tracking-button);
  text-transform: uppercase;
  text-align: center;
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--ease-entrance),
    border-color var(--duration-fast) var(--ease-entrance),
    color var(--duration-fast) var(--ease-entrance);
}

/* Primary — cyan fill, near-black label. */
.btn--primary {
  background: var(--cyan);
  border-color: var(--cyan);
  color: var(--ink-deep);
}

/*
  1.4.11 — the cyan fill measures 2.4-2.9:1 against the light grounds, just
  under the 3:1 an interactive control's boundary needs. The fill stays exactly
  the brand cyan; a one-pixel deeper-cyan rim (4.15:1 on paper) supplies the
  compliant boundary and is all but invisible at a glance.
*/
.section--transformation .btn--primary,
.section--services .btn--primary,
.section--reviews .btn--primary,
.section--faq .btn--primary,
.section--estimate .btn--primary {
  border-color: var(--cyan-deep);
}

/* On a dark ground the primary inverts to white. */
.section--hero .btn--primary:hover,
.section--process .btn--primary:hover,
.section--work .btn--primary:hover {
  background: var(--white);
  border-color: var(--white);
  color: var(--ink);
}

/*
  On a light ground it goes to INK, not white.

  The prototype inverted the transformation and FAQ CTAs to white on hover —
  a white button on a near-white ground, which disappears. The handoff's own
  written rule is the correct one and is what is implemented here:
  "#16A2E4 -> #fff (on dark) or -> #0E1116 with white text (on light)".
*/
.section--transformation .btn--primary:hover,
.section--services .btn--primary:hover,
.section--reviews .btn--primary:hover,
.section--estimate .btn--primary:hover,
.section--faq .btn--primary:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--white);
}

/*
  The header CTA is the one button that changes ground underneath itself: it
  starts over the near-black hero and ends over the 92%-opaque paper bar after
  the scroll flip. Two consequences, both easy to miss because they only show
  up once you scroll:

  1. Against the paper bar the cyan fill measures ~2.2:1 — under the 3:1 a
     control's boundary needs (1.4.11). It takes the same deeper-cyan rim the
     light sections use.
  2. The hover rules below are scoped to `.section--*`, and the header is not
     inside a section, so it had no hover state at all. It needs both
     directions — to white over the dark hero, to ink over the paper bar.
*/
.site-header .btn--primary:hover {
  background: var(--white);
  border-color: var(--white);
  color: var(--ink);
}

.site-header--solid .btn--primary { border-color: var(--cyan-deep); }

.site-header--solid .btn--primary:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--white);
}

/* Secondary — white fill, used on dark grounds. */
.btn--secondary {
  background: var(--white);
  border-color: var(--white);
  color: var(--ink);
  font-weight: 600;
}

.btn--secondary:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--white);
}

/* Outline — used on light grounds. Border is --color-border-strong, not a
   hairline tint: an interactive boundary must reach 3:1 (1.4.11). */
.btn--outline {
  background: transparent;
  border-color: var(--color-border-strong);
  color: var(--ink);
  font-weight: 600;
}

.btn--outline:hover {
  border-color: var(--ink);
  background: rgba(14, 17, 22, 0.05);
}

/* Compact — the header CTA. */
.btn--compact {
  padding: 0.8125rem 1.25rem;
  font-size: var(--text-eyebrow);
  letter-spacing: var(--tracking-nav);
}

/* Block — the form submit. */
.btn--block {
  width: 100%;
  background: var(--ink);
  border-color: var(--ink);
  color: var(--white);
  padding: 1.25rem;
  letter-spacing: 0.09em;
}

.btn--block:hover {
  background: var(--brand-blue);
  border-color: var(--brand-blue);
}

/* --- Button group: the repeated "primary + call + note" row -------------- */

.btn-group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-3xl);
}

.btn-group--centred { justify-content: center; }
.btn-group--flush { margin-top: 0; }

.btn-group__note {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.btn-group__note--on-dark { color: var(--color-text-inverse-muted); }

@media (max-width: 30rem) {
  /* Below 480px a two-up button row wraps into an awkward orphan; make both
     full width so the CTA never renders as a half-width stub. */
  .btn-group > .btn { width: 100%; }
}



/* ---- site-header.css ---- */
/* ==========================================================================
   COMPONENT: site-header
   Transparent over the hero, then flips to a blurred paper bar once the user
   is past 72vh. The flip is applied by JS toggling `.site-header--solid`;
   all the styling lives here.
   ========================================================================== */

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-header);
  background: transparent;
  border-bottom: 1px solid transparent;
  transition:
    background-color var(--duration-base) ease,
    border-color var(--duration-base) ease,
    backdrop-filter var(--duration-base) ease;
}

/*
  Legibility scrim for the transparent state.

  The header is transparent over the hero by design, but the hero photo scrolls
  up underneath it long before the solid flip at 72vh — putting white nav links
  over a bright, high-key interior shot. Measured white-on-photo worst case
  there is about 1.1:1.

  This is a soft top-down scrim that holds >= 0.70 alpha across the full height
  of the bar and only fades out below it, so the links stay legible over ANY
  photograph (7.3:1 against pure white) while the bar still reads as
  transparent rather than as a filled header. It fades away as the solid state
  takes over.
*/
.site-header::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: calc(100% + 118px);
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(14, 17, 22, 0.80) 0%,
    rgba(14, 17, 22, 0.72) 41%,
    rgba(14, 17, 22, 0.36) 70%,
    rgba(14, 17, 22, 0) 100%);
  transition: opacity var(--duration-base) ease;
}

.site-header--solid::before { opacity: 0; }

/* The scrim is a positioned child, so the bar's own content — which is not
   positioned — would paint underneath it. */
.site-header__inner { position: relative; z-index: 1; }
.site-header__progress { z-index: 2; }

.site-header--solid {
  background: rgba(246, 244, 240, 0.92);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom-color: rgba(14, 17, 22, 0.1);
}

.site-header__inner {
  max-width: var(--container-max);
  margin-inline: auto;
  padding: 1.125rem var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

/* Scroll progress — 2px, pinned to the header's bottom edge. */
.site-header__progress {
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100%;
  height: 2px;
  background: var(--cyan);
  transform: scaleX(0);
  transform-origin: left center;
}

.site-header__logo {
  display: flex;
  align-items: center;
  flex: none;
}

.site-header__logo img {
  height: 2.875rem;
  width: auto;
  /* Invisible over the hero (the hero has its own large lockup); fades in
     with the solid bar. */
  opacity: 0;
  transition: opacity var(--duration-base) ease;
}

.site-header--solid .site-header__logo img { opacity: 1; }

/* Keyboard users can reach the logo before the header has gone solid — show
   it on focus so the link is not an invisible target. */
.site-header__logo:focus-visible img { opacity: 1; }

@media (max-width: 45rem) {
  .site-header__logo img { height: 2.25rem; }
  .site-header__inner { padding-block: 0.875rem; }
}



/* ---- nav-primary.css ---- */
/* ==========================================================================
   COMPONENT: primary navigation
   --------------------------------------------------------------------------
   BEM EXCEPTION — APPROVED, DO NOT "FIX".
   The class names `menu`, `menu-item`, `menu-item-has-children`, `sub-menu`
   and `current-menu-item` mirror WordPress's wp_nav_menu() walker output.
   Keeping them means a registered nav menu drops straight into this markup at
   stage 2 with no CSS rewrite. Everything else in this file is BEM.

   `current-menu-item` is styling only and tells assistive tech nothing — the
   accessible signal is `aria-current`, set by JS in assets/js/main.js.
   ========================================================================== */

.nav-primary {
  display: flex;
  align-items: center;
  gap: clamp(0.875rem, 1.6vw, 1.75rem);
  flex: none;
  white-space: nowrap;
  color: var(--white);
  transition: color var(--duration-base) ease;
}

.site-header--solid .nav-primary { color: var(--ink); }

.nav-primary .menu {
  display: flex;
  align-items: center;
  gap: clamp(0.875rem, 1.6vw, 1.75rem);
  margin: 0;
  color: inherit;
}

.nav-primary .menu-item > a {
  display: inline-block;
  /* The visible label is 13px; the padding lifts the hit area past the
     24x24 minimum (2.5.8) without changing the type. */
  padding-block: 0.5rem;
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-nav);
  text-transform: uppercase;
  color: inherit;
  border-bottom: 1px solid transparent;
  transition: border-color var(--duration-fast) ease, opacity var(--duration-fast) ease;
}

.nav-primary .menu-item > a:hover { border-bottom-color: currentColor; }

/* Current section. Deliberately quiet — a 1px rule, not a colour change. */
.nav-primary .menu-item.current-menu-item > a { border-bottom-color: currentColor; }

.nav-primary__phone {
  font-size: var(--text-meta);
  font-weight: 600;
  color: inherit;
  white-space: nowrap;
  padding-block: 0.375rem;
}

.nav-primary__phone:hover { text-decoration: underline; text-underline-offset: 4px; }

/* --- Mobile disclosure ---------------------------------------------------
   ADDITION beyond the supplied design, flagged in the handoff notes: below
   1100px the design hides the nav links with no replacement, leaving the
   section shortcuts unreachable for anyone not scrolling the full page.
   This is the standard disclosure pattern in the page's own visual language.
   -------------------------------------------------------------------------- */

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 2.75rem;
  height: 2.75rem;
  flex: none;
  color: inherit;
  border: 1px solid currentColor;
  border-radius: var(--radius-control);
}

.nav-toggle__bars {
  position: relative;
  display: block;
  width: 1.125rem;
  height: 2px;
  background: currentColor;
  transition: background-color var(--duration-fast) ease;
}

.nav-toggle__bars::before,
.nav-toggle__bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background: currentColor;
  transition: transform var(--duration-fast) var(--ease-entrance);
}

.nav-toggle__bars::before { top: -6px; }
.nav-toggle__bars::after  { top: 6px; }

.nav-toggle[aria-expanded="true"] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after  { transform: translateY(-6px) rotate(-45deg); }

.nav-panel {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: var(--ink);
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
  padding: var(--space-md) var(--gutter) var(--space-xl);
}

.nav-panel .menu {
  display: flex;
  flex-direction: column;
  gap: 0;
  max-width: var(--container-max);
  margin-inline: auto;
}

.nav-panel .menu-item > a {
  display: block;
  padding: 1.0625rem 0;
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: var(--tracking-nav);
  text-transform: uppercase;
  color: var(--white);
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}

.nav-panel .menu-item > a:hover { color: var(--cyan-light); }
.nav-panel .menu-item.current-menu-item > a { color: var(--cyan-light); }

@media (max-width: 68.75rem) {   /* 1100 */
  .nav-primary > .menu { display: none; }
  .nav-toggle { display: inline-flex; }
}

@media (min-width: 68.8125rem) {
  /* Above the breakpoint the panel can never be shown, even if a stale
     `open` state survives a resize. */
  .nav-panel { display: none !important; }
}

@media (max-width: 35rem) {      /* 560 — keep the header on one line */
  .site-header .btn--compact { display: none; }
  .nav-primary { gap: 0.75rem; }
}



/* ---- hero.css ---- */
/* ==========================================================================
   COMPONENT: hero
   ========================================================================== */

.hero {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  /* Both alignment values are load-bearing. Centring the hero made the gap
     under the nav grow with viewport height; centring the grid left the photo
     sitting lower than the logo. flex-start + stretch keeps the logo and the
     photo top edges flush at the top padding on any screen height. */
  align-items: flex-start;
  overflow: hidden;
  padding-block: 6.5rem var(--space-4xl);
}

.hero__glow {
  position: absolute;
  inset: -10% -5%;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(58% 52% at 76% 40%, rgba(30, 60, 143, 0.55) 0%, rgba(14, 17, 22, 0) 68%),
    radial-gradient(40% 40% at 92% 74%, rgba(22, 162, 228, 0.28) 0%, rgba(14, 17, 22, 0) 70%);
}

/*
  Bottom scrim — resolves the hero to EXACTLY #101A2C at its lower edge, which
  is the colour the next section starts on.

  Without it the ambient glow is still lit where `overflow: hidden` cuts the
  hero off, so the boundary showed as a hard horizontal line: lifted blue above
  it, flat navy below. Masking the glow itself does not fix this — the glow is
  parallaxed, so any fade built into it slides down as you scroll and the clip
  returns.

  This scrim is pinned to the hero instead of to the glow, so it holds at every
  scroll position. It sits above the glow and below the content (see the
  z-index ladder), and the photo carries its own scrim to the same colour.
*/
.hero::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 28%;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(16, 26, 44, 0) 0%,
    rgba(16, 26, 44, 0.55) 55%,
    rgba(16, 26, 44, 0.88) 80%,
    #101A2C 100%);
}

.hero__grid {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: 1.02fr 0.98fr;
  gap: clamp(2rem, 4vw, 3.75rem);
  align-items: stretch;
}

/* Scaled down against the enlarged H1: the lockup now reads as the mark above
   the headline rather than competing with it for the same visual weight. */
.hero__logo {
  width: clamp(10rem, 13vw, 12.5rem);
  margin-bottom: 1.25rem;
}

/* The lead lines now live inside the h1 — see hero.html for why. Same visual
   treatment as the eyebrow block they replace. */
.hero__title-lead {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  margin-bottom: 1.125rem;
}

.hero__kicker {
  font-size: var(--text-eyebrow);
  font-weight: 700;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--cyan);
}

.hero__place {
  font-size: var(--text-meta);
  color: var(--color-text-inverse-muted);
}

/* The h1 is now just a container; the display treatment belongs to the line
   that carries it. Without this split the small lead lines would inherit
   132px serif. */
.hero__title {
  margin: 0;
  display: block;
}

.hero__title-main {
  display: block;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--text-h1);
  line-height: 0.98;
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
}

.hero__lede {
  margin: var(--space-lg) 0 0;
  max-width: 32.5rem;
  font-size: var(--text-lead);
  line-height: 1.6;
  color: var(--color-text-inverse-body);
}

.hero__actions { margin-top: 2.25rem; }

/* The global `* { margin: 0 }` reset means a bare <p> contributes no spacing,
   so the rating badge sat flush against the button row. */
.hero__rating { margin-top: 2rem; }

/* --- Photo rotator -------------------------------------------------------- */

.rotator {
  position: relative;
  height: 100%;
  min-height: 30rem;
  overflow: hidden;
  border-radius: var(--radius-control);
  /* Striped placeholder so the frame reads as intentional before the first
     photo decodes. */
  background: repeating-linear-gradient(122deg, #171C23 0 22px, #1E242D 22px 44px);
}

.rotator__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--duration-slow) ease;
}

.rotator__slide.is-active { opacity: 1; }

.rotator__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(14, 17, 22, 0) 55%, #101A2C 100%);
}

.rotator__controls {
  position: absolute;
  left: 1.375rem;
  right: 1.375rem;
  bottom: 1.375rem;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
}

.rotator__dots {
  display: flex;
  align-items: center;
  /* No gap: the 24px targets below supply the spacing themselves. */
  gap: 0;
}

/*
  The visible indicator is a 3px bar, but 2.5.8 measures the TARGET. Each
  button is padded out to at least 24x24 so the row passes on its own rather
  than leaning on the spacing exemption — at the design's 8px bar with a 7px
  gap the centres sat 15px apart and failed.
*/
.rotator__dot {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  min-height: 1.5rem;
  padding: 0.65625rem 0.5rem;
}

.rotator__dot::before {
  content: "";
  display: block;
  height: 3px;
  width: 0.5rem;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.55);
  transition: width 0.5s ease, background-color 0.5s ease;
}

.rotator__dot[aria-current="true"]::before {
  width: 1.625rem;
  background: var(--cyan);
}

/* Pause/play — WCAG 2.2.2. The rotator auto-advances every 5.2s indefinitely,
   which needs a mechanism to stop it. ADDITION beyond the supplied design,
   flagged in the handoff notes; sized and toned to sit with the dots. */
.rotator__pause {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  color: var(--white);
  /* Opaque enough that the control's boundary holds against ANY photo
     underneath it, not just the six currently in the rotator. */
  background: rgba(14, 17, 22, 0.78);
  border: 1px solid rgba(255, 255, 255, 0.72);
  border-radius: var(--radius-control);
  transition: background-color var(--duration-fast) ease;
}

.rotator__pause:hover { background: rgba(14, 17, 22, 0.85); }
.rotator__pause svg { width: 0.625rem; height: 0.625rem; fill: currentColor; }
.rotator__pause .rotator__icon-play { display: none; }
.rotator__pause[data-paused="true"] .rotator__icon-play { display: block; }
.rotator__pause[data-paused="true"] .rotator__icon-pause { display: none; }

/* --- Responsive ----------------------------------------------------------- */

@media (max-width: 62.5rem) {   /* 1000 */
  .hero__grid { grid-template-columns: 1fr; gap: var(--space-3xl); }
  .hero { min-height: 0; padding-block: 7.5rem var(--section-pad-sm); }
  .rotator { min-height: 24rem; height: 60vw; max-height: 32rem; }
}

@media (max-width: 45rem) {     /* 720 */
  .rotator { min-height: 18rem; height: 72vw; }
  .hero__logo { margin-bottom: var(--space-md); }
}



/* ---- answers.css ---- */
/* ==========================================================================
   COMPONENT: quick-answers strip
   The 1px grid gap IS the hairline — the grid's own background shows through.
   ========================================================================== */

.answers {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.14);
}

.answers__cell {
  background: var(--navy-800);
  padding: 1.875rem 1.625rem 2.125rem;
}

.answers__label {
  font-size: var(--text-micro);
  font-weight: 600;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--cyan);
  margin-bottom: var(--space-xs);
}

.answers__value {
  margin: 0;
  font-size: var(--text-body);
  line-height: 1.5;
  color: var(--color-text-inverse-strong);
}

@media (max-width: 56.25rem) { .answers { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 35rem)    { .answers { grid-template-columns: 1fr; } }



/* ---- transformation.css ---- */
/* ==========================================================================
   COMPONENT: the transformation mask reveal — the page's signature moment
   --------------------------------------------------------------------------
   The brand mark is a CSS mask over a finished-kitchen photo. On scroll JS
   grows `mask-size` from `auto 16%` to `auto 266%` of the container height,
   eased by a ^1.4 exponent, so the mark opens into the room.

   Growth is on the HEIGHT axis deliberately: the mark is taller than wide
   (535 x 588), so height-based scaling fills the frame edge to edge at the
   end. Width-based scaling leaves bands down both sides.

   The mask source must stay VECTOR — it scales roughly 16x and a raster mark
   visibly blurs.
   ========================================================================== */

.transformation__head { max-width: var(--container-narrow); margin-inline: auto; text-align: center; position: relative; z-index: 2; }

.transformation__eyebrow { margin-bottom: var(--space-md); }

.transformation__title {
  margin-inline: auto;
  max-width: 51.25rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--text-h2-large);
  line-height: 1.04;
  color: var(--white);
}

.transformation__lede {
  margin: var(--space-lg) auto 0;
  max-width: 37.5rem;
  font-size: var(--text-body-lg);
  line-height: var(--leading-body);
  color: var(--color-text-inverse-muted);
}

.transformation__reveal {
  position: relative;
  z-index: 1;
  margin: 4.375rem auto 0;
  max-width: var(--container-mask);
  height: 64vh;
  min-height: 27.5rem;
  background:
    var(--ink)
    url("../images/kitchen-cabinets-navy-wide-after.jpg")
    center / cover no-repeat;
  -webkit-mask-image: url("../images/logo-mask.svg");
          mask-image: url("../images/logo-mask.svg");
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: auto 16%;
          mask-size: auto 16%;
}

/*
  The reveal image is masked down to the logo silhouette, so only a fraction of
  it is ever visible. A phone has no use for the full-size file — and a CSS
  background cannot be reached by srcset, so the swap happens here.
*/
@media (max-width: 56.25rem) {
  .transformation__reveal {
    background-image: url("../images/kitchen-cabinets-navy-wide-after-800w.jpg");
  }
}

/* With reduced motion the mask never animates, so it is fixed at a size that
   reads as a finished image rather than a stranded 16% speck. */
@media (prefers-reduced-motion: reduce) {
  .transformation__reveal {
    -webkit-mask-size: auto 240%;
            mask-size: auto 240%;
  }
}

.transformation__actions { max-width: var(--container-mask); margin-inline: auto; }

@media (max-width: 45rem) {
  .transformation__reveal { height: 52vh; min-height: 20rem; }
}



/* ---- services.css ---- */
/* ==========================================================================
   COMPONENT: services — a vertical tablist
   Implemented as the ARIA tabs pattern (roving tabindex, arrow keys, Home/End)
   rather than as buttons that merely swap a div: the panels are alternative
   views of one region, which is exactly what tabs describe.
   ========================================================================== */

.services__body {
  display: grid;
  grid-template-columns: 22.5rem 1fr;
  gap: clamp(2rem, 4vw, 3.5rem);
  align-items: start;
}

.services__tabs {
  display: flex;
  flex-direction: column;
  border-top: 1px solid rgba(14, 17, 22, 0.12);
}

.services__tab {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2xs);
  padding: 1.625rem 1.375rem;
  border: 0;
  border-bottom: 1px solid rgba(14, 17, 22, 0.12);
  background: transparent;
  color: var(--ink);
  text-align: left;
  font-size: var(--text-tab);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  transition: background-color var(--duration-fast) ease, color var(--duration-fast) ease;
}

.services__tab:hover { background: rgba(14, 17, 22, 0.05); }

.services__tab[aria-selected="true"] {
  background: var(--ink);
  color: var(--white);
}

.services__note {
  margin: var(--space-lg) 2px 0;
  font-size: var(--text-meta);
  line-height: 1.6;
  color: var(--color-text-muted);
}

.services__panel {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.services__panel[hidden] { display: none; }

.services__lede {
  margin: 0 0 1.875rem;
  max-width: 47.5rem;
  font-family: var(--font-display);
  font-size: var(--text-service-lede);
  line-height: 1.24;
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

.services__photo {
  margin-top: 2.125rem;
  position: relative;
  height: 20rem;
  overflow: hidden;
  border-radius: var(--radius-control);
  background: repeating-linear-gradient(122deg, #EFEAE2 0 22px, #E4DDD2 22px 44px);
}

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

.services__photo img[data-focal="46"] { object-position: center 46%; }
.services__photo img[data-focal="62"] { object-position: center 62%; }

.services__cta { align-self: flex-start; margin-top: 2.125rem; }

/* --- Chip list (read-only item inventory, not a control) ----------------- */

.chip-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  margin: 0;
}

.chip {
  border: 1px solid var(--color-border-hairline);
  padding: 0.6875rem 1rem;
  font-size: var(--text-sm);
  letter-spacing: 0.02em;
  border-radius: var(--radius-control);
  color: var(--ink);
}

@media (max-width: 56.25rem) {   /* 900 */
  .services__body {
    grid-template-columns: 1fr;
    gap: var(--space-xl);
  }

  .services__cta { align-self: stretch; }

  /* Stacked, the "every project also includes…" note fell between the tabs
     and the panel they control, breaking that relationship. Move it below the
     panel so choosing a tab leads straight into its content. */
  .services__tabs-col { display: contents; }
  .services__tabs { order: 1; }
  .services__panels { order: 2; }
  .services__note { order: 3; margin-top: 0; }
}

@media (max-width: 30rem) {
  .services__photo { height: 14rem; }
  .services__tab { padding: 1.25rem 1rem; font-size: 1.25rem; }
}



/* ---- value-card.css ---- */
/* ==========================================================================
   COMPONENT: value-card
   Shares its resting and hover treatment with .review-card exactly — if one
   changes, change both.
   ========================================================================== */

/* Standalone section heading — owns its own width and spacing rather than
   borrowing .section-head, which collides with .headline. */
.values__title {
  max-width: 51.25rem;
  margin-bottom: clamp(2.25rem, 4.5vw, 3.375rem);
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-md);
}

.value-card {
  background: var(--white);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-card);
  padding: 2.25rem 1.875rem 2.5rem;
  min-height: 18.75rem;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-card);
  transition: box-shadow 0.3s ease, transform 0.3s ease;
}

.value-card:hover {
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-3px);
}

/* The brand splat, one per card, rotated and tinted so four identical shapes
   do not read as a repeat. Inline SVG with a fill attribute, never a CSS
   mask: a failed mask renders as a solid coloured square, which is worse than
   no mark at all. */
.value-card__mark {
  width: 2.5rem;
  height: 2.75rem;
  margin-bottom: var(--space-lg);
  overflow: visible;
}

.value-card:nth-child(1) .value-card__mark { fill: var(--cyan);       transform: rotate(0deg); }
.value-card:nth-child(2) .value-card__mark { fill: var(--brand-blue); transform: rotate(128deg); }
.value-card:nth-child(3) .value-card__mark { fill: var(--cyan);       transform: rotate(214deg); }
.value-card:nth-child(4) .value-card__mark { fill: var(--brand-blue); transform: rotate(302deg); }

/* Two of these four titles wrap to a second line and two do not, which left
   the body copy starting at different heights across the row. Reserving two
   lines keeps the bodies on a common baseline. Released back to auto once the
   cards stack and there is no row to align to. */
.value-card__title {
  min-height: calc(2 * 1.2em);
  margin: 0 0 0.875rem;
  font-family: var(--font-sans);
  font-size: var(--text-h3-card);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  line-height: 1.2;
  color: var(--ink);
}

.value-card__body {
  margin: 0;
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
  color: var(--color-text-muted);
}

@media (max-width: 68.75rem) { /* Standalone section heading — owns its own width and spacing rather than
   borrowing .section-head, which collides with .headline. */
.values__title {
  max-width: 51.25rem;
  margin-bottom: clamp(2.25rem, 4.5vw, 3.375rem);
}

.card-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 38.75rem) {
  /* Standalone section heading — owns its own width and spacing rather than
   borrowing .section-head, which collides with .headline. */
.values__title {
  max-width: 51.25rem;
  margin-bottom: clamp(2.25rem, 4.5vw, 3.375rem);
}

.card-grid { grid-template-columns: 1fr; }
  .value-card { min-height: 0; }
}



/* ---- process.css ---- */
/* ==========================================================================
   COMPONENT: process steps
   ========================================================================== */

.process__watermark {
  position: absolute;
  top: 52%;
  left: 0;
  width: 11.875rem;
  opacity: 0.24;
  pointer-events: none;
  transform: translate(-30%, -50%);
}

.process__inner { position: relative; }

.step-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-xl);
}

.step { border-top: 2px solid rgba(255, 255, 255, 0.28); padding-top: 1.375rem; }

.step__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-xs);
  margin-bottom: 2.125rem;
}

/* Spelled-out numerals in the display serif are the intended treatment —
   digits 01-04 were removed site-wide. */
.step__num {
  font-family: var(--font-display);
  font-size: var(--text-figure);
  line-height: 0.9;
  color: var(--cyan-light);
}

.step__arrow {
  font-size: 1.375rem;
  line-height: 1;
  color: rgba(255, 255, 255, 0.62);
}

.step__title {
  margin: 0 0 var(--space-xs);
  font-family: var(--font-sans);
  font-size: var(--text-h3);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
}

.step__body {
  margin: 0;
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
  color: var(--color-text-inverse-body);
}

@media (max-width: 68.75rem) { .step-grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-2xl) var(--space-xl); } }
@media (max-width: 38.75rem) {
  .step-grid { grid-template-columns: 1fr; }
  .process__watermark { width: 7.5rem; }
}



/* ---- work.css ---- */
/* ==========================================================================
   COMPONENT: work stack
   The photos carry a continuous scroll-linked zoom across the whole `cover`
   range, so they are never static while the section is on screen.
   ========================================================================== */

.work__grid {
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  gap: var(--space-md);
}

.work__figure {
  position: relative;
  margin: 0;
  overflow: hidden;
  border-radius: var(--radius-control);
  background: repeating-linear-gradient(122deg, #171C23 0 22px, #1E242D 22px 44px);
}

.work__figure--tall { grid-row: span 2; height: 35rem; }
.work__figure--short { height: 16.875rem; }

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

.work__caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 1.375rem 1.5rem;
  display: flex;
  justify-content: space-between;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--white);
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.72));
}

.work__caption-title { font-weight: 600; }
.work__caption-place { color: rgba(255, 255, 255, 0.82); }

@media (max-width: 56.25rem) {
  .work__grid { grid-template-columns: 1fr; }
  .work__figure--tall { grid-row: auto; height: 26rem; }
  .work__figure--short { height: 15rem; }
}

.work__title { max-width: 40rem; margin-bottom: var(--space-2xl); }



/* ---- statement.css ---- */
/* ==========================================================================
   COMPONENT: statement — one typographic beat between the work and the reviews
   ========================================================================== */

.statement__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-3xl);
  align-items: end;
}

.statement__body { max-width: 62.5rem; }

.statement__rule {
  display: block;
  width: 4rem;
  height: 2px;
  background: var(--cyan);
  margin-bottom: 2.125rem;
}

.statement__setup {
  margin: 0;
  font-size: var(--text-statement-setup);
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.62);
  max-width: 32.5rem;
}

.statement__payoff {
  margin: 1.125rem 0 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--text-statement);
  line-height: 1.02;
  letter-spacing: -0.02em;
  text-wrap: pretty;
}

.statement__payoff em { font-style: italic; }

/* Cyan, not the gradient mark — the gradient version is deep navy and
   disappears entirely on black. */
.statement__mark {
  width: clamp(5.25rem, 8.5vw, 8.75rem);
  fill: var(--cyan);
  opacity: 0.4;
  margin-bottom: var(--space-xs);
  overflow: visible;
}

@media (max-width: 47.5rem) {
  .statement__grid { grid-template-columns: 1fr; gap: var(--space-xl); }
  .statement__mark { margin-bottom: 0; }
}



/* ---- google-badge.css ---- */
/* ==========================================================================
   COMPONENT: Google attribution
   --------------------------------------------------------------------------
   Built from real assets rather than text approximations — that is what makes
   the rating read as credible rather than as a claim.

   The star path is defined ONCE in a hidden <svg><defs> at the top of the
   document and referenced with <use href="#ct-star">. Repeating the path per
   card produces duplicate DOM ids.

   The review count degrades on purpose: with a count it reads
   "12 Google reviews", without one it reads "Google reviews". It must never
   render a dash, a zero, or an empty string.
   ========================================================================== */

.stars { display: block; fill: var(--google-gold); }

/* Contrast note (1.4.11): Google's gold measures ~1.8:1 on white. It is kept
   verbatim because it is Google's own brand colour and altering it
   misrepresents the source — so the rating is ALSO carried in adjacent visible
   text on every instance, and the row has an accessible name. The stars are
   therefore decoration of a value stated in words, not the only carrier of it.
   Do not remove the visible rating text next to any star row. */

/*
  The badge scales from ONE number. Every internal measurement is in `em`, so
  changing --badge-scale resizes the mark, the stars, the padding and the gaps
  together and in proportion. Currently 0.85 — the design's size less 15%.
*/
.google-badge {
  --badge-scale: 0.85;

  display: inline-flex;
  align-items: center;
  font-size: calc(var(--text-meta) * var(--badge-scale));
  gap: 0.897em;
  padding: 0.828em 1.241em 0.828em 0.966em;
  border: 1px solid var(--color-border-strong-inverse);
  border-radius: var(--radius-card);
  background: rgba(255, 255, 255, 0.05);
  color: var(--white);
  transition: background-color var(--duration-fast) ease, border-color var(--duration-fast) ease;
}

.google-badge:hover {
  border-color: rgba(255, 255, 255, 0.8);
  background: rgba(255, 255, 255, 0.12);
}

.google-badge__mark { flex: none; width: 1.379em; height: 1.379em; }

/* The SVG carries width/height attributes for the unscaled case; these
   override them so the stars scale with the badge. */
.google-badge__stars { flex: none; width: 5.931em; height: 1.034em; }

.google-badge__text {
  font-size: 1em;
  white-space: nowrap;
}

.google-badge__rating { font-weight: 700; }
.google-badge__count { color: var(--color-text-inverse-body); }

/* --- Header rating block in the reviews section -------------------------- */

.rating-badge {
  display: flex;
  align-items: center;
  gap: 1.375rem;
  border: 1px solid var(--color-border-strong-inverse);
  padding: 1.375rem 1.75rem;
  border-radius: var(--radius-card);
  color: var(--white);
}

.rating-badge__figure {
  font-family: var(--font-display);
  font-size: var(--text-figure);
  line-height: 1;
}

.rating-badge__meta {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  margin-top: 0.5625rem;
}

.rating-badge__meta svg { flex: none; }

.rating-badge__link {
  font-size: var(--text-chip);
  color: var(--color-text-inverse-body);
  text-decoration: underline;
  text-underline-offset: 3px;
  padding-block: 0.25rem;
}

.rating-badge__link:hover { color: var(--white); }



/* ---- reviews.css ---- */
/* ==========================================================================
   COMPONENT: review-card
   Resting and hover treatment matches .value-card exactly.
   ========================================================================== */

/*
  Black behind the white header, paper by the time the cards begin.

  25 stops. The handoff's own seven colours are all still here; the extras are
  Lab-interpolated along the same path, and every position is chosen so
  PERCEIVED lightness follows a smootherstep across the band — gentle out of
  the black, gentle into the paper, the change carried through the middle.

  Two things made the earlier version read as a hard edge rather than a
  descent: the band was only 200px for a full black-to-paper journey, and it
  left the flat black with an abrupt change in rate. The band is now 192-352px
  and eases at both ends.

  Positions are `calc(100% - Npx)` so the whole transition sits at the BOTTOM
  of this wrapper — which is the empty space under the header. Above
  that band the ground is solid #0E1116 no matter how tall the header grows
  when the rating badge wraps below the headline on a narrow screen. That is
  what keeps the white heading off a mid-grey ground at every width.
*/
.reviews__ramp {
  /* Long enough for the descent to stay smooth, short enough not to read as
     dead air. At 22rem the gap from heading to cards measured 390px against
     28-38px elsewhere on the page — the ramp had stopped being a transition
     and become a hole. The smootherstep easing is what lets it work at this
     shorter length: the earlier hard edge came from the abrupt START of the
     descent, not from the distance. */
  --reviews-ramp: clamp(9rem, 16vw, 14rem);

  /* FULL BLEED. The section carries `padding-inline: var(--gutter)`, so a
     plain child stops 32px short of the viewport on each side and the dark
     ground reads as a wide band with paper gutters down both edges. Cancel
     the section's padding with a negative margin, then re-apply the same
     gutter as this element's own padding so the header content stays aligned
     with every other section on the page. */
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  padding-block: 1.25rem var(--reviews-ramp);
  background: linear-gradient(180deg,
    #0E1116 0,
    #0E1116 calc(100% - var(--reviews-ramp)),
    #111419 calc(100% - var(--reviews-ramp) * 0.8822),
    #13161C calc(100% - var(--reviews-ramp) * 0.8552),
    #14191F calc(100% - var(--reviews-ramp) * 0.8282),
    #161B22 calc(100% - var(--reviews-ramp) * 0.8106),
    #1F2328 calc(100% - var(--reviews-ramp) * 0.7598),
    #282B2F calc(100% - var(--reviews-ramp) * 0.7215),
    #313435 calc(100% - var(--reviews-ramp) * 0.6872),
    #3A3D3C calc(100% - var(--reviews-ramp) * 0.6571),
    #4A4C4A calc(100% - var(--reviews-ramp) * 0.6120),
    #5B5C59 calc(100% - var(--reviews-ramp) * 0.5694),
    #6C6C68 calc(100% - var(--reviews-ramp) * 0.5299),
    #7E7D77 calc(100% - var(--reviews-ramp) * 0.4900),
    #8C8B84 calc(100% - var(--reviews-ramp) * 0.4580),
    #9B9992 calc(100% - var(--reviews-ramp) * 0.4253),
    #A9A7A0 calc(100% - var(--reviews-ramp) * 0.3924),
    #B8B5AE calc(100% - var(--reviews-ramp) * 0.3573),
    #C2BFB9 calc(100% - var(--reviews-ramp) * 0.3309),
    #CDCAC3 calc(100% - var(--reviews-ramp) * 0.2998),
    #D7D4CE calc(100% - var(--reviews-ramp) * 0.2679),
    #E2DFD9 calc(100% - var(--reviews-ramp) * 0.2266),
    #E7E4DF calc(100% - var(--reviews-ramp) * 0.2037),
    #ECE9E4 calc(100% - var(--reviews-ramp) * 0.1766),
    #F1EFEA calc(100% - var(--reviews-ramp) * 0.1337),
    #F6F4F0 100%);
}

.reviews__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.75rem;
  margin-bottom: 0;
  color: var(--white);
}

.reviews__head-text { max-width: 45rem; }

.review-grid {
  /* The ramp's own bottom padding is the separation; this is a small breath
     on top of it, not a second gap. */
  margin-top: var(--space-md);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-md);
}

.review-card {
  background: var(--white);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-card);
  padding: 1.875rem 1.875rem 1.625rem;
  min-height: 17.5rem;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-card);
  transition: box-shadow 0.3s ease, transform 0.3s ease;
}

.review-card:hover {
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-3px);
}

.review-card__head {
  display: flex;
  align-items: center;
  gap: 0.8125rem;
}

.review-card__avatar {
  flex: none;
  width: 2.625rem;
  height: 2.625rem;
  border-radius: var(--radius-round);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.0625rem;
  font-weight: 600;
}

.review-card:nth-child(1) .review-card__avatar { background: var(--avatar-1); }
.review-card:nth-child(2) .review-card__avatar { background: var(--avatar-2); }
.review-card:nth-child(3) .review-card__avatar { background: var(--avatar-3); color: var(--ink-deep); }

.review-card__id { flex: 1; min-width: 0; }

.review-card__name {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--ink);
}

.review-card__source {
  font-size: var(--text-eyebrow);
  color: var(--color-text-muted);
  margin-top: 2px;
}

.review-card__google { flex: none; width: 1.1875rem; height: 1.1875rem; }

.review-card__rating {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  margin: var(--space-md) 0 1rem;
}

.review-card__rating-value {
  font-size: var(--text-eyebrow);
  font-weight: 600;
  color: var(--color-text-muted);
}

.review-card__quote {
  margin: 0;
  font-size: var(--text-quote);
  line-height: 1.62;
  color: rgba(14, 17, 22, 0.86);
  text-wrap: pretty;
}

@media (max-width: 68.75rem) { .review-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 47.5rem)  {
  .review-grid { grid-template-columns: 1fr; }
  .review-card { min-height: 0; }
  .rating-badge { width: 100%; }
}



/* ---- estimate.css ---- */
/* ==========================================================================
   COMPONENT: estimate form
   --------------------------------------------------------------------------
   Static placeholder markup — action="#", no endpoint. At stage 2 this becomes
   a Gravity Form; Gravity Forms renders its own markup, so visual parity there
   means CSS targeting GF's own classes, not reusing these.
   ========================================================================== */

.estimate__grid {
  display: grid;
  grid-template-columns: 1fr 1.05fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}

.estimate__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--text-h2-estimate);
  line-height: 1.03;
}

.estimate__lede {
  margin: 1.5rem 0 0;
  font-size: var(--text-body-lg);
  line-height: var(--leading-body);
  color: var(--color-text-muted);
  max-width: 28.75rem;
}

.estimate__contact {
  margin-top: 2.5rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}

.estimate__phone {
  font-family: var(--font-display);
  font-size: var(--text-phone);
  letter-spacing: var(--tracking-tight);
  color: var(--brand-blue);
  align-self: flex-start;
}

.estimate__phone:hover { color: var(--cyan-deep); }

.estimate__address {
  font-size: var(--text-meta);
  color: var(--color-text-muted);
  font-style: normal;
}

.estimate__mark { margin-top: 3.125rem; width: 6.5rem; opacity: 0.9; }

/*
  The form is the page's single conversion point, and as a #F6F4F0 panel on a
  #FFFFFF section it was a shade away from invisible. It is now a dark panel:
  the strongest contrast available on a light section, and consistent with a
  page that alternates dark and light the whole way down.

  Everything inside it inverts with it — see the block below. The inputs stay
  WHITE on purpose: a dark field on a dark panel reads as decoration, whereas a
  white one reads as something to fill in.
*/
/*
  White focus ring inside this panel, not the usual cyan. Cyan is now the
  panel's SELECTED and SUBMIT colour, so a cyan ring around a cyan chip reads
  as part of the fill rather than as focus. The ring sits 3px outside the
  control, on the navy, so it stays visible around the white inputs too.
*/
.estimate__form {
  --color-focus-ring: var(--white);

  position: relative;
  background: var(--navy-700);
  border: 0;
  border-radius: var(--radius-card);
  padding: 2.75rem 2.5rem 2.5rem;
  box-shadow: 0 18px 48px rgba(14, 17, 22, 0.18);
}

/* Cyan rule across the top — the same accent that marks every primary action
   on the page, used here to say "this is the one that matters". */
.estimate__form::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--cyan);
  border-radius: var(--radius-card) var(--radius-card) 0 0;
}

/* --- Inverted form furniture -------------------------------------------- */

.estimate__form .field__label,
.estimate__form .chip-group__legend {
  color: var(--cyan-light);
}

.estimate__form .field__required { color: var(--cyan); }

.estimate__form .field__hint { color: var(--color-text-inverse-muted); }

.estimate__form .field__control {
  border-color: transparent;
  background: var(--white);
  color: var(--ink);
}

.estimate__form .field__control:hover { border-color: var(--cyan); }

.estimate__form .field__control::placeholder { color: #6B7280; }

/* Unselected chips read as outlines on the dark panel; the selected one takes
   the cyan fill so the current choice is unmistakable. */
.estimate__form .chip-toggle__label {
  background: transparent;
  border-color: var(--color-border-strong-inverse);
  color: var(--white);
}

.estimate__form .chip-toggle__label:hover {
  border-color: var(--white);
  background: rgba(255, 255, 255, 0.08);
}

.estimate__form .chip-toggle__input:checked + .chip-toggle__label {
  background: var(--cyan);
  border-color: var(--cyan);
  color: var(--ink-deep);
}

/* The submit was ink, which would vanish on this panel. */
.estimate__form .btn--block {
  background: var(--cyan);
  border-color: var(--cyan);
  color: var(--ink-deep);
}

.estimate__form .btn--block:hover {
  background: var(--white);
  border-color: var(--white);
  color: var(--ink);
}

.estimate__form .estimate__success-title { color: var(--white); }
.estimate__form .estimate__success-body { color: var(--color-text-inverse-body); }

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.125rem;
}

.form-row > * { min-width: 0; }

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
}

.field__label {
  font-size: var(--text-label);
  font-weight: 600;
  letter-spacing: var(--tracking-field);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.field__required {
  color: var(--brand-blue);
  margin-left: 2px;
}

/* 1.4.11 — the input boundary is an interactive control's boundary and needs
   >= 3:1. The design's rgba(14,17,22,.2) measures ~1.5:1 on the paper ground
   and fails; --color-border-strong is the compliant equivalent at the same
   visual weight. */
.field__control {
  border: 1px solid var(--color-border-strong);
  background: var(--white);
  padding: 0.875rem;
  /* 16px minimum, or iOS Safari zooms the page on focus. */
  font-size: var(--text-body);
  border-radius: var(--radius-control);
  color: var(--ink);
}

.field__control:hover { border-color: var(--ink); }

textarea.field__control { resize: vertical; min-height: 6.5rem; }

.field--stacked { margin-top: 1.375rem; }

.field__hint {
  font-size: var(--text-eyebrow);
  color: var(--color-text-muted);
}

/* --- Project type: a single-select radio group ---------------------------
   The design draws these as toggle chips. They are implemented as radios in a
   fieldset with a legend, styled to match exactly: single-select IS a radio
   group, and only a real input carries a value into the submission, announces
   the question it belongs to, and supports arrow-key selection.
   -------------------------------------------------------------------------- */

.chip-group { margin-top: 1.375rem; }

.chip-group__legend {
  font-size: var(--text-label);
  font-weight: 600;
  letter-spacing: var(--tracking-field);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.chip-group__options {
  margin-top: var(--space-xs);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.chip-toggle { display: inline-flex; }

.chip-toggle__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.chip-toggle__label {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--color-border-strong);
  background: var(--white);
  color: var(--ink);
  padding: 0.6875rem 0.9375rem;
  font-size: var(--text-chip);
  border-radius: var(--radius-control);
  cursor: pointer;
  transition: background-color var(--duration-fast) ease, color var(--duration-fast) ease, border-color var(--duration-fast) ease;
}

.chip-toggle__label:hover { border-color: var(--ink); }

.chip-toggle__input:checked + .chip-toggle__label {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--white);
}

/* --- Success state -------------------------------------------------------- */

.estimate__success { padding: 2.5rem 0; text-align: center; }

.estimate__success[hidden] { display: none; }

.estimate__success-title {
  font-family: var(--font-display);
  font-size: 2.125rem;
  line-height: 1.2;
  margin-bottom: var(--space-xs);
  color: var(--ink);
}

.estimate__success-body {
  margin: 0;
  font-size: var(--text-body);
  color: var(--color-text-muted);
}

@media (max-width: 56.25rem) { .estimate__grid { grid-template-columns: 1fr; } }
@media (max-width: 33.75rem) {
  .form-row { grid-template-columns: 1fr; }
  .estimate__form { padding: 1.75rem 1.5rem 1.5rem; }
}

/* Submit failure. Sits above the button so it cannot be missed, and uses a
   colour that reads as a problem against the dark panel rather than as another
   accent. */
.estimate__error {
  margin-top: 1.375rem;
  padding: 0.875rem 1rem;
  border-left: 3px solid #FF8A7A;
  background: rgba(255, 138, 122, 0.12);
  color: #FFD9D3;
  font-size: var(--text-body-sm);
  line-height: 1.5;
}

.estimate__error[hidden] { display: none; }

.estimate__form .btn--block[aria-busy="true"] { opacity: 0.75; cursor: progress; }

/* Turnstile. The widget renders in its own iframe, so it cannot inherit the
   panel's type — dark theme is set on the element itself in the markup. This
   only reserves its space so nothing jumps when it finishes loading. */
.estimate__challenge {
  margin-top: 1.375rem;
  min-height: 65px;
}



/* ---- faq.css ---- */
/* ==========================================================================
   COMPONENT: faq accordion — single-open
   Buttons with aria-expanded/aria-controls rather than <details>: the panel
   animates via a 0fr -> 1fr grid row, which <details> cannot do without
   scripting its own open state anyway.
   ========================================================================== */

/* Standalone section heading — see .values__title. */
.faq__title { margin-bottom: clamp(1.75rem, 3.5vw, 2.75rem); }

.faq__list { border-top: 1px solid var(--color-border-hairline); }

.faq__item { border-bottom: 1px solid var(--color-border-hairline); }

.faq__question {
  width: 100%;
  background: none;
  border: 0;
  padding: 1.625rem 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1.5rem;
  text-align: left;
  font-size: 1.25rem;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink);
}

.faq__question:hover { color: var(--brand-blue); }

.faq__icon {
  flex: none;
  font-size: 1.625rem;
  line-height: 1;
  color: var(--cyan-deep);
  transition: transform var(--duration-fast) ease;
}

.faq__question[aria-expanded="true"] .faq__icon { transform: rotate(45deg); }

/*
  Open by default, collapsed only when a question is explicitly closed.

  That ordering is deliberate. The markup ships every question expanded, so
  with JS switched off all eight answers are readable; main.js then collapses
  to single-open on load. The reverse default would hide seven answers from
  anyone whose script never runs.

  The selector matches the ITEM, not the button: the button lives inside an
  <h3> (so the accordion appears in the document outline), which makes the
  panel a sibling of the heading rather than of the button. An adjacent-sibling
  selector silently never matches here.
*/
.faq__panel {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 0.3s ease;
}

.faq__item:has(.faq__question[aria-expanded="false"]) .faq__panel {
  grid-template-rows: 0fr;
}

.faq__panel-inner { min-height: 0; overflow: hidden; }

.faq__answer {
  margin: 0;
  padding: 0 0 1.75rem;
  max-width: 47.5rem;
  font-size: var(--text-quote);
  line-height: 1.7;
  color: var(--color-text-muted);
}

/* A pending answer renders lighter on purpose — a visible signal that the
   copy is not final. Detected in the markup by data-status="placeholder". */
/* 4.71:1 on the FAQ section's end colour — the lighter #6A7078 measured 4.24. */
.faq__answer[data-status="placeholder"] { color: #63696F; font-style: italic; }

@media (max-width: 30rem) {
  .faq__question { font-size: 1.0625rem; padding: 1.25rem 0; }
}



/* ---- site-footer.css ---- */
/* ==========================================================================
   COMPONENT: site-footer
   --------------------------------------------------------------------------
   The background is an eased 11-stop paper -> black ramp and the deep top
   padding is what gives it room to travel. A 2-stop ramp bands visibly here;
   keep every intermediate stop.
   ========================================================================== */

/*
  Same fix as the process section: the paper -> navy ramp is bound to the
  footer's TOP PADDING in a fixed-height layer rather than expressed as a
  percentage of a footer whose height triples when the columns stack.

  The stops are spaced at perceptually even steps — computed by interpolating
  from #EFECE6 to #132238 in CIELAB with a smootherstep on position, then
  converted back to sRGB. That matters because browsers interpolate BETWEEN
  stops in sRGB: evenly spaced sRGB values crowd the perceived change into the
  midtones and flatten both ends, which is what made the earlier ramp read as
  a hard band rather than a descent. The path still passes within a shade of
  the handoff's own greys (#BDBDBE vs #B9B7B3, #898C95 vs #8E8D8C).

  It reaches the base at 92% and holds, so a little settled navy sits under
  the ramp before the footer columns begin.

  A 2-stop ramp bands visibly here — do not simplify it. If you change the
  endpoint, regenerate the stops rather than editing them by hand.
*/
.site-footer {
  --footer-ramp: var(--section-pad-footer);
  /* Navy 700, not ink. The near-black base read as flat and dead under the
     footer's grey ramp; the navy keeps a trace of the brand blue in the
     deepest part of the page. Taken from the design palette, not invented. */
  background: var(--navy-700);
  color: var(--color-text-inverse);
  padding: var(--section-pad-footer) var(--gutter) 2.5rem;
  position: relative;
}

.site-footer::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--footer-ramp);
  pointer-events: none;
  background: linear-gradient(180deg,
    #EFECE6 0%,
    #EEEBE5 6.1%,
    #EAE8E2 12.3%,
    #E1DFDB 18.4%,
    #D2D0CF 24.5%,
    #BDBDBE 30.7%,
    #A4A5AB 36.8%,
    #898C95 42.9%,
    #6E737F 49.1%,
    #545B6B 55.2%,
    #3E4759 61.3%,
    #2C374B 67.5%,
    #1F2C41 73.6%,
    #17253B 79.7%,
    #142238 85.9%,
    #132238 92%,
    #132238 100%);
}

.site-footer > .container { position: relative; }

.site-footer__grid {
  display: grid;
  grid-template-columns: 1.3fr 1fr 1fr 1fr;
  gap: var(--space-2xl);
  padding-bottom: var(--space-3xl);
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}

.site-footer__logo { width: 15.75rem; max-width: 100%; margin-bottom: 1.125rem; }

.site-footer__tagline {
  margin: 0.875rem 0 0;
  font-family: var(--font-display);
  font-size: 1.375rem;
  color: var(--cyan-light);
}

.site-footer__heading {
  font-size: var(--text-micro);
  font-weight: 600;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.62);
  margin-bottom: 1.125rem;
}

.site-footer__list {
  display: flex;
  flex-direction: column;
  gap: 0.5625rem;
  font-size: 0.9375rem;
  color: var(--color-text-inverse-body);
}

.site-footer__list--contact { gap: 0.625rem; font-size: var(--text-body-sm); }

.site-footer__phone { color: var(--white); font-weight: 600; align-self: flex-start; }
.site-footer__phone:hover { text-decoration: underline; text-underline-offset: 4px; }

.site-footer__cta { color: var(--cyan-light); align-self: flex-start; }
.site-footer__cta:hover { text-decoration: underline; text-underline-offset: 4px; }

.site-footer__address { font-style: normal; }

.site-footer__bottom {
  padding-top: 1.625rem;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.875rem;
  font-size: var(--text-xs);
  color: rgba(255, 255, 255, 0.62);
}

.site-footer__credit {
  font-family: var(--font-mono);
  letter-spacing: 0.1em;
}

@media (max-width: 56.25rem) { .site-footer__grid { grid-template-columns: 1fr 1fr; gap: var(--space-xl); } }
@media (max-width: 35rem) {
  .site-footer__grid { grid-template-columns: 1fr; }
  .site-footer__logo { width: 13rem; }
}



/* ---- mobile-cta.css ---- */
/* ==========================================================================
   COMPONENT: mobile CTA bar — <= 720px only
   Shown via a media query, not JS, so it is correct before scripts run and
   correct if they never do.
   ========================================================================== */

.mobile-cta {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-mobile-cta);
  display: none;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: rgba(255, 255, 255, 0.2);
}

.mobile-cta__link {
  padding: 1.125rem;
  text-align: center;
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: var(--tracking-button);
  text-transform: uppercase;
}

.mobile-cta__link--call { background: var(--ink); color: var(--white); }
.mobile-cta__link--estimate { background: var(--cyan); color: var(--ink-deep); }

@media (max-width: 45rem) { .mobile-cta { display: grid; } }

/*
  The bar offers the same two actions as the hero's own buttons, and on a
  short phone it sits on top of them: at 375x812 it covered the lower 21px of
  "Call 630-877-8441" and all of the Google rating beneath it. Fitting that
  content above the bar would need ~108px of tightening and would fail again
  on a narrower phone, so the bar gets out of the way instead — it stays
  tucked below the fold until the hero's buttons scroll out of reach, then
  slides up.

  The hidden pre-state is scoped to .js-cta, which main.js adds. With scripting
  off the bar behaves exactly as it always did: visible, from first paint.
*/
.js-cta .mobile-cta {
  transform: translateY(100%);
  transition: transform 0.28s var(--ease-entrance);
}

.js-cta .mobile-cta.is-visible { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js-cta .mobile-cta { transition: none; }
}

/* At 360px and below "Free Estimate" wraps to two lines, which makes the bar
   taller than the body's reserved bottom padding and lets it cover content. */
@media (max-width: 22.5rem) {
  .mobile-cta__link {
    font-size: var(--text-xs);
    letter-spacing: 0.04em;
    padding-inline: 0.5rem;
  }
}




/* ---- utilities.css ---- */
/* ==========================================================================
   UTILITIES — last in the cascade, deliberately few.
   A utility here is a genuinely cross-cutting concern. Anything that belongs
   to one component belongs in that component's file.
   ========================================================================== */

.u-flow-lg > * + * { margin-top: var(--space-3xl); }

.u-text-inverse-muted { color: var(--color-text-inverse-muted); }

.u-mark-decorative { pointer-events: none; }

/* Horizontally scrollable regions must be keyboard reachable and named — see
   the wrapper's role="region" tabindex="0" aria-label in the markup. */
.u-scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}


