/* ==========================================================================
   Response Roofing — main.css
   Response You Can Count On.

   Build notes
   - No frameworks. Vanilla CSS, custom properties, logical grouping.
   - Brand teal (#28939D) is used ONLY where it does not carry text.
     Text-bearing teal uses --teal-action (5.38:1 on white) or
     --teal-light (7.78:1 on black). See "Color" below.
   - Every path in this file is RELATIVE (../images/...), so the whole
     folder can be moved between the test server and the live domain
     without a single edit. See global/config.php for link handling.
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. FONT
   Archivo Variable, latin subset, width 62-125 + weight 100-900.
   One file, 44.7 KB, SIL Open Font License 1.1.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'Archivo';
  src: url('../fonts/archivo-variable-subset.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-stretch: 62% 125%;
  font-style: normal;
  font-display: swap;
}


/* --------------------------------------------------------------------------
   2. TOKENS
   -------------------------------------------------------------------------- */

:root {
  /* Color  ---------------------------------------------------------------- */
  --black:        #231F20;   /* brand black, PMS-style rich black */
  --black-soft:   #2E2A2B;   /* raised surfaces on black */
  /* The Get Quote / Thank You head. Five points above --black: enough that
     the solid black trust bar below reads as its own band, not enough to
     read as a different color. Client's call, 24 Aug — the previous gradient
     lifted to +9 at the seam and that was too much. Change this ONE value to
     tune the seam; nothing else needs touching. */
  --black-head:   #282425;

  /* Hairlines on a black surface. ** THIS WAS RAISED FROM .12 TO .22 ON
     26 Aug. ** At 12% over --black the line composites to about rgb(61,58,58)
     against rgb(35,31,32) - roughly a 26/255 difference. That is enough to
     register as a seam on a good desktop panel and not enough to see on a
     phone in daylight, which is exactly what the client reported: "horizontal
     line should show". 22% roughly doubles the separation.
     If a divider on black ever needs adjusting again, adjust it HERE - the
     desktop column divider, the mobile rules and the steps band all read from
     this one value and must not drift apart. */
  --rule-on-black: rgba(255,255,255,.22);
  --white:        #FFFFFF;
  --slate:        #D9D9D6;   /* brand Soft Slate, PMS Cool Gray 1 */
  --slate-mid:    #EEEEEC;
  --slate-deep:   #6B6F70;   /* on WHITE only — 5.9:1. Fails on black.      */
  --slate-muted:  #9BA0A1;   /* on BLACK only — 6.16:1, WCAG AA             */

  --teal-brand:   #28939D;   /* LOGO / GRAPHIC ONLY — 3.65:1, never behind text */
  --teal-action:  #1F757E;   /* buttons + links on white — 5.38:1, WCAG AA    */
  --teal-light:   #4EC3CE;   /* links + fills on black  — 7.78:1, WCAG AAA    */
  --teal-pale:    #8FE0E7;   /* hover on the frosted header — 10.85:1 on black */
  --teal-wash:    #F1F7F8;   /* tinted section background                     */
  --wash-1:       #EAF4F5;   /* coastal wash ramp — light                     */
  --wash-2:       #E0EEF0;   /* coastal wash ramp — mid                       */
  --wash-3:       #F7FBFB;   /* coastal wash ramp — near-white                */

  --ink:          #231F20;
  --ink-soft:     #484344;   /* body copy on white — 9.4:1                    */

  /* Type ------------------------------------------------------------------ */
  --font: 'Archivo', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  --wd-hero:   125;   /* display width axis */
  --wd-head:   118;
  --wd-sub:    105;
  --wd-body:    92;
  --wd-ui:     100;

  --step--1: clamp(0.83rem, 0.80rem + 0.15vw, 0.92rem);
  --step-0:  clamp(1.00rem, 0.96rem + 0.20vw, 1.10rem);
  --step-1:  clamp(1.20rem, 1.12rem + 0.40vw, 1.44rem);
  --step-2:  clamp(1.44rem, 1.30rem + 0.70vw, 1.95rem);
  --step-3:  clamp(1.73rem, 1.50rem + 1.15vw, 2.65rem);
  --step-4:  clamp(2.07rem, 1.68rem + 1.95vw, 3.60rem);
  --step-5:  clamp(2.49rem, 1.85rem + 3.20vw, 4.90rem);

  /* Space ----------------------------------------------------------------- */
  --sp-1: 0.25rem;  --sp-2: 0.5rem;   --sp-3: 0.75rem;  --sp-4: 1rem;
  --sp-5: 1.5rem;   --sp-6: 2rem;     --sp-7: 3rem;     --sp-8: 4rem;
  --sp-9: 6rem;
  --section-y: clamp(3.25rem, 8vw, 6.5rem);

  /* Layout ---------------------------------------------------------------- */
  --maxw: 1220px;
  --maxw-header: 1440px;   /* wider than content so the nav can breathe */
  --gutter: clamp(1rem, 4vw, 2.5rem);
  --radius: 10px;
  --radius-sm: 6px;

  --shadow-1: 0 1px 2px rgba(35,31,32,.06), 0 4px 12px rgba(35,31,32,.06);
  --shadow-2: 0 2px 6px rgba(35,31,32,.08), 0 16px 36px rgba(35,31,32,.10);

  --ease: cubic-bezier(.2,.7,.3,1);
  --dur: 260ms;

  --header-h: 76px;
  --trust-h:    76px;   /* desktop overlay height — fold math depends on it */
  /* Sticky Call Now / Free Inspection bar. 56px min-height + 1px top border.
     Shown below 1040px, so it is part of the phone/tablet fold math the way
     --trust-h is part of the desktop one. Keep in step with .mobile-bar a. */
  --mobile-bar-h: 57px;
}


/* --------------------------------------------------------------------------
   3. RESET
   -------------------------------------------------------------------------- */

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; scroll-padding-top: calc(var(--header-h) + 1rem); }

body {
  margin: 0;
  background: var(--white);
  color: var(--ink-soft);
  font-family: var(--font);
  font-variation-settings: 'wdth' var(--wd-body), 'wght' 400;
  font-size: var(--step-0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Archivo is a variable font. font-variation-settings on an ancestor beats
   font-weight on a descendant, so <strong> and <b> were inheriting 'wght' 400
   from body and rendering identical to the copy around them. Restate the axis
   at the same width so the bold does not change the letterfit.
   Placed here on purpose: .footer-blurb strong comes later and still wins. */
strong, b { font-variation-settings: 'wdth' var(--wd-body), 'wght' 700; }

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

h1, h2, h3, h4, h5 {
  color: var(--ink);
  margin: 0 0 var(--sp-4);
  line-height: 1.08;
  text-wrap: balance;
}

figure { margin: 0; }   /* UA default is 1em 40px — it was eating 80px off every card */

p { margin: 0 0 var(--sp-4); }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0 0 var(--sp-4); padding-left: 1.15rem; }

a { color: var(--teal-action); text-underline-offset: .18em; }
a:hover { color: var(--black); }

button { font: inherit; }

:focus-visible {
  outline: 3px solid var(--teal-brand);
  outline-offset: 3px;
  border-radius: 3px;
}

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


/* --------------------------------------------------------------------------
   4. UTILITIES
   -------------------------------------------------------------------------- */

.wrap { max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.wrap--narrow { max-width: 860px; }

.section { padding-block: var(--section-y); }
.section--dark { background: var(--black); color: var(--slate); }
.section--dark h2, .section--dark h3, .section--dark h4 { color: var(--white); }
.section--wash { background: var(--teal-wash); }
.section--slate { background: var(--slate-mid); }

/* --------------------------------------------------------------------------
   COASTAL BANDS
   One system across the four light sections so they read as a set rather
   than four unrelated background colors. Each is a soft vertical wash with
   an optional nail-pattern watermark bled into one edge.
   -------------------------------------------------------------------------- */

.band { position: relative; isolation: isolate; }

.band--down  { background: linear-gradient(180deg, var(--wash-1) 0%, #FFFFFF 68%); }
.band--up    { background: linear-gradient(180deg, #FFFFFF 0%, var(--wash-1) 100%); }
.band--deep  { background: linear-gradient(168deg, var(--wash-3) 0%, var(--wash-2) 52%, var(--wash-3) 100%); }
.band--soft  { background: linear-gradient(180deg, #FFFFFF 0%, var(--wash-3) 42%, var(--wash-1) 100%); }

/* Watermark. Sits under the content, never over it. */
.band--mark::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background-image: url('../images/pattern-nails.webp');
  background-size: 300px auto;
  opacity: .05;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 45%, #000 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, #000 45%, #000 100%);
}
.band--mark-right::before {
  -webkit-mask-image: linear-gradient(to left, #000 0%, transparent 58%);
          mask-image: linear-gradient(to left, #000 0%, transparent 58%);
}

/* Hairline rules that tie the bands together at their seams. */
.band--rule-top    { border-top: 1px solid rgba(40,147,157,.20); }
.band--rule-bottom { border-bottom: 1px solid rgba(40,147,157,.20); }

/* An inset version of the same rule. .band--rule-* draws on the section, so
   it runs the full width of the viewport; this one sits INSIDE a .wrap and
   therefore starts and ends exactly where the content does. Used above the
   gallery grid, where a full-bleed line cutting past the photographs looked
   like it belonged to something else. */
/* --gap-rule is the space above AND below the inset rule. One value, used
   by three declarations, so the two gaps cannot drift apart. */
:root { --gap-rule: clamp(2rem, 5vw, 3.25rem); }

.rule-inset {
  height: 1px;
  background: rgba(40,147,157,.20);
  /* NO top margin. The space above comes from the section ABOVE, as
     padding — see the note on .section--pad-b-rule. */
  margin: 0 0 var(--gap-rule);
}

/* --- Spacing around an inset rule -----------------------------------------
   ** WHY THIS IS PADDING AND NOT MARGIN. **
   Two stacked sections each contribute their own --section-y to the gap
   between them, so the rule had 102px + 102px above it against 52px below.

   The obvious fix — zero both edges and give the rule a symmetric margin —
   does NOT work, and the reason is worth knowing: with no padding and no
   border at that boundary, the paragraph's bottom margin and the rule's top
   margin COLLAPSE THROUGH it. The result is neither the sum nor the value
   you set; it measured 34px against the 52px below.

   Padding cannot collapse. So the section above supplies the gap as padding
   and the rule supplies the one below as margin, both from --gap-rule.
   Measured equal at 1440, 1280, 900, 600 and 390. */
/* Minus 0.35rem, and that is not a fudge. The gap above ends at the last
   PAINTED pixel of text, but a line box also carries half-leading and room
   for descenders below that — so an identical padding value reads about
   6px larger above than below. Measured on the rendered page rather than
   computed: 59/53 before the trim, 54/53 after. */
.section--pad-b-rule { padding-bottom: calc(var(--gap-rule) - 0.35rem); }
.section--flush-t    { padding-top: 0; }

.visually-hidden {
  position: absolute !important; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--teal-action); color: var(--white);
  padding: var(--sp-3) var(--sp-5); border-radius: 0 0 var(--radius-sm) 0;
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  text-decoration: none;
}
.skip-link:focus { left: 0; }

.eyebrow {
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  text-transform: uppercase;
  letter-spacing: .16em;
  font-size: var(--step--1);
  color: var(--teal-action);
  margin: 0 0 var(--sp-3);
}
.section--dark .eyebrow { color: var(--teal-light); }

.h-display {
  font-variation-settings: 'wdth' var(--wd-hero), 'wght' 700;
  font-size: var(--step-5);
  letter-spacing: -.015em;
  text-transform: uppercase;
}
.h-section {
  font-variation-settings: 'wdth' var(--wd-head), 'wght' 700;
  font-size: var(--step-3);
  letter-spacing: -.008em;
  text-transform: uppercase;
}
.h-card {
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700;
  font-size: var(--step-1);
  letter-spacing: 0;
}

.lede { font-size: var(--step-1); line-height: 1.5; max-width: 60ch; }
.section--dark .lede { color: var(--slate); }
.measure { max-width: 66ch; }
.center { text-align: center; margin-inline: auto; }
.nowrap { white-space: nowrap; }

/* Forces a break on narrow screens only. Its partner hides the separator
   that the break replaces — a middot left stranded at the end of a line
   reads as a typo, so wherever .br-mobile splits a dot-delimited string,
   wrap that dot in .sep-mobile-hide. */
.br-mobile { display: none; }
.sep-mobile-hide { display: inline; }
@media (max-width: 759px) {
  .br-mobile { display: inline; }
  .sep-mobile-hide { display: none; }
}

/* .br-mobile's partner: breaks at 760 and up, a space below. Needed because
   a heading often wants THREE lines on a phone and TWO on a desktop, which
   .br-sm-up cannot express — it swaps at 400, so between 400 and 759 both it
   and .br-mobile are live and the heading gets a break at every marked gap.
   Use .br-xs / .br-sm-up for a swap at 400, .br-mobile / .br-desktop for a
   swap at 760. Same rule as the others: a space before each one in the
   source, or the words butt together at the width where it is hidden. */
.br-desktop { display: inline; }
@media (max-width: 759px) { .br-desktop { display: none; } }

/* ---- BREAKS THAT SWAP AT 400px ---------------------------------------
   400 is where a phone stops being able to hold the wider arrangement of
   a line. Below it, the same sentence needs a different set of break
   points rather than one fewer — otherwise the natural wrap strands a
   single word on a line of its own.

   Used as a pair. Every gap in the string is either a break or a space at
   any given width, never both and never neither:

     .br-xs      breaks below 400, is a space at 400 and up
     .br-sm-up   is a space below 400, breaks at 400 and up

   As with .br-mobile, the source needs a space before each one, or the
   text nodes butt together at whichever width the break is hidden. */
.br-xs    { display: none; }
.br-sm-up { display: inline; }

@media (max-width: 399px) {
  .br-xs    { display: inline; }
  .br-sm-up { display: none; }
}


/* --------------------------------------------------------------------------
   5. BUTTONS
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .55em;
  /* justify-content centers the text as a flex item, but once the label
     wraps, that item is full width and its lines fall back to the inherited
     text-align — which left-aligned a stacked two-line button on a 320px
     phone. This centers the lines within it. */
  text-align: center;
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  font-size: var(--step--1);
  text-transform: uppercase; letter-spacing: .1em;
  padding: 1.05em 1.75em;
  border: 2px solid transparent; border-radius: var(--radius-sm);
  text-decoration: none; cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
  min-height: 48px;
}
.btn:hover { transform: translateY(-2px); }

.btn--primary { background: var(--teal-action); color: var(--white); border-color: var(--teal-action); }
.btn--primary:hover { background: var(--black); border-color: var(--black); color: var(--white); }

.btn--on-dark { background: var(--teal-light); color: var(--black); border-color: var(--teal-light); }
.btn--on-dark:hover { background: var(--white); border-color: var(--white); color: var(--black); }

/* White fill on the teal band; inverts to teal-on-white on hover. */
.btn--on-teal { background: var(--white); color: var(--teal-action); border-color: var(--white); }
.btn--on-teal:hover { background: var(--teal-action); color: var(--white); border-color: var(--white); }

.btn--ghost { background: transparent; color: var(--ink); border-color: var(--slate-deep); }
.btn--ghost:hover { border-color: var(--black); background: var(--black); color: var(--white); }

.btn--ghost-dark { background: transparent; color: var(--white); border-color: rgba(255,255,255,.35); }
.btn--ghost-dark:hover { background: var(--white); color: var(--black); border-color: var(--white); }
/* Only inside the teal CTA band does the call button pick up teal type. */
.cta-band .btn--ghost-dark:hover { color: var(--teal-action); }

/* Equal-width buttons, with no media query anywhere.

   auto-fit + minmax means the browser fits as many 16rem tracks as the
   container allows and shares the space equally between them. Two buttons
   sit side by side down to roughly 570px and then drop to one full-width
   column — equal in both states, at every width, whatever the label lengths
   or the rendered font width turn out to be.

   Two earlier attempts used a fixed breakpoint (560px, then 700px). Both
   left a band where the pair had wrapped but the equalising rule had not
   fired yet, because the real wrap point moves with font rendering. A
   threshold was the wrong mechanism for this. */
.btn-row {
  display: grid;
  /* 21rem, not 18rem, and the number is load-bearing.

     "Get a Free Roof Inspection" needs 323px to sit on one line. At an
     18rem basis inside the hero's old 41rem measure each track came out at
     322px — one pixel short — so the label wrapped, the button went to two
     lines, and the pair looked broken on a desktop.

     21rem (336px) is wider than that longest label, which turns it into an
     invariant rather than a coincidence: if two buttons are side by side,
     the track is at least 336px and neither label can wrap. If they cannot
     both have 336px, auto-fit stacks them full width, where they have even
     more room. Do not lower this without re-measuring the longest label. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
  gap: var(--sp-3);
  /* stretch, not center. Equal *widths* were the ask, but at some viewport
     widths "Get a Free Roof Inspection" wraps to two lines while "Call
     (854) 894-2128" does not, and centered items then sit at 83px against
     59px. Stretching gives every button in a row the row's height; the
     label stays vertically centered because .btn is itself a flex box. */
  align-items: stretch;
}
.btn-row .btn { width: 100%; }

/* A lone button should not stretch the width of the section. */
.btn-row:has(> .btn:only-child) { grid-template-columns: minmax(min(100%, 18rem), max-content); }

/* --- A lone button, centered in its row -----------------------------------
   ** WHY THIS IS A MODIFIER AND NOT THE DEFAULT. **
   A single button gets ONE max-content column, which is narrower than the
   row. With the default justify-content it sits at the row's start — so a
   row that is itself perfectly centered still renders the button off to the
   left. That is correct where the button belongs to left-aligned content,
   which is the case on the home page, and wrong where everything around it
   is centered.

   Below 760px the shared rule already centers it, which is why this only
   ever looked wrong on a desktop. */
.btn-row--center:has(> .btn:only-child) { justify-content: center; }

/* Centered bands keep the pair to a sensible measure instead of spanning
   the full content width. */
.cta-band .btn-row { max-width: 45rem; margin-inline: auto; }

.link-arrow {
  display: inline-flex; align-items: center; gap: .5em;
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  font-size: var(--step--1); text-transform: uppercase; letter-spacing: .1em;
  text-decoration: none; color: var(--teal-action);
}
.link-arrow::after {
  content: '\2192'; font-size: 1.75em; line-height: 0.7;
  transition: transform var(--dur) var(--ease);
}
.link-arrow:hover::after { transform: translateX(6px); }
.section--dark .link-arrow { color: var(--teal-light); }
.section--dark .link-arrow:hover { color: var(--white); }


/* --------------------------------------------------------------------------
   6. HEADER + NAV
   -------------------------------------------------------------------------- */

/* Solid white at rest; charcoal frosted glass once it overlaps content.
   Trigger is 80px of scroll, set in main.js. */
.site-header {
  position: sticky; top: 0; z-index: 100;
  background: var(--white);
  border-bottom: 1px solid var(--slate);
  transition: background 320ms var(--ease), backdrop-filter 320ms var(--ease),
              box-shadow 320ms var(--ease), border-color 320ms var(--ease);
}

.site-header.is-stuck {
  background: rgba(28,25,26,.68);
  -webkit-backdrop-filter: saturate(160%) blur(16px);
          backdrop-filter: saturate(160%) blur(16px);
  border-bottom-color: rgba(255,255,255,.14);
  box-shadow: 0 8px 30px rgba(0,0,0,.30);
}

/* Everything in the bar inverts together. */
.logo--light { display: none; }
.is-stuck .logo--dark  { display: none; }
.is-stuck .logo--light { display: block; }

.is-stuck .nav__link,
.is-stuck .sub-toggle { color: var(--teal-pale); }
/* The current page marks itself in --teal-action, which is tuned for white
   backgrounds and reads almost navy on the frosted charcoal bar. It has to
   take the same pale teal as its siblings once the header inverts.
   Specificity 0,3,0, so this does not depend on source order. */
.is-stuck .nav__link[aria-current="page"] { color: var(--teal-pale); }
.is-stuck .nav__link[aria-current="page"]::after { background: rgba(143,224,231,.55); }
.is-stuck .header-phone { color: var(--white); }
.is-stuck .nav__link:hover,
.is-stuck .sub-toggle:hover,
.is-stuck .header-phone:hover { color: var(--white); }
.is-stuck .nav__link::after,
.is-stuck .sub-toggle::after { background: rgba(143,224,231,.55); }
.is-stuck .sub-toggle svg { opacity: .6; }

.is-stuck .social-link { color: var(--white); border-color: rgba(255,255,255,.34); }
.is-stuck .social-link:hover { background: var(--teal-light); border-color: var(--teal-light); color: var(--black); }

.is-stuck .btn--primary {
  background: var(--teal-light); color: var(--black); border-color: var(--teal-light);
}
.is-stuck .btn--primary:hover { background: var(--white); border-color: var(--white); color: var(--black); }

/* The toggle stays black with white type on the frosted header, exactly as
   it is on the white one, so the header and the sticky header read as the
   same control. It used to flip to teal here, which made the same button
   look like two different buttons depending on scroll position.
   The faint outline is the only addition: black on frosted charcoal is
   about 1.1:1, so without it the button's edge disappears even though the
   label is still perfectly legible. */
.is-stuck .nav-toggle {
  background: var(--black);
  color: var(--white);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.28);
}
.site-header__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: clamp(1.25rem, 2.6vw, 2.5rem);
  max-width: var(--maxw-header);
  height: var(--header-h);   /* fixed, not min — the fold math depends on it */
}
.site-logo img { max-height: 46px; width: auto; }
.site-logo { display: block; flex: 0 0 auto; }
.site-logo img { width: clamp(132px, 17vw, 190px); height: auto; }

/* Below the desktop breakpoint the 17vw term is tiny, so every phone got
   the 132px floor — which looks undersized on anything bigger than a small
   handset. Two steps up instead, keyed to real device widths: a Galaxy S9
   or iPhone at 360-411 and the larger 412+ handsets. Anything under 360
   keeps 132px, which the client confirmed is right at that size.

   This covers all three places the mark appears on a phone — the header,
   the frosted sticky header and the open nav menu — because they are the
   same element in every case. */
@media (min-width: 360px) and (max-width: 1039px) {
  .site-logo img { width: 156px; }
}
/* 400, not 412. The client's Galaxy S9 screenshot is 1080 device pixels at
   a 2.625 ratio, which is a 411px layout viewport — a 412 threshold would
   have missed the exact device the request came from. 400 covers the whole
   411/412/414/430 group. */
@media (min-width: 400px) and (max-width: 1039px) {
  .site-logo img { width: 170px; }
}

.nav { display: none; }
.nav__list { display: flex; align-items: center; gap: clamp(.6rem, 1.6vw, 1.5rem); list-style: none; margin: 0; padding: 0; }
.nav__link {
  position: relative; display: inline-block; padding: .5rem 0 .18rem;
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 600;
  font-size: var(--step--1); text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink); text-decoration: none; white-space: nowrap;
}
.nav__link::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  height: 2px; background: rgba(40,147,157,.38);
  transform: scaleX(0); transform-origin: center;
  transition: transform var(--dur) var(--ease);
}
.nav__link:hover { color: var(--teal-action); }
.nav__link:hover::after, .nav__link[aria-current="page"]::after { transform: scaleX(1); }
.nav__link[aria-current="page"] { color: var(--teal-action); }

.has-sub { position: relative; }
.sub-toggle {
  position: relative; display: inline-flex; align-items: center; gap: .4em;
  background: none; border: 0; cursor: pointer; padding: .5rem 0 .18rem;
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 600;
  font-size: var(--step--1); text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink);
}
.sub-toggle::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  height: 2px; background: rgba(40,147,157,.38);
  transform: scaleX(0); transform-origin: center;
  transition: transform var(--dur) var(--ease);
}
.sub-toggle:hover { color: var(--teal-action); }
.sub-toggle:hover::after, .sub-toggle[aria-expanded="true"]::after { transform: scaleX(1); }
.sub-toggle svg { width: 9px; height: auto; flex: 0 0 auto; opacity: .5; transition: transform var(--dur) var(--ease), opacity var(--dur) var(--ease); }   /* flex:0 0 auto — same squeeze as the phone icon */
.sub-toggle:hover svg { opacity: .85; }
.sub-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }

/* Centered under the Services label. The 10px pad-top is a hover bridge so
   the menu does not close while the cursor travels from label to menu. */
.submenu {
  position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  min-width: 264px;
  padding: 10px 0 0; margin: 0; list-style: none;
  display: none; z-index: 20;
}
.submenu[data-open="true"] { display: block; }
.submenu::before {
  content: ''; position: absolute; inset: 10px 0 0 0;
  background: var(--white); border: 1px solid var(--slate);
  border-radius: var(--radius); box-shadow: var(--shadow-2);
}
.submenu li { position: relative; }
.submenu li:first-of-type { margin-top: .45rem; }
.submenu li:last-of-type { margin-bottom: .45rem; }
.submenu a {
  position: relative; display: block; text-align: center;
  padding: .72rem 1rem; margin: 0 .45rem; border-radius: var(--radius-sm);
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 600;
  font-size: var(--step--1); color: var(--ink); text-decoration: none;
  white-space: nowrap;
}
.submenu a:hover { background: var(--teal-wash); color: var(--teal-action); }

.header-actions { display: none; align-items: center; gap: clamp(1rem, 1.9vw, 1.65rem); }

/* Shorter than the page buttons — 76px of header cannot carry a 48px pill
   with breathing room above and below. */
.header-actions .btn {
  white-space: nowrap;
  padding: .68em 1.5em;
  min-height: 42px;
  font-size: .78rem;
}
.header-phone {
  display: inline-flex; align-items: center; gap: .4em;
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700;
  font-size: var(--step-0); color: var(--ink); text-decoration: none; white-space: nowrap;
}
.header-phone:hover { color: var(--teal-action); }
/* The icon is a flex item and would otherwise be squeezed to nothing by the
   compression tier — it measured 0px wide at 1040-1060 and 7px at 1080, so
   it silently disappeared on exactly the laptops that tier exists for. */
.header-phone svg { flex: 0 0 auto; }
.header-social { display: flex; gap: var(--sp-2); }
.social-link, .header-phone, .header-actions .btn { transition: color 220ms var(--ease), background 220ms var(--ease), border-color 220ms var(--ease); }
.social-link {
  display: grid; place-items: center; width: 40px; height: 40px;
  border-radius: 50%; color: var(--ink); border: 1px solid var(--slate);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.social-link:hover { background: var(--teal-action); border-color: var(--teal-action); color: var(--white); }

/* Phone shortcut + menu toggle, paired on the right of the mobile bar. */
.mobile-controls { display: flex; align-items: center; gap: var(--sp-3); }

.mobile-call {
  display: inline-grid; place-items: center;
  width: 44px; height: 44px; border-radius: var(--radius-sm);
  background: var(--teal-action); color: var(--white); text-decoration: none;
}
.mobile-call:hover { background: var(--black); color: var(--white); }
/* Stays --teal-action with a white icon on the frosted header, exactly as on
   the white one — so the phone button and the MENU button beside it both
   read as the same control at any scroll position.

   It used to flip to --teal-light, which forced the icon to black for
   contrast. Keeping --teal-light and simply whitening the icon was not an
   option: white on --teal-light is 2.10:1, well under the 3:1 a meaningful
   icon needs. On --teal-action it is 5.38:1.

   Same 1px outline as the toggle, for the same reason — a solid button on
   frosted charcoal needs its edge drawn. */
.is-stuck .mobile-call {
  background: var(--teal-action);
  color: var(--white);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.28);
}
/* .is-stuck .mobile-call and .mobile-call:hover both weigh 0,2,0, and the
   stuck rule comes later — so without this the button had no hover or focus
   state at all once the header stuck. Same source-order tie that has bitten
   the nav color, the trust bar and the hero pagination. */
.is-stuck .mobile-call:hover,
.is-stuck .mobile-call:focus-visible {
  background: var(--black);
  color: var(--white);
}

.nav-toggle {
  display: inline-flex; align-items: center; gap: .5em;
  background: var(--black); color: var(--white); border: 0; border-radius: var(--radius-sm);
  padding: .7rem 1rem; cursor: pointer; min-height: 44px;
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  font-size: var(--step--1); text-transform: uppercase; letter-spacing: .08em;
}
/* Icon and label swap on open so the close affordance is obvious. */
.nav-toggle .icon-close, .nav-toggle .label-close { display: none; }
.nav-toggle[aria-expanded="true"] .icon-open,
.nav-toggle[aria-expanded="true"] .label-open { display: none; }
.nav-toggle[aria-expanded="true"] .icon-close,
.nav-toggle[aria-expanded="true"] .label-close { display: inline; }
/* Open state keeps the same black-and-white treatment. The X and the word
   CLOSE are the state change; recolouring the button as well was doing the
   same job twice and made the control look like it had swapped identity. */
.nav-toggle[aria-expanded="true"] { background: var(--black); color: var(--white); }

/* Hover and focus take the same teal as the phone button sitting beside
   them, with white type. --teal-action rather than --teal-light because
   white on --teal-light is about 2:1 and unreadable; white on
   --teal-action is 5.38:1 and passes AA. Applies in both states and on
   the frosted sticky header, so the control never changes behaviour
   depending on where the page is scrolled to. */
.nav-toggle:hover,
.nav-toggle:focus-visible,
.nav-toggle[aria-expanded="true"]:hover,
.nav-toggle[aria-expanded="true"]:focus-visible,
.is-stuck .nav-toggle:hover,
.is-stuck .nav-toggle:focus-visible {
  background: var(--teal-action);
  color: var(--white);
}

.mobile-nav {
  display: none; background: var(--black); color: var(--white);
  border-top: 1px solid rgba(255,255,255,.12);
}
.mobile-nav[data-open="true"] { display: block; }
.mobile-nav ul { list-style: none; margin: 0; padding: var(--sp-4) 0; }
.mobile-nav li + li { border-top: 1px solid rgba(255,255,255,.10); }
/* Scoped to the list. Unscoped, this was overriding the CTA button's own
   display and padding and pushing its label to the left. */
.mobile-nav ul a {
  display: block; padding: .95rem var(--gutter); color: var(--white); text-decoration: none;
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 600;
  text-transform: uppercase; letter-spacing: .06em; font-size: var(--step--1);
}
.mobile-nav ul a:hover { background: rgba(255,255,255,.07); color: var(--teal-light); }
.mobile-nav .is-child a { padding-left: calc(var(--gutter) + 1.25rem); text-transform: none; letter-spacing: 0; opacity: .88; }
.mobile-nav__cta { padding: var(--sp-4) var(--gutter) var(--sp-6); }
.mobile-nav__cta .btn { width: 100%; }
.mobile-nav__social { display: flex; justify-content: center; gap: var(--sp-3); padding: 0 var(--gutter) var(--sp-6); }
.mobile-nav__social .social-link { color: var(--white); border-color: rgba(255,255,255,.3); }

@media (min-width: 1040px) {
  .nav { display: block; }
  .header-actions { display: flex; }
  .mobile-controls { display: none; }
  .mobile-nav { display: none !important; }
}

/* The full desktop bar — 190px logo + 610px nav + 475px of actions + gaps
   and gutters — needs about 1430px. It was switching on at 1040px, so
   every laptop from roughly 1040 to 1360 (1280x800 and 1366x768 among
   them) pushed the phone number and the Free Inspection button off the
   right edge and put a horizontal scrollbar on the page.

   This tier compresses the bar to fit instead of moving the breakpoint up,
   which would have handed a 1280px laptop the phone menu. The social icons
   step aside first: they are the only item here that is decoration rather
   than a conversion path, and they are still in the footer. */
@media (min-width: 1040px) and (max-width: 1379px) {
  .site-header__inner { gap: 1.15rem; }
  .header-social { display: none; }
  .header-actions { gap: .85rem; }
  .site-logo img { width: 148px; }
  .nav__list { gap: clamp(.5rem, 1.15vw, 1.1rem); }
  .nav__link, .sub-toggle { letter-spacing: .05em; }
  .header-actions .btn { padding: .68em 1.05em; letter-spacing: .07em; }
  .header-phone { font-size: var(--step--1); }
}

/* Two knife-edges found by a 5px sweep of 1035-1460, both measured, neither
   visible in a screenshot:

   1040-1045   5px and 1px of horizontal overflow. It was there all along,
               hidden because the phone icon is a flex item and was being
               squeezed to 0px wide rather than pushing the bar over. Fixing
               the icon (see .header-phone svg) exposed it.
   1360-1364   4px of overflow at the exact width where this tier switched
               off and the social icons came back. Handled by moving the
               tier boundary from 1359 to 1379, above, so the full-width bar
               does not start until there is room for it.

   Re-swept 1035-1460 in 5px steps after both fixes: clean. */
@media (min-width: 1040px) and (max-width: 1099px) {
  .site-header__inner { gap: .7rem; }
  .header-actions { gap: .6rem; }
}


/* --------------------------------------------------------------------------
   7. HERO
   -------------------------------------------------------------------------- */

/* The hero fills the first screen below the header. The trust bar is a
   frosted overlay pinned to its bottom edge, so it lands on the fold
   without being subtracted from the hero's height.

   Kept entirely in CSS. An earlier version measured the bar in JS and
   published it as a custom property, which resized the hero after first
   paint and cost 0.18 CLS. Do not reintroduce post-paint measurement. */
.hero {
  position: relative; overflow: hidden;
  background: var(--black); color: var(--white);
  min-height: max(480px, calc(100svh - var(--header-h)));
  display: flex; align-items: center;
}
.hero > .wrap { width: 100%; }

.hero__slides { position: absolute; inset: 0; }
.hero__slide { position: absolute; inset: 0; overflow: hidden; opacity: 0; transition: opacity 900ms var(--ease); }
.hero__slide[data-active="true"] { opacity: 1; }

/* The <picture> has to be stretched too, not just the <img>.

   height:100% on the image resolves against its parent — <picture> — and a
   <picture> with no height of its own is auto, so the percentage was being
   thrown away and the image fell back to its 16:9 intrinsic ratio. On a
   1500px desktop that ratio happens to overflow the hero, so it looked
   right; on a 390px phone the photo stopped 219px down a 615px hero and
   everything below it was bare black. That was the black slab sitting
   behind the mobile CTA buttons. */
.hero__slide picture { position: absolute; inset: 0; display: block; }
.hero__slide img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* Bias the crop upward so the roofline sits in frame instead of
     a wide empty foreground. Tune per photo if needed. */
  object-position: center 38%;
  transform: scale(1.02);          /* base state; Ken Burns takes over below */
  will-change: transform;
}

/* Two passes: a horizontal scrim that carries the copy on the left, and a
   soft floor so the bottom edge does not end on a hard line. Deliberately
   lighter than a flat wash — the photograph should still read as one. */
.hero__slide::after {
  content: ''; position: absolute; inset: 0;
  background:
    linear-gradient(100deg, rgba(24,21,22,.90) 0%, rgba(24,21,22,.72) 34%,
                            rgba(24,21,22,.34) 62%, rgba(24,21,22,.12) 100%),
    linear-gradient(to top, rgba(24,21,22,.55) 0%, rgba(24,21,22,0) 34%);
}

/* Phones and small tablets get a different scrim entirely.

   The desktop pair is angled at 100deg, which works across a landscape box
   but compounds badly in a tall narrow one: the lower-left region lands
   near the dark end of the angled wash AND inside the floor gradient, so
   the two stack toward 90% black straight through the button band.

   One vertical wash instead, shaped to the content rather than the box:
   heavy over the headline and paragraph, opening up to 32% through the
   buttons so the roof is clearly visible behind them, then falling away
   into the black trust bar so the hero does not end on a hard line.

   The stops are not taste. Every one was measured against the brightest
   pixel behind each text box on all three slides:

       eyebrow vs --teal-light   5.81:1   (needs 4.5)
       headline vs white         9.90:1   (needs 4.5)
       paragraph vs --slate      5.25:1   (needs 4.5)
       call button vs white      8.17:1   (needs 4.5, with the glass below)
       pagination vs surround    3.80:1   (needs 3.0, non-text UI)

   The teal eyebrow is the tightest of the five, which is why the top stop
   is the darkest — teal on a sunlit roof is the first thing to fail.
   Re-measure before touching any stop; the method is noted in the README. */
@media (max-width: 759px) {
  .hero__slide::after {
    background:
      linear-gradient(to bottom, rgba(24,21,22,.88) 0%,  rgba(24,21,22,.86) 17%,
                                 rgba(24,21,22,.76) 37%, rgba(24,21,22,.72) 62%,
                                 rgba(24,21,22,.32) 76%, rgba(24,21,22,.38) 86%,
                                 rgba(24,21,22,.82) 100%);
  }
  /* The photo now shows through behind the buttons, which is the point —
     but the outlined Call button is white type on nothing, so it carries
     its own glass so the label never depends on what is behind it. */
  .hero .btn--ghost-dark {
    background: rgba(24,21,22,.62);
    border-color: rgba(255,255,255,.58);
  }
  /* Same reason: the unfilled half of each progress bar has to stay
     readable against a brighter backdrop than it had before. The ring
     gives each bar its own dark edge so its boundary does not depend on
     whatever the photograph is doing behind it (WCAG 1.4.11). */
  /* ::before, not the button — the button is a transparent 24px target. */
  .hero__dot::before {
    background: rgba(255,255,255,.55);
    box-shadow: 0 0 0 1px rgba(24,21,22,.55), 0 1px 4px rgba(24,21,22,.55);
  }
}

.hero__inner {
  position: relative; z-index: 2;
  padding-block: clamp(2.5rem, 6vw, 4.25rem);
  padding-bottom: clamp(7rem, 14vw, 9rem);   /* clears the trust overlay */
  /* 44rem, so the button row can give each track the 21rem it needs to
     keep both labels on one line. The headline keeps its own 41rem measure
     below so this does not change where "Response You / Can Count On."
     breaks, and .hero__sub is already held to 52ch. */
  max-width: 44rem;
}
.hero h1 { max-width: 41rem; }
.hero__eyebrow {
  font-variation-settings: 'wdth' 96, 'wght' 600;
  text-transform: uppercase; letter-spacing: .11em;
  font-size: var(--step--1); color: var(--teal-light);
  margin: 0 0 var(--sp-4); white-space: nowrap;
}
@media (max-width: 780px) { .hero__eyebrow { white-space: normal; letter-spacing: .09em; } }
/* Below 1040px the trust bar is a plain band underneath, not an overlay,
   so the hero has no reason to be pinned to the viewport. Locking it to
   100svh on a short phone squeezed the eyebrow, headline, paragraph, two
   stacked buttons and the pagination into whatever height was left.

   Height follows content here. The hero ends where the buttons end, the
   trust bar starts cleanly below it, and the page simply scrolls — which
   the client confirmed is fine on mobile. Matched to the 1040px overlay
   threshold so the hero and the trust bar always agree about which of the
   two layouts they are in. */
@media (max-width: 1039px) {
  /* The hero occupies exactly the strip between the header and the sticky
     CTA bar — nothing of it hides underneath the bar, and the trust band
     starts the moment you scroll.

     min-height, not height: on a short device the copy is allowed to make
     the hero taller rather than being clipped. svh, not vh or dvh, so the
     figure is the SMALL viewport height — the one with the browser's URL
     bar showing. Sizing to dvh would make the hero grow and shrink as that
     bar hides on scroll, which drags the whole page with it. */
  .hero {
    /* The 1px is the header's bottom border. --header-h is the inner
       height; the element measures 77px. Without it the hero ends one
       pixel underneath the sticky bar. */
    min-height: calc(100svh - var(--header-h) - 1px - var(--mobile-bar-h));
    /* The pagination lives in this padding. Because the box is border-box,
       taking it out of the hero's content box means align-items:center
       centers the copy in what is left — so the copy reads as vertically
       centered AND the bars sit low, near the sticky buttons. Padding the
       inner instead would center the padded box and leave the copy high. */
    padding-bottom: clamp(2.5rem, 7vw, 3.5rem);
  }
  /* Symmetric, so it does not fight the centering above. */
  .hero__inner { padding-block: clamp(1.5rem, 4vw, 2.25rem); }
  /* NB: the pagination's `bottom` is set in section 7b, not here. The base
     .hero__controls rule lives there and weighs the same, so a value set
     here would simply lose on source order. */
}

.hero h1 {
  color: var(--white); margin-bottom: var(--sp-5);
  /* Held in a variable so .hero--tight below can cap it without repeating
     the curve. The home page computes exactly what it always did. */
  --h1-size: clamp(2.15rem, 1.55rem + 2.6vw, 3.75rem);
  font-size: var(--h1-size);
  letter-spacing: -.015em;
}

/* ** A DISPLAY HEADING CANNOT BE WIDER THAN ITS LONGEST WORD. **
   A word that will not fit does not wrap and does not widen its box — it
   simply paints past the edge of the screen, which is why an overflow sweep
   that measures element boxes reports nothing. "STRAIGHTFORWARD" was 45px
   past the edge at 411px and 132px at 320px.

   The cap is derived, not guessed: the heading fits when its size is at most
   (available width / 13.03), 13.03 being the measured ratio of that word's
   width to the font size. Available width is the viewport less the wrap's
   32px, so the cap is 7.5vw - 3px with a little margin. min() means it only
   ever applies where it is needed — above roughly 570px the normal curve is
   already the smaller of the two and this does nothing at all.

   Opt in per page with $hero_long_word = true. Do not apply it globally:
   the other six interior headings fit, and shrinking them costs scale for
   no reason. */
/* The cap itself lives with .hero--page h1 further down — that rule has a
   higher specificity and would otherwise win, which is the seventh
   source-order tie on this project. Look there. */
.hero__sub {
  font-size: var(--step-0); color: var(--slate); line-height: 1.6;
  margin-bottom: var(--sp-6); max-width: 52ch;
}


/* --------------------------------------------------------------------------
   7b. HERO SLIDER — PROGRESS PAGINATION + KEN BURNS

   The whole slider is driven by CSS animation, not a JS timer. The active
   slide's progress bar animates from 0 to 100% width; when it finishes,
   its animationend event tells JS to advance. That means:

     - the bar, the Ken Burns pan and the slide change can never drift apart
     - pausing is a single `animation-play-state` flip, no elapsed-time math
     - prefers-reduced-motion removes the animations, which removes the
       auto-advance entirely, with no separate code path

   Horizontal, bottom right — per the Blue Water Electric reference.
   -------------------------------------------------------------------------- */

:root { --hero-dur: 6500ms; }

.hero__controls {
  position: absolute; z-index: 3;
  right: clamp(1.25rem, 5vw, 4rem);
  bottom: calc(var(--trust-h) + clamp(1rem, 2.5vw, 1.75rem));
  display: flex; flex-direction: row; align-items: center; gap: var(--sp-4);
}
.hero__dots { display: flex; flex-direction: row; gap: var(--sp-3); }

/* --- Hero pause toggle -----------------------------------------------------
   WCAG 2.2.2. Sized 32x32 so it clears the 24x24 minimum target size in
   WCAG 2.2 (2.5.8) with room to spare, on a slide photograph where a
   mis-tap is easy. Two glyphs live in the markup at once and CSS shows one
   or the other off aria-pressed, so the button never changes width and the
   state a screen reader reads and the state a person sees cannot disagree.
   Colors are the same white-on-scrim family as .hero__dot, and the same
   1px dark ring, because the backdrop is photography and can be any value
   underneath. --------------------------------------------------------------- */
.hero__pause {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0; cursor: pointer;
  color: var(--white);
  background: rgba(24,21,22,.42);
  border: 0; border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.55);
  -webkit-backdrop-filter: blur(4px);
          backdrop-filter: blur(4px);
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.hero__pause:hover { background: var(--teal-action); box-shadow: inset 0 0 0 1px var(--teal-action); }
.hero__pause:focus-visible { outline: 3px solid var(--teal-light); outline-offset: 4px; }

/* Rest = playing = offer "pause". Pressed = paused = offer "play". */
.hero__pause .icon-play  { display: none; }
.hero__pause[aria-pressed="true"] .icon-pause { display: none; }
.hero__pause[aria-pressed="true"] .icon-play  { display: block; }

/* The glyphs are optically centered, not geometrically — the play triangle
   sits right of its own bounding box center, so it needs nudging back. */
.hero__pause .icon-play { transform: translateX(1px); }

/* No motion preference: nothing is moving, so there is nothing to pause.
   Hiding it is correct rather than lazy — a control that does nothing is
   worse than no control. */
@media (prefers-reduced-motion: reduce) {
  .hero__pause { display: none; }
}

/* Below 1040px the pagination is centered in the viewport — the client asked
   for that in v12 and it still holds. Adding the toggle to the same flex row
   would center the GROUP instead, pushing the bars off-center by half a
   button. Taking the toggle out of the flow keeps the bars dead center.

   RIGHT, NOT LEFT, AND HIGHER THAN THE BARS ALONE WOULD SIT.
   The bars are 3px tall, so a 32px button centered on their line hangs 16px
   below them — which put it on the seam between the hero photograph and the
   trust bar, and it read as something that had come loose. The whole row is
   lifted so the button clears that edge, and the button sits at the right,
   away from the logo side and under the thumb on a phone held right-handed.
   The bars stay centered to the pixel either way — that is horizontal, and
   this is vertical. */
@media (max-width: 1039px) {
  /* The vertical offset is NOT set here — see the .hero__controls block in
     section 7b, which comes later in the file and would overrule it. */
  .hero__controls { position: absolute; }
  .hero__pause {
    position: absolute;
    left: auto;
    right: clamp(1rem, 5vw, 2.5rem);
    top: 50%;
    transform: translateY(-50%);
  }
}

/* ** THE BUTTON IS 24px TALL. THE BAR YOU SEE IS STILL 3px. **
   WCAG 2.2 (2.5.8) asks for a 24x24 target; these were 38x3, which is a
   genuinely hard thing to hit with a thumb. Rather than thicken the bar and
   change a design the client has approved, the BUTTON grows to 24px with a
   transparent background and the visible bar is painted by ::before,
   centered inside it. Nothing moves on screen; the tappable area is eight
   times larger.

   The bar has to be a pseudo-element rather than the button's own
   background, because a background fills the whole button box. */
.hero__dot {
  /* NO overflow:hidden. It was here to clip the progress fill to the
     rounded track, but the fill carries the same 2px radius itself, so the
     clip was doing nothing the fill was not already doing — and on a 25px
     box it clipped the bar's own drop shadow at both ends, which shows as a
     hairline difference against the approved build. */
  position: relative;
  width: 54px; height: 25px; padding: 0; border: 0; cursor: pointer;
  background: transparent; border-radius: 2px;
  /* The button is 24px tall but OCCUPIES 3px of layout, so the control row
     is exactly the height it has always been and nothing below the hero
     moves. The extra 22px of target hangs into transparent space above and
     below, where there is nothing else to hit.
     25 and not 24, so every number stays a whole pixel: (25 - 3) / 2 = 11
     exactly. At 24 the bar centers on a half pixel, the browser rounds it,
     and the result is a faint shift against the approved build. */
  margin-block: -11px;
}
.hero__dot::before {
  content: ''; position: absolute; left: 0; right: 0; top: 11px;
  height: 3px; border-radius: 2px;
  background: rgba(255,255,255,.30);
  transition: background var(--dur) var(--ease);
}
.hero__dot:hover::before { background: rgba(255,255,255,.55); }
/* The ring goes on the BAR, not on the button. The button is now a 25px
   invisible target, and an outline around that would draw a large box in
   mid-air where the visitor can see nothing to explain it. On ::before the
   ring sits exactly where it always has. */
.hero__dot:focus-visible { outline: none; }
.hero__dot:focus-visible::before { outline: 3px solid var(--teal-light); outline-offset: 6px; }

/* Centered on the same 3px line as the track above. Was inset:0 when the
   button itself was 3px tall. */
.hero__dot-fill {
  position: absolute; left: 0; top: 11px;
  width: 0; height: 3px; background: var(--teal-light); border-radius: 2px;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  /* Progress bar fills over the slide duration, then fires animationend. */
  .hero__dot[aria-current="true"] .hero__dot-fill {
    animation: heroFill var(--hero-dur) linear forwards;
  }
  /* Slow push-in on whichever slide is showing. */
  .hero__slide[data-active="true"] img {
    animation: heroKenBurns calc(var(--hero-dur) + 900ms) ease-out forwards;
  }
  /* One flag pauses the bar, the pan and therefore the advance together. */
  .hero[data-paused="true"] .hero__dot-fill,
  .hero[data-paused="true"] .hero__slide img { animation-play-state: paused; }
}

/* No motion preference: bars are static, the active one simply reads teal,
   and nothing auto-advances. */
@media (prefers-reduced-motion: reduce) {
  .hero__dot[aria-current="true"]::before { background: var(--teal-light); height: 4px; }
}

@keyframes heroFill {
  from { width: 0; }
  to   { width: 100%; }
}
@keyframes heroKenBurns {
  from { transform: scale(1.02); }
  to   { transform: scale(1.11); }
}

/* Bottom right is the Blue Water Electric placement, and it needs the
   --trust-h offset so it clears the frosted bar overlaying the hero. Both
   only make sense where that overlay exists, which is 1040px and up.

   Below that the bar stacks under the hero instead, so there is no overlay
   to clear and no right-hand column to anchor to — the pagination would
   read as a stray fragment in the corner. It centers under the buttons. */
@media (max-width: 1039px) {
  .hero__controls {
    left: 0; right: 0;
    /* Low in the hero, just above the sticky CTA bar. The hero reserves
       this strip as its own bottom padding, so the bars sit in clear space
       rather than under the copy.

       ** THIS IS THE RULE THAT SETS THE MOBILE OFFSET. ** There is an
       earlier .hero__controls block up in section 7 with the same weight;
       anything set there loses to this on source order. Raised from
       .9rem in v23 so the 32px pause button, which is centered on this
       3px line, clears the seam between the hero and the trust bar
       instead of straddling it. */
    bottom: clamp(1.85rem, 5vw, 2.35rem);
    justify-content: center; gap: var(--sp-3);
  }
  .hero__dot { width: 38px; }
}


/* --------------------------------------------------------------------------
   8. TRUST STRIP  (words + icons only — no logos, per client)
   -------------------------------------------------------------------------- */

/* The trust bar is a sibling of the hero, not a child.

   Desktop: pulled up by exactly its own height so it overlays the bottom of
   the hero photo with the same frosted treatment as the sticky header. The
   hero keeps overflow:hidden, which matters — the Ken Burns transform scales
   the slides past the viewport, and an earlier in-hero version had to unclip
   the hero to stack the bar, which leaked 106px of horizontal scroll on
   phones.

   Phones: no negative margin, so it simply stacks underneath as a solid
   band. The frosted overlay ate too much of an already short hero. */
.trust {
  position: relative; z-index: 4;
  background: var(--black);
  border-bottom: 1px solid rgba(255,255,255,.10);
}

/* The overlay treatment starts at 1040px, NOT 760px, because 1040px is
   where .mobile-bar stops being displayed.

   Between 760 and 1039 the two were fighting: the trust bar pinned itself
   to the bottom of the hero and the sticky Call Now bar — 56px, fixed to
   the viewport bottom — sat on top of it. Measured at 900x820, 59px of the
   77px bar was covered. The one element the client wanted visible at the
   fold was 77% hidden on every tablet.

   Below 1040 the bar now stacks under the hero exactly as it does on a
   phone, which is the layout the client approved, and the sticky CTA has
   the bottom of the screen to itself. */
@media (min-width: 1040px) {
  .trust {
    margin-top: calc(-1 * var(--trust-h));
    background: rgba(28,25,26,.55);
    -webkit-backdrop-filter: saturate(160%) blur(16px);
            backdrop-filter: saturate(160%) blur(16px);
    border-top: 1px solid rgba(255,255,255,.14);
    border-bottom: 0;
  }
}

/* Phone layout is the BASE, and desktop is the override.

   It used to be the other way round, with a max-width:759px block sitting
   ABOVE the base rule. Both weigh 0,1,0, so the base won and the list kept
   a fixed 132px height on a phone — while five items in two columns need
   about 250px. align-content:center then centered the overflow, pushing the
   first row of icons up OVER the hero photo and the last item down INTO the
   white section below, white type on a pale background. The bar was never
   really stacking on mobile; it was spilling out of a box too short for it.

   Writing it mobile-first also closes a gap: max-width:759px and
   min-width:760px both miss a viewport of 759.5px, which a fractional
   device pixel ratio can produce. There is no unstyled width now. */
/* Six columns is a scaffold, not six items. Licensed / Bonded / Insured
   take two columns each on the first row, Atlas Shingles and Locally Owned
   take three each on the second. That gives a 3 + 2 split where both rows
   fill the full width and stay centered, without needing a wrapper element
   or a second list. */
.trust__list {
  display: grid; grid-template-columns: repeat(6, 1fr);
  gap: var(--sp-5) var(--sp-3);
  list-style: none; margin: 0; padding: var(--sp-6) 0;
  height: auto; align-content: start;
  text-align: center;
}
/* Six items on the six-column scaffold. Two across on a phone, three across
   once the columns are wide enough to hold "LOCALLY OWNED" on one line —
   measured, the change-over is between 411 and 560. Below that, three across
   wraps the two longest labels and the row goes ragged. */
.trust__list li { grid-column: span 3; }
@media (min-width: 560px) { .trust__list li { grid-column: span 2; } }
.trust__list li {
  display: flex; flex-direction: column; align-items: center; gap: .35rem;
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  font-size: var(--step--1); text-transform: uppercase; letter-spacing: .1em;
  color: var(--white);
}
.trust__list svg { width: 26px; height: 26px; color: var(--teal-light); }

/* One row, locked height — the fold math above depends on --trust-h.
   Same 1040px threshold as the overlay, for the reason given there. */
@media (min-width: 1040px) {
  .trust__list {
    /* AUTO columns, not 1fr. Six equal tracks are only ~177px inside the
       wrap and "LOCALLY OWNED" needs ~190, so equal columns forced a second
       line inside a bar whose height is locked at 76px. Auto-sized tracks
       spread by the free space let each label take exactly the width it
       needs — which is what a row of credentials wants anyway. */
    grid-template-columns: repeat(6, auto);
    justify-content: space-between;
    gap: var(--sp-3) var(--sp-4);
    height: var(--trust-h);      /* locked — see the hero comment above */
    padding: 0; align-items: center; align-content: center;
  }
  .trust__list li { flex-direction: row; gap: .6rem; justify-content: center;
                    white-space: nowrap; }   /* belt and braces with auto tracks */
  /* One item per column here — the phone spans must not carry over, or the
     bar stretches into a two-row mess. */
  .trust__list li { grid-column: auto; }
}

/* The tracking used to ease to .06em between 1040 and 1199, because six
   EQUAL columns were too narrow for the longest label. Auto-sized tracks
   removed the need, so the tracking is now uniform at every width — one
   less place for the bar to look subtly different on a small laptop.
   Re-measured one-line at 1040, 1080, 1120, 1200 and up. */


/* --------------------------------------------------------------------------
   9. GRIDS + CARDS
   -------------------------------------------------------------------------- */

.grid { display: grid; gap: var(--sp-5); }
.grid--3 { grid-template-columns: 1fr; }
.grid--4 { grid-template-columns: 1fr; }
@media (min-width: 720px) {
  .grid--3 { grid-template-columns: repeat(2, 1fr); }
  .grid--4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1000px) {
  .grid--3 { grid-template-columns: repeat(3, 1fr); }
  .grid--4 { grid-template-columns: repeat(4, 1fr); }
}

.reason {
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-7) var(--sp-5) var(--sp-6);
  background: var(--white); border: 1px solid rgba(40,147,157,.20);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(35,31,32,.04), 0 14px 34px rgba(31,117,126,.07);
  transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
.reason::before {
  content: ''; position: absolute; inset: 0 0 auto 0; height: 4px;
  background: linear-gradient(90deg, var(--teal-brand), var(--teal-light));
}
.reason:hover { transform: translateY(-4px); box-shadow: 0 2px 6px rgba(35,31,32,.06), 0 22px 48px rgba(31,117,126,.14); }

/* Icon sits in a tinted well rather than floating on white. */
.reason__icon {
  width: 60px; height: 60px; padding: 14px; border-radius: 50%;
  color: var(--teal-action);
  background: linear-gradient(150deg, var(--wash-1), #FFFFFF);
  border: 1px solid rgba(40,147,157,.22);
  flex: 0 0 auto;
}
.reason h3 { margin: 0; }
.reason p { margin: 0; font-size: var(--step-0); }

.service {
  position: relative;
  display: flex; flex-direction: column;
  background: var(--white); border: 1px solid rgba(40,147,157,.20);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(35,31,32,.04), 0 14px 34px rgba(31,117,126,.07);
  overflow: hidden; transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.service::before {
  content: ''; position: absolute; inset: 0 0 auto 0; height: 4px;
  background: linear-gradient(90deg, var(--teal-brand), var(--teal-light));
}
.service .service__body { padding-top: var(--sp-7); }
.service:hover { box-shadow: 0 2px 6px rgba(35,31,32,.06), 0 24px 52px rgba(31,117,126,.16); transform: translateY(-4px); }
.service__body { padding: var(--sp-6) var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-3); flex: 1; }
.service h3 { margin: 0; }
.service p { margin: 0; flex: 1; }
.service ul { list-style: none; padding: 0; margin: 0; display: grid; gap: .4rem; }
/* Nail is centered on the row, not hung off the first line's cap height.
   min-height matches the nail so single-line items cannot sit high. */
.service li {
  display: flex; gap: .65em; align-items: center;
  font-size: var(--step-0); line-height: 1.5;
  min-height: 22px;
}
.service li img { width: 9px; height: auto; flex: 0 0 auto; align-self: center; }
.service .link-arrow { margin-top: var(--sp-2); }


/* --------------------------------------------------------------------------
   10. SPLIT (local roots + why atlas)
   -------------------------------------------------------------------------- */

.split { display: grid; gap: var(--sp-6); align-items: center; }
@media (min-width: 900px) {
  .split { grid-template-columns: 1fr 1fr; gap: clamp(2rem, 5vw, 4.5rem); }
  .split--reverse .split__media { order: 2; }
}

/* Both split images carry the same corner radius as the service and review
   cards. The modifier classes existed in the markup with no rules behind
   them, which is why the radius disappeared from the Local Roots and Atlas
   photographs — nothing was styling them at all.

   The radius goes on the wrapper with overflow:hidden rather than on the
   <img>, so the shadow is cast by the rounded shape instead of the square
   one underneath it. */
.split__media {
  border-radius: var(--radius);
  overflow: hidden;
}
.split__media img { display: block; width: 100%; height: auto; }

/* On white: a soft teal-tinted lift, matching the cards. */
.split__media--framed {
  box-shadow: 0 1px 2px rgba(35,31,32,.05), 0 18px 46px rgba(31,117,126,.16);
}
/* On the dark Atlas band a drop shadow is invisible, so the image gets a
   faint teal edge and a deeper shadow to separate it from the black. */
.split__media--glow {
  box-shadow: 0 0 0 1px rgba(78,195,206,.22), 0 26px 60px rgba(0,0,0,.55);
}
/* --------------------------------------------------------------------------
   11. PROCESS
   -------------------------------------------------------------------------- */

/* Sits directly under the black Why-Atlas section, so it needs its own
   surface. Charcoal gradient + a teal hairline reads as a distinct band. */
.section--process {
  background: linear-gradient(178deg, #33302F 0%, #292627 55%, #211E1F 100%);
  color: var(--slate);
  border-top: 1px solid rgba(78,195,206,.28);
}
.section--process h2, .section--process h3 { color: var(--white); }
.section--process .eyebrow { color: var(--teal-light); }

.steps { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-5); counter-reset: step; }
@media (min-width: 720px) { .steps { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .steps { grid-template-columns: repeat(4, 1fr); } }

/* THREE steps, not four. The base grid is built for the four-step How It
   Works section; with three items the fourth column simply stays empty and
   the row reads as left-justified with a hole on the right.
   ** This modifier is applied on the page, never inside
   global/how-it-works-section.php ** — that section has four steps and must
   keep its four columns. */
@media (min-width: 1000px) { .steps--3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 720px) and (max-width: 999px) {
  /* Two columns would leave a lone third item stranded underneath. Below
     the three-across breakpoint these stack instead. */
  .steps--3 { grid-template-columns: 1fr; max-width: 40rem; margin-inline: auto; }
}
.steps li { counter-increment: step; padding-top: var(--sp-5); border-top: 3px solid rgba(78,195,206,.35); }
.steps li::before {
  content: counter(step, decimal-leading-zero);
  display: block; margin-bottom: var(--sp-3);
  font-variation-settings: 'wdth' var(--wd-hero), 'wght' 700;
  font-size: var(--step-2); color: var(--teal-light); line-height: 1;
}
.steps h3 { margin: 0 0 var(--sp-2); font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700; font-size: var(--step-1); }
.steps p { margin: 0; font-size: var(--step-0); }

.steps-cta { margin-top: clamp(2.25rem, 5vw, 3.5rem); display: flex; justify-content: center; }


/* --------------------------------------------------------------------------
   12. REVIEWS
   -------------------------------------------------------------------------- */

.rating-summary {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: var(--sp-3) var(--sp-5); margin-bottom: var(--sp-7);
}
.stars { display: inline-flex; gap: .14em; color: var(--teal-brand); }
.stars svg { width: 22px; height: 22px; }
.rating-summary p { margin: 0; font-variation-settings: 'wdth' var(--wd-sub), 'wght' 600; }

/* Five reviews: three across, then two centered. Gutter and row gap are the
   same value so the spacing reads even on every edge. */
.reviews-grid {
  display: grid; gap: var(--sp-5);
  grid-template-columns: 1fr;
}
@media (min-width: 700px)  { .reviews-grid { grid-template-columns: repeat(6, 1fr); } }
@media (min-width: 700px) {
  .reviews-grid > * { grid-column: span 3; }
  .reviews-grid > *:nth-child(1),
  .reviews-grid > *:nth-child(2),
  .reviews-grid > *:nth-child(3) { grid-column: span 2; }
  .reviews-grid > *:nth-child(4) { grid-column: 2 / span 2; }
  .reviews-grid > *:nth-child(5) { grid-column: 4 / span 2; }
}

.review {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-4);
  padding: var(--sp-6) var(--sp-5); text-align: center;
  background: var(--white); border: 1px solid rgba(40,147,157,.20);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(35,31,32,.04), 0 14px 34px rgba(31,117,126,.07);
}
.review blockquote { margin: 0; font-size: var(--step-0); }
.review figcaption {
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700;
  font-size: var(--step--1); color: var(--ink); text-transform: uppercase; letter-spacing: .07em;
}
.review .stars svg { width: 18px; height: 18px; }


/* --------------------------------------------------------------------------
   13. SERVICE AREAS
   -------------------------------------------------------------------------- */

.areas { display: grid; gap: var(--sp-6); align-items: stretch; }
@media (min-width: 900px) { .areas { grid-template-columns: 1fr 1fr; gap: clamp(2rem,5vw,4rem); } }
.areas__list { columns: 2; column-gap: var(--sp-6); list-style: none; margin: 0 0 var(--sp-6); padding: 0; }
.areas__list li {
  break-inside: avoid; padding: .45rem 0; display: flex; gap: .6em; align-items: center;
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 600; font-size: var(--step-0);
  min-height: 24px;
}
.areas__list img { width: 8px; height: auto; flex: 0 0 auto; align-self: center; }
/* The "and surrounding areas" tail. The list keeps its own bottom margin for
   the case where the tail is not present, so pull it in when it is. */
.areas__list:has(+ .areas__more) { margin-bottom: var(--sp-3); }

/* --- Region cards ---------------------------------------------------------
   Each region on the service areas page sits in its own card. With four
   groups of plain headings and lists stacked on one background, the eye had
   nothing to separate them and the page read as one long list with words
   in it. Same visual language as the service cards elsewhere — white
   surface, soft border, generous padding — so it is recognisably the same
   site rather than a new pattern invented for one page. */
.area-card {
  background: var(--white);
  border: 1px solid rgba(35,31,32,.08);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(35,31,32,.04), 0 14px 34px rgba(31,117,126,.07);
  padding: clamp(1.25rem, 3vw, 2rem);
  margin-bottom: clamp(1rem, 2.5vw, 1.5rem);
}
/* The heading already has the card's padding above it, so its own top
   margin would double the gap. */
.area-card > :first-child { margin-top: 0; }
.area-card .areas__list { margin-bottom: 0; }

/* Full-width variant, for the service areas page where the list no longer
   shares the row with the map. Two columns across the whole page would put
   one town name in the middle of 570px of nothing. */
/* One column below 360. At 320 a two-column list gives each town about
   128px, and "Georgetown", "Spartanburg" and "Summerville" each ran a few
   pixels past their column. A single column at that width is not a
   compromise — it is the only honest fit. */
.areas__list--wide { columns: 1; }
@media (min-width: 360px)  { .areas__list--wide { columns: 2; } }
@media (min-width: 700px)  { .areas__list--wide { columns: 3; } }
@media (min-width: 1040px) { .areas__list--wide { columns: 4; } }
/* Body size, not --step--1. It was reading as a footnote when it is really
   the last item of the list above it. Client's call, 26 Aug. ** THIS TOUCHES
   THE HOME PAGE **, which is otherwise locked - it was asked for. */
.areas__more { margin: 0 0 var(--sp-6); color: var(--slate-deep); }
.areas > div:last-child { align-self: center; }
/* Single column: the map leads, the way the Local Roots and Atlas images do.
   Source order keeps the copy first for screen readers and for the two-column
   layout; only the visual order flips. */
@media (max-width: 899px) {
  .areas > div:last-child { order: -1; }
}
.areas__map {
  display: block; width: 100%; height: auto;
  border: 0; border-radius: var(--radius);
  box-shadow: 0 14px 40px rgba(31,117,126,.16);
}


/* --------------------------------------------------------------------------
   14. FAQ
   -------------------------------------------------------------------------- */

.faq { max-width: 900px; margin-inline: auto; }

/* Wide enough that short display headings stay on one line. */
.section-head { max-width: 52rem; }
.faq__item { border-bottom: 1px solid var(--slate); }
.faq__item:first-child { border-top: 1px solid var(--slate); }
.faq__q {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  width: 100%; text-align: left; background: none; border: 0; cursor: pointer;
  padding: var(--sp-5) 0; color: var(--ink);
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700;
  font-size: var(--step-1); line-height: 1.3;
}
.faq__q:hover { color: var(--teal-action); }
.faq__icon { flex: 0 0 auto; width: 22px; height: 22px; transition: transform var(--dur) var(--ease); color: var(--teal-action); }
.faq__q[aria-expanded="true"] .faq__icon { transform: rotate(45deg); }
.faq__a { padding: 0 0 var(--sp-5); max-width: 72ch; }
.faq__a[hidden] { display: none; }


/* --------------------------------------------------------------------------
   15. PARTNER LOGOS
   -------------------------------------------------------------------------- */

.partners { text-align: center; }
.partners__row {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: clamp(2.5rem, 8vw, 6rem); margin-top: 0;
}
.partners__item { display: grid; place-items: center; gap: var(--sp-3); }
.partners__item img { width: auto; max-height: 150px; }
.partners__item--ccu img { max-height: 190px; }
.partners__item--atlas img { max-height: 158px; }

/* Placeholder slot for logos we do not yet have files for */
.logo-slot {
  display: grid; place-items: center; text-align: center;
  min-width: 190px; min-height: 96px; padding: var(--sp-4);
  border: 2px dashed var(--slate-deep); border-radius: var(--radius);
  color: var(--slate-deep);
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  font-size: var(--step--1); text-transform: uppercase; letter-spacing: .08em;
}


/* --------------------------------------------------------------------------
   16. CTA BAND
   -------------------------------------------------------------------------- */

.cta-band {
  position: relative; background: var(--teal-action); color: var(--white);
  overflow: hidden; text-align: center;
}
.cta-band::before {
  content: ''; position: absolute; inset: 0;
  background-image: url('../images/pattern-rr-large.webp');
  background-size: 340px auto;
  background-position: center -40px;
  opacity: .07;
}
.cta-band .wrap { position: relative; z-index: 1; }
.cta-band h2 { color: var(--white); }
.cta-band p { color: rgba(255,255,255,.92); font-size: var(--step-1); }
.cta-band .btn-row { justify-content: center; }


/* --------------------------------------------------------------------------
   17. FOOTER
   Centered stack, after the Herlong Architects reference: brandmark, then the
   blurb and social, then a horizontal nav row, then contact, then the legal
   line. Hairline rules separate the blocks.
   -------------------------------------------------------------------------- */

/* padding-top is --section-y, the same value every .section uses, so the
   gap above the logo matches the gap the teal CTA band leaves below its
   call button. It was --sp-9 (a flat 96px), which read as a dead zone on a
   phone where --section-y resolves to 52px. Tying them to one token means
   they stay matched at every width instead of only at one. */
.site-footer { background: var(--black); color: var(--slate); padding-block: var(--section-y) var(--sp-6); text-align: center; }
.footer-inner { display: grid; justify-items: center; }

.footer-logo img { width: clamp(150px, 18vw, 200px); height: auto; margin-inline: auto; }

/* The wrapper is wide; the measure lives on the paragraphs. The license
   line opts out of it so the whole credential string holds one line on a
   desktop — at 52ch it was breaking after "License". */
.footer-blurb { max-width: 72rem; margin: var(--sp-5) auto 0; }
.footer-blurb p { max-width: 52ch; margin: 0 auto var(--sp-3); }
.footer-blurb p:last-child { margin-bottom: 0; }
.footer-blurb .footer-license,
.footer-blurb .footer-address { max-width: none; }
.footer-blurb strong { color: var(--white); font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700; }

.footer-social { display: flex; justify-content: center; gap: var(--sp-3); margin-top: var(--sp-5); }
.footer-social .social-link { color: var(--white); border-color: rgba(255,255,255,.3); }
.footer-social .social-link:hover { background: var(--teal-light); border-color: var(--teal-light); color: var(--black); }

.footer-rule {
  width: 100%; max-width: 1000px; height: 1px;
  background: rgba(255,255,255,.14); margin: var(--sp-7) auto;
}

/* Column gap only. Vertical rhythm comes from padding on the items, not
   from row-gap, because a zero-height break element sits BETWEEN two row
   gaps and therefore costs double — 59px where a natural wrap costs 43.
   A negative margin cannot claw that back either: the flex line's cross
   size clamps at zero, so the margin is simply ignored.
   With the spacing on the items, a break contributes exactly nothing. */
.footer-nav ul {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 0 clamp(1.25rem, 3vw, 2.5rem);
  list-style: none; padding: 0;
  margin: calc(var(--sp-4) / -2) 0;   /* cancels the outer half-padding */
}
/* ** THE ROW IS UNCHANGED. THE LINK GROWS INTO IT. **
   The row is 43px tall, but that height came from padding on the <li>, so
   only the 15px of text was tappable — the space around it looked like part
   of the link and was not.
   Moving the padding onto the anchor is the obvious fix and the wrong one:
   the moment the <li>'s child becomes a block, the <li> loses its text
   strut and every row shrinks from 43px to 38px. The footer is approved and
   nothing may move.
   So the anchor STAYS INLINE and takes vertical padding. Padding on an
   inline box does not push lines apart, so the layout is untouched to the
   pixel — but the padded box is what the browser hit-tests, and what a
   checker measures. 15px of text becomes a 39px target inside a 43px row.
   Deliberately not calc'd up to the full 43: leaving a hair of the row
   unclaimed guarantees two stacked rows can never overlap each other's
   targets, whatever the font ends up scaling to. */
.footer-nav li { padding-block: calc(var(--sp-4) / 2); }
.footer-nav a { padding-block: var(--sp-3); }
.footer-nav a {
  color: var(--slate); text-decoration: none;
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 600;
  font-size: var(--step--1); text-transform: uppercase; letter-spacing: .11em;
  white-space: nowrap;
}
.footer-nav a:hover { color: var(--teal-light); }

/* Zero-height flex items that take a whole row and force a break. Two of
   them, because the desktop and mobile lists want different splits:

     --desktop  eight links wrapped 6 + 2, which read as an accident.
                This forces 4 + 4. Only where four fit across.
     --mobile   gives "Free Roof Inspection" a line of its own, so the
                longest label is not sharing a row with a short one. */
.footer-nav__break { flex-basis: 100%; height: 0; padding: 0 !important; margin: 0; }
.footer-nav__break--desktop { display: none; }
.footer-nav__break--mobile  { display: block; }
@media (min-width: 900px) {
  .footer-nav__break--desktop { display: block; }
  .footer-nav__break--mobile  { display: none; }
}

.footer-contact {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: var(--sp-3) var(--sp-4);   /* applied on both sides of the dot */
}
.footer-contact a {
  display: inline-flex; align-items: center; gap: .45em;
  color: var(--white); text-decoration: none;
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700;
  font-size: var(--step-0);
}
.footer-contact svg { flex: 0 0 auto; color: currentColor; }   /* matches the number */
.footer-contact a:hover { color: var(--teal-light); }
.footer-contact__sep { color: var(--slate-muted); line-height: 1; }

/* Stacked on phones, where a mid-line dot between two full-width rows
   would sit on its own line and read as a typo. */
@media (max-width: 559px) {
  .footer-contact { flex-direction: column; gap: var(--sp-3); }
  .footer-contact__sep { display: none; }
}

.footer-bottom {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: var(--sp-2) var(--sp-4);   /* applied on both sides of the dot */
  margin-top: var(--sp-7); padding-top: var(--sp-5);
  border-top: 1px solid rgba(255,255,255,.14);
  width: 100%; max-width: 1000px;
  font-size: var(--step--1); color: var(--slate-muted);
}
.footer-bottom p { margin: 0; }
.footer-bottom__sep { color: var(--slate-muted); line-height: 1; }

/* The separator and the stacking share one breakpoint on purpose: the dot
   only makes sense while the two sit on a line together, and a middot left
   between two stacked rows reads as a typo. Same pairing as the footer
   contact line above. */
@media (max-width: 559px) {
  .footer-bottom { flex-direction: column; gap: var(--sp-3); }
  .footer-bottom__sep { display: none; }
}

/* Privacy Policy is a placeholder until the page exists. Rendered as a
   non-navigating control with a tooltip on hover AND keyboard focus, so it
   is not a dead link and not invisible to keyboard users. */
.tip { position: relative; display: inline-block; }
.tip__label {
  font: inherit; background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--slate-muted); text-decoration: none;
}
.tip__label:hover { color: var(--teal-light); }
.tip__bubble {
  position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translate(-50%, 4px);
  background: var(--teal-light); color: var(--black);
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  font-size: .72rem; text-transform: uppercase; letter-spacing: .09em;
  padding: .45rem .7rem; border-radius: 5px; white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity 200ms var(--ease), transform 200ms var(--ease);
}
.tip__bubble::after {
  content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  border: 6px solid transparent; border-top-color: var(--teal-light);
}
.tip:hover .tip__bubble,
.tip__label:focus-visible + .tip__bubble { opacity: 1; transform: translate(-50%, 0); }

/* Inline tel: link inside body copy — "give us a call". */
.call-link {
  color: var(--teal-action);
  font-variation-settings: 'wdth' var(--wd-body), 'wght' 700;
  text-decoration: underline; text-underline-offset: .18em;
  text-decoration-thickness: 2px; text-decoration-color: rgba(40,147,157,.45);
  transition: color 200ms var(--ease), text-decoration-color 200ms var(--ease);
  white-space: nowrap;
}
.call-link:hover { color: var(--black); text-decoration-color: var(--black); }


/* --------------------------------------------------------------------------
   18. STICKY MOBILE CALL BAR
   -------------------------------------------------------------------------- */

.mobile-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
  display: grid; grid-template-columns: 1fr 1fr;
  background: var(--black); border-top: 1px solid rgba(255,255,255,.14);
  padding-bottom: env(safe-area-inset-bottom, 0);
}
.mobile-bar a {
  display: flex; align-items: center; justify-content: center; gap: .5em;
  padding: 1rem .5rem; min-height: 56px; text-decoration: none;
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  font-size: var(--step--1); text-transform: uppercase; letter-spacing: .08em;
}
.mobile-bar__call  { color: var(--white); }
.mobile-bar__quote { background: var(--teal-light); color: var(--black); }

/* ** THESE TWO ARE LOAD-BEARING ON A TOUCH SCREEN. **
   A phone applies :hover after a tap, and the site-wide
   `a:hover { color: var(--black) }` then turns the Call Now label black on a
   black bar - invisible. These restate the resting colors FOR the hover
   state, so a tap changes nothing at all. Removing them looks harmless and
   is not: it was measured turning rgb(35,31,32) on rgb(35,31,32).
   They sit before the gated block below so a real pointer still overrides
   them. */
.mobile-bar__call:hover  { color: var(--white); }
.mobile-bar__quote:hover { color: var(--black); }

/* ** THE BACKGROUND SWAP IS GATED BEHIND (hover: hover). **
   This bar only exists on phones, and a phone applies :hover after a tap and
   KEEPS it until you touch something else — so tapping Call Now left a white
   button sitting in the bar afterwards, which is what the client reported.
   A device with a real pointer still gets the hover; a touch screen never
   does, so the call button stays black and the quote button stays teal.

   The explicit colors are still required: without them the global
   `a:hover { color: black }` turns the Call Now label black on a black bar. */
@media (hover: hover) and (pointer: fine) {
  .mobile-bar__call:hover  { background: var(--white); color: var(--black); }
  .mobile-bar__quote:hover { background: var(--white); color: var(--teal-action); }
}

/* Keyboard focus does NOT swap the background any more — it draws a ring
   instead. Swapping it was the same white-button problem for anyone tabbing
   through, and a ring is the clearer signal in any case. inset, because the
   bar is flush to the bottom of the screen and an outset ring would be cut
   off by the edge. */
.mobile-bar a:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 3px var(--teal-action);
}
.mobile-bar__quote:focus-visible { box-shadow: inset 0 0 0 3px var(--black); }

.mobile-bar a { transition: background var(--dur) var(--ease), color var(--dur) var(--ease); }
.mobile-bar svg { width: 18px; height: 18px; }
@media (min-width: 1040px) { .mobile-bar { display: none; } }
@media (max-width: 1039px) { body { padding-bottom: 56px; } }


/* --------------------------------------------------------------------------
   18b. MOBILE ALIGNMENT

   Sections were a mix: some openers centered, some left. On a narrow screen
   that reads as inconsistency rather than variety, so below 760px every
   section opener (eyebrow, h2, lede) and every CTA row centers. Paragraph
   copy and bullet lists deliberately stay left-aligned — centered body text
   is harder to read and the client asked for it left.
   -------------------------------------------------------------------------- */

@media (max-width: 759px) {
  .section .eyebrow,
  .section > .wrap > h2,
  .section > .wrap > .lede,
  .section > .wrap > .section-head,
  .split > div:not(.split__media) > .eyebrow,
  .split > div:not(.split__media) > h2,
  .areas > div > .eyebrow,
  .areas > div > h2,
  /* The hero was left out of this because <section class="hero"> is not a
     .section, so none of the selectors above reached it — which is exactly
     why its opener stayed left while every other one on the page centered. */
  .hero__eyebrow,
  .hero h1 { text-align: center; }

  /* Every call to action lines up the same way. */
  .btn-row,
  .steps-cta { justify-items: center; justify-content: center; }
  .btn-row:has(> .btn:only-child) { grid-template-columns: minmax(min(100%, 18rem), 1fr); }

  /* The heading measure caps look odd once centered. */
  .lede, .measure { margin-inline: auto; }
}


/* --------------------------------------------------------------------------
   19. REVEAL ANIMATION  (subtle, opt-out respected)
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  .reveal      { opacity: 0; transform: translateY(18px); transition: opacity 640ms var(--ease), transform 640ms var(--ease); }
  .reveal.is-in{ opacity: 1; transform: none; }

  .reveal--fade       { transform: none; }

  /* Horizontal entrances only where there is room for them. Below this
     they would push past the viewport and create a scrollbar. */
  @media (min-width: 900px) {
    .reveal--left     { transform: translateX(-22px); }
    .reveal--right    { transform: translateX(22px); }
  }
  @media (max-width: 899px) {
    .reveal--left,
    .reveal--right    { transform: translateY(18px); }
  }
  .reveal--left.is-in,
  .reveal--right.is-in{ transform: none; }

  .reveal--scale       { transform: scale(.97); }
  .reveal--scale.is-in { transform: none; }
}


/* --------------------------------------------------------------------------
   19b. FIRST-SCREEN ENTRANCE

   The rest of the page reveals on scroll, via IntersectionObserver. The
   first screen must not — nobody scrolls to it, so an observer would either
   fire instantly (pointless) or, worse, leave content invisible if it fired
   late. This is a plain CSS animation on a timeline that starts at first
   paint, so the eyebrow, headline, paragraph, buttons and then the trust
   bar arrive in reading order as one movement.

   Deliberately NOT JavaScript-driven. With animation-fill-mode: both the
   element is hidden only while its own delay is counting down; if
   animations never run — old browser, JS irrelevant here — everything is
   simply visible. Nothing can strand the hero in an invisible state.

   Desktop and tablet only. On a phone the hero is already the whole screen
   and the client asked to keep it simple, so it paints plain.

   Cost check: opacity and transform only, so it is composited and cannot
   move layout — CLS stays 0. The whole sequence finishes at 1.16s and the
   LCP element is the hero photograph, not the type, so LCP is unaffected.
   Both re-measured; see the README. */
@media (min-width: 760px) and (prefers-reduced-motion: no-preference) {
  .hero__eyebrow,
  .hero h1,
  .hero__sub,
  .hero__inner .btn-row,
  .trust__list li {
    animation: firstScreen 620ms var(--ease) both;
  }
  .hero__eyebrow        { animation-delay:  80ms; }
  .hero h1              { animation-delay: 180ms; }
  .hero__sub            { animation-delay: 300ms; }
  .hero__inner .btn-row { animation-delay: 400ms; }

  /* The trust bar rides the same timeline instead of waiting to be
     scrolled into view, so it lands as the tail of the hero rather than
     as a separate event. */
  .trust__list li:nth-child(1) { animation-delay: 500ms; }
  .trust__list li:nth-child(2) { animation-delay: 540ms; }
  .trust__list li:nth-child(3) { animation-delay: 580ms; }
  .trust__list li:nth-child(4) { animation-delay: 620ms; }
  .trust__list li:nth-child(5) { animation-delay: 660ms; }
  .trust__list li:nth-child(6) { animation-delay: 700ms; }
}

@keyframes firstScreen {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}


/* --------------------------------------------------------------------------
   19c. INTERIOR PAGES

   Shared furniture for every page that is not the home page. The home page
   opens with a full-screen slider; interior pages open with either a single
   hero image or, where there is no image, the compact .page-head band.
   -------------------------------------------------------------------------- */

/* --- Trust bar, in-flow variant -------------------------------------------
   The default .trust is an OVERLAY: at 1040px and up it pulls itself up over
   the bottom of the hero with a negative margin and goes translucent. On a
   page with no hero there is nothing to overlay, and that negative margin
   would drag it up over the content above. .trust--flow keeps it in normal
   flow as a solid black band. Use it on any page without a hero image. */
/* A hairline at the top: below 1040px, and on any page where this bar
   follows the dark page head, both surfaces are black and the join would
   otherwise be invisible. */
.trust--flow {
  background: var(--black);
  border-top: 1px solid rgba(255,255,255,.14);
}
@media (min-width: 1040px) {
  .trust--flow {
    margin-top: 0;
    background: var(--black);
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
    border-top: 0;
    border-bottom: 1px solid rgba(255,255,255,.10);
  }
}

/* --- Page hero — a single photograph -------------------------------------
   Every interior page except the two form pages opens with one still image
   rather than the home page's slider. It reuses .hero so the type scale,
   the scrim, the mobile centering and the trust bar overlay all behave
   exactly as they do on the home page — the only differences are height and
   the absence of anything that rotates.

   ** SHORTER THAN THE HOME HERO ON PURPOSE. ** The home page is a front
   door and can afford a full screen. An interior page is somewhere a
   visitor arrived wanting an answer, and a full-screen photograph between
   them and it is a toll booth. */
.hero--page { min-height: min(60svh, 30rem); }
@media (min-width: 1040px) { .hero--page { min-height: min(66svh, 34rem); } }

/* --- Mobile focal point ---------------------------------------------------
   A crop that works on a 16:9 desktop frame often puts the subject off to
   one side of a narrow phone frame, because the phone shows a tall slice of
   the middle. --hero-pos lets a page name the part of the image worth
   keeping, and it is set from the same focal point the file was cropped
   around, so the two cannot drift apart.
   Default is dead center, which is right for most of them. */
.hero--page .hero__slide img { object-position: var(--hero-pos, 50% 50%); }

/* --- nowrap that yields on a narrow phone ---------------------------------
   ** WHY THIS EXISTS. ** Holding a place name together is right on a
   desktop and wrong on a 320px phone: .nowrap cannot break, the hero has
   overflow:hidden, so instead of wrapping the words are simply CUT OFF —
   and because nothing overflows the document, no overflow sweep catches it.
   Measured on the services hero: "South Carolina" ran 64px past the edge at
   320px and 24px past at 360, which is one of the two phones the client
   tests on. It fits from 390 up.

   So the words hold together where there is room and wrap where there is
   not. Below 390 a wrapped place name is far better than half of one. */
.nowrap-yield { white-space: nowrap; }
@media (max-width: 389px) { .nowrap-yield { white-space: normal; } }

/* --- Page hero copy -------------------------------------------------------
   Centered, unlike the home page's left-aligned hero. A page hero carries a
   heading and one line, not a headline plus paragraph plus two buttons, and
   centering reads as a title rather than a pitch. */
/* --- Page hero scrim ------------------------------------------------------
   ** THE HOME PAGE SCRIM IS WRONG FOR THIS HERO AND HAS TO BE REPLACED. **
   That one is a 100deg angled pair, dark on the left and nearly clear on
   the right, because the home page's copy is left-aligned and the
   photograph is meant to stay readable on the other side. Centered copy
   crosses the whole frame, so the right-hand half of every line lands on
   the bright part and the contrast falls apart — measured 2.6:1 on the
   sub-line before this.

   Symmetric instead: an even wash with the top and bottom carrying more, so
   the heading has a floor and the trust bar has something to sit on. Values
   are measured, not chosen — see the contrast note in section 7b. */
.hero--page .hero__slide::after {
  background:
    linear-gradient(180deg,
      rgba(24,21,22,.84) 0%,
      rgba(24,21,22,.78) 42%,
      rgba(24,21,22,.76) 70%,
      rgba(24,21,22,.88) 100%);
}

.hero--page .hero__inner { max-width: 52rem; margin-inline: auto; text-align: center; }
.hero--page h1 {
  --h1-size: clamp(2rem, 1.35rem + 2.6vw, 3.4rem);
  font-size: var(--h1-size);
  line-height: 1.04;
  margin-bottom: var(--sp-4);
  max-width: none;
}

/* ** THE CAP FOR HEADINGS WITH AN OVER-LONG WORD. **
   .hero--page.hero--tight, not .hero--tight: the rule above is (0,2,1) and
   a plainer selector loses the tie and does nothing at all. This is the
   seventh time a source-order or specificity tie has caused a defect here.

   --h1-word is the measured ratio of the heading's longest word to the font
   size, set per page. To re-derive it after a copy change: render the page,
   shrink the h1 until scrollWidth stops exceeding clientWidth, then divide
   clientWidth by that font size and add about 3%.

       Straightforward .. 13.4   (roofing-services)
       Replacement, .... 10.2   (roof-replacement-near-me)

   min() means the cap only bites where the word genuinely will not fit —
   at every wider viewport the normal curve is already the smaller of the
   two and this changes nothing. */
.hero--page.hero--tight h1 {
  font-size: min(var(--h1-size), calc((100vw - 2rem) / var(--h1-word, 13.4)));
}
/* --teal-pale, NOT --teal-light. Measured against the brightest pixel the
   scrim leaves behind this line, teal-light comes out at 3.5:1 and fails;
   teal-pale is 6.5:1 and passes. The eyebrow is small text, so it needs
   4.5:1 — the headline beside it is large and would have passed either way,
   which is exactly the trap: the eyebrow is the line that fails quietly. */
.hero--page .hero__eyebrow { color: var(--teal-pale); }
.hero--page .hero__sub { margin-inline: auto; max-width: 52ch; }

/* --- Page head ------------------------------------------------------------
   The compact opener for a page with no hero image. Same vertical rhythm as
   a section, on the light coastal wash, with the H1 at hero weight but
   section scale — it is still the page's one and only <h1>. */
.page-head {
  background: linear-gradient(180deg, var(--wash-3) 0%, var(--white) 100%);
  padding-block: clamp(2.5rem, 6vw, 4.5rem) clamp(2rem, 5vw, 3.5rem);
  text-align: center;
}

.page-head .eyebrow { margin-bottom: var(--sp-3); }
.page-head h1 {
  margin: 0 auto var(--sp-4);
  max-width: 22ch;
  color: var(--ink);
  font-size: clamp(1.9rem, 1.35rem + 2.2vw, 3.1rem);
  font-variation-settings: 'wdth' var(--wd-head), 'wght' 700;
  line-height: 1.06;
  letter-spacing: -0.015em;
}
.page-head .lede { margin-inline: auto; max-width: 58ch; }

/* ** THE DARK BLOCK MUST STAY BELOW THE LIGHT ONE. ** .page-head--dark h1
   and .page-head h1 weigh exactly the same, so whichever is written last
   wins. Put this block above and the headline renders in ink on black —
   invisible, and a screenshot is the only thing that catches it. This is
   the sixth defect on this project caused by a source-order tie. */
/* --- Page head, dark ------------------------------------------------------
   Black, with the type carrying it. Used ONLY on the two pages that have no
   photograph of their own — get-quote.php and thank-you.php. Every other
   page opens with a hero image, and the home page keeps its slider.

   Same markup as .page-head, different colors, so a page changes treatment
   by gaining one class and nothing else moves. */
.page-head--dark {
  position: relative;
  /* ** A GRADIENT, NOT FLAT BLACK, AND THE REASON MATTERS. **
     These are the only two pages with a black head, and the trust bar
     directly below is also black — so the join vanished and the two read as
     one undifferentiated slab. The head lifts very slightly at its bottom
     edge, which puts the SOLID black bar below it in relief: the bar is the
     darker of the two and reads as its own band.

     ** THE LIFT IS DELIBERATELY SMALL AND IT IS A BRAND TOKEN. **
     --black-soft is the palette's existing "raised surface on black", the
     same value used elsewhere on the site, so this is not a one-off gray
     invented for these two pages. The client asked for the logo's black to
     stay black with only enough differentiation to separate the bar — an
     earlier, lighter charcoal read as a color change rather than a seam.
     Doing it this way round also keeps the credentials as the thing that
     stands out, rather than lightening the bar and washing it out. */
  background: var(--black-head);
  padding-block: clamp(3rem, 7vw, 5rem) clamp(2.5rem, 5.5vw, 4rem);
}

/* A whisper of the brand nail mark so the black is not a flat void. 5% is
   deliberate: it catches the light on a good screen, is invisible on a bad
   one, and never competes with the type. */
.page-head--dark::before {
  content: ''; position: absolute; inset: 0;
  /* ** THE TILE REPEATS AS OF v20. ** It was one centred copy at
     min(56rem, 130%), which showed two marks on a desktop hero and barely
     one on a phone. The file is a 700x700 tile with two marks set on a
     diagonal, built to repeat — so repeating it is what it was drawn for,
     and the marks come out larger AND more numerous rather than one or the
     other. Sized in rem so the count is driven by the hero's real width
     rather than by the viewport twice over. */
  background: url('../images/pattern-rr-large.webp') center / 26rem repeat;
  opacity: .05;
  pointer-events: none;
}
.page-head--dark > .wrap { position: relative; }   /* above the pattern */

.page-head--dark .eyebrow { color: var(--teal-light); }
.page-head--dark h1 {
  color: var(--white);
  font-size: clamp(2.1rem, 1.4rem + 2.8vw, 3.6rem);
  line-height: 1.02;
}
/* --slate, not white: it sits back from the headline instead of competing
   with it. 13.8:1 on black, so stepping back costs nothing in legibility. */
.page-head--dark .lede { color: var(--slate); }

/* NO RULE UNDER THE EYEBROW. There was a short teal one here; removed at
   the client's request so these two pages open the same way the interior
   pages with a photograph do. Consistency across the set beats a flourish
   that only two pages carry. */


/* --- Contact choices ------------------------------------------------------
   "You choose how" — three ways to reach the company, as a plain list rather
   than three competing buttons. The point is that none of them is pushed. */
.choices { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-4); }
.choices li { display: flex; gap: var(--sp-3); align-items: flex-start; }
.choices svg { width: 22px; height: 22px; flex: 0 0 auto; margin-top: 2px; color: var(--teal-action); }
.choices strong {
  display: block;
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700;
  color: var(--ink);
}
/* inline-block plus vertical padding takes these from 17px to 25px. They
   are standalone links on their own line, not links inside a sentence, so
   the WCAG inline exception does not cover them. The padding is vertical
   only, so the text does not move sideways. */
.choices a {
  color: var(--teal-action);
  /* 17px inline -> 25px. These are standalone links on their own line, not
     links inside a sentence, so the WCAG inline exception does not cover
     them. Padding is vertical only and cancelled by the margin, so nothing
     moves in either direction. */
  display: inline-block;
  padding-block: 4px;
  margin-block: -4px;
}

/* --- Leak notice ----------------------------------------------------------
   Deliberately not styled as a warning. It is a helpful redirect, not an
   alarm, so it gets the teal wash and a teal rule rather than red. */
.notice {
  background: var(--teal-wash);
  border-left: 4px solid var(--teal-action);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: var(--sp-4) var(--sp-5);
  margin: 0 0 var(--sp-6);
}
.notice p { margin: 0; }
.notice strong { font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700; color: var(--ink); }

/* --- Form layout ---------------------------------------------------------- */
.form-split { display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: start; }
@media (min-width: 900px) {
  .form-split { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
  /* The form column is sticky on tall screens only. On a short laptop a
     sticky column that is taller than the viewport can never be scrolled
     past, which traps the page. */
  @media (min-height: 900px) {
    .form-split__aside { position: sticky; top: calc(var(--header-h) + var(--sp-6)); }
  }
}

/* --- Wufoo embed ----------------------------------------------------------
   The iframe is a separate document on Wufoo's domain: our stylesheet cannot
   reach inside it. css/form.css does that job, attached in the Wufoo backend.
   What we control from here is the frame itself.

   ** THE HEIGHT IS FIXED AND SET BY HAND. ** A Wufoo iframe does not resize
   to its content, so this is a container with a known height, not a box that
   grows. It has to be tall enough for the form PLUS the space the inline
   error messages take when validation fires, or the submit button ends up
   below the fold of the frame and the form looks broken. */
.form-panel {
  background: var(--white);
  border: 1px solid rgba(35,31,32,.10);
  border-radius: var(--radius);
  box-shadow: 0 1px 2px rgba(35,31,32,.05), 0 18px 46px rgba(31,117,126,.10);
  padding: clamp(1.25rem, 3vw, 2rem);
}
.form-panel__title {
  margin: 0 0 var(--sp-4);
  font-size: var(--step-1);
  font-variation-settings: 'wdth' var(--wd-sub), 'wght' 700;
  color: var(--ink);
}
/* On a phone this is the only heading on screen once the form scrolls into
   view — the aside above it has gone. At --step-1 it read as a label rather
   than the title of the thing you are about to fill in. Phones only; the
   desktop layout has the aside beside it for scale and is unchanged. */
@media (max-width: 759px) {
  .form-panel__title { font-size: var(--step-2); line-height: 1.18; }
}
.wufoo-embed { display: block; width: 100%; border: 0; }

/* 22px, two under the minimum. The two pixels are added BELOW the text and
   taken straight back as negative margin: the words do not move, the card
   does not grow, the target clears 24. (Growing it symmetrically does not
   work here — a later rule sets margin-top on this link, so a negative
   margin-block would only win on the bottom side and the card would gain a
   pixel.) */
.link-arrow { padding-bottom: 2px; margin-bottom: -2px; }


/* --- Two more that the sweep turned up once the four above were done -----
   Same principle: grow the hit box, leave the layout alone. */

/* The Privacy Policy tooltip toggle, 22px — two short. It is inline-block,
   so the two pixels are added below the text and taken straight back as
   negative margin. */
/* 2px got it to exactly 24, which rounds under the minimum at some zoom
   levels. 4px clears it properly. */
.tip__label { padding-bottom: 4px; margin-bottom: -4px; }

/* The link that opens the Wufoo form in its own tab if the embed cannot
   run. Inline, so vertical padding grows the target and moves nothing. */
.wufoo-embed a { padding-block: var(--sp-1); }


/* --- THE ATLAS SECTION ----------------------------------------------------
   Rebuilt 24 Aug to the client's layout: two columns, then a full-width band
   for the four steps.

   WHAT THE FIRST ATTEMPT GOT WRONG. The intro sat above the columns at
   .measure (66ch) centred, while the grid below ran at 76rem. The columns
   started further left than the intro, and the intro's last paragraph landed
   in the right column's horizontal space, so it read as a third column and
   the section looked like three overlapping blocks. Widening the intro would
   have patched the symptom; moving it INTO the left column removes the two
   mismatched containers altogether. ** Do not reintroduce a full-width block
   above these columns at a different width. **
   -------------------------------------------------------------------------- */
/* margin-top, because .atlas-col > :first-child resets the h3's own top
   margin (it has to, or the heading double-spaces against the paragraph
   above it when the columns stack). Without this the column headings sat
   16px under a display-size h2 and read as attached to it. The comparable
   gap on this page is the warranty section's, where 48px of card padding
   does the same job - so 48px here, from the existing --sp-7 token rather
   than a number invented for this one spot. */
.atlas-grid { max-width: 66ch; margin-inline: auto; margin-top: var(--sp-7); }

@media (min-width: 1040px) {
  .atlas-grid {
    max-width: 78rem;
    display: grid; grid-template-columns: 1fr 1fr;
    gap: 0 clamp(2.5rem, 4vw, 4.5rem);
    align-items: start;
  }

  /* ** display:contents, deliberately. ** .atlas-detail exists only so the
     Read More has a single thing to collapse on a phone. At this width it
     dissolves, its two children become grid items of .atlas-grid directly,
     and that is what lets the steps band span both columns. A plain wrapper
     would trap the steps inside the right-hand column.
     Safe here because it is an unstyled <div> with no role of its own - the
     old display:contents accessibility bug only affected elements whose
     semantics mattered. */
  .atlas-detail { display: contents; }

  /* The rule between the columns is drawn on column B rather than as a third
     track, so it cannot affect the sizing of either. */
  .atlas-col--b {
    border-left: 1px solid var(--rule-on-black);
    padding-left: clamp(2.5rem, 4vw, 4.5rem);
  }
}

/* Stacked on a phone, column B needs air above its heading. The space goes
   on the WRAPPER, not on the heading, so the heading's own reset can stay -
   putting it on the heading would double up with the paragraph margin above
   it at some widths and not at others. */
.atlas-col > :first-child { margin-top: 0; }

/* ** BELOW 1040 THE COLUMN DIVIDER BECOMES A HORIZONTAL RULE. **
   Stacked, the two topics ran into each other: 48px of margin alone did not
   read as a boundary, and the client could not tell where one ended. This is
   the same divider the desktop draws between the columns, turned on its
   side. Above 1040 it goes back to being vertical (see .atlas-col--b in the
   grid block). */
.atlas-col--b {
  margin-top: var(--sp-7);
  padding-top: var(--sp-7);
  border-top: 1px solid var(--rule-on-black);
}
@media (min-width: 1040px) {
  .atlas-col--b { margin-top: 0; padding-top: 0; border-top: 0; }
}

/* --- The four steps: its own band, centred, full width -------------------
   The rule runs at every width, not just desktop. It is what makes this read
   as its own section rather than as a third thing bolted to the bottom of
   the right-hand column - which is exactly the confusion the client flagged
   in the previous version. */
.atlas-steps {
  margin-top: var(--sp-7);
  padding-top: var(--sp-7);
  border-top: 1px solid var(--rule-on-black);
}
@media (min-width: 1040px) {
  .atlas-steps {
    grid-column: 1 / -1;          /* under BOTH columns */
    margin-top: var(--sp-8);
  }
}
/* Wider than the reading measure, because it is a centred opener rather than
   running prose - and because four columns beneath it need the room. */
.atlas-steps__lede,
.atlas-steps__close { max-width: 54rem; margin-inline: auto; }
.atlas-steps__close { margin-top: var(--sp-6); }
.atlas-steps h3 { margin-bottom: var(--sp-3); }

/* --- steps-list--4: the numeral ABOVE the copy, four across --------------
   The default .steps-list puts a 1.8rem badge beside the text. At a quarter
   of the page there is not room for the badge AND a readable measure on one
   line, so the badge moves above and the copy runs the full column width.
   Text stays left-aligned inside each step: the heading, the lede and the
   closing line are centred, but four columns of centred body copy would give
   every line a different starting point and be genuinely hard to read. */
.steps-list--4 { margin-top: var(--sp-6); }

/* ** THE LEAD-IN IS A BLOCK, SO THE BODY ALWAYS STARTS ON A NEW LINE. **
   Inline, each step broke wherever its lead-in happened to end: steps 1 and
   3 fell on a clean line, step 2 left "3M" stranded on the end of its
   heading, and step 4 ran "shift. Samples go to an" together. Four steps
   with four different shapes. As a block they all read the same way and no
   step can strand a word again after a copy edit.

   :first-child, NOT plain strong - step 1 has a second <strong> mid-sentence
   ("minimum 10% blend") that must stay inline. */
.steps-list--4 > li > strong:first-child { display: block; margin-bottom: .2rem; }
.steps-list--4 > li { padding-left: 0; }
.steps-list--4 > li::before { position: static; margin-bottom: .7rem; }

@media (min-width: 700px) {
  .steps-list--4 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--sp-6) clamp(1.5rem, 3vw, 2.5rem); }
  .steps-list--4 > li { margin-bottom: 0; }
}
@media (min-width: 1040px) {
  .steps-list--4 { grid-template-columns: repeat(4, 1fr); }
}

/* --- The Read More (below 1040 only) ------------------------------------ */
/* Collapsed state. max-height rather than display:none so the copy fades out
   mid-sentence - the reader can see that it continues, which is what makes a
   Read More read as "there is more" rather than "that is all". */
.atlas-detail.is-collapsed {
  max-height: 15rem;
  overflow: hidden;
  position: relative;
  -webkit-mask-image: linear-gradient(180deg, #000 60%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 60%, transparent 100%);
}

/* ** [hidden] MUST BE RESTATED WHENEVER A RULE SETS display. **
   The browser's own [hidden] { display: none } is a plain type-less rule and
   loses to any class that sets display, so .atlas-toggle{display:flex} made
   the button visible on desktop even with the attribute set. */
.atlas-toggle[hidden] { display: none; }

.atlas-toggle {
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  margin: var(--sp-5) auto 0;
  padding: .7rem 1.4rem;
  background: transparent; cursor: pointer;
  border: 1px solid rgba(255,255,255,.32); border-radius: 999px;
  color: var(--teal-light);
  font-family: inherit; font-size: var(--step--1);
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  text-transform: uppercase; letter-spacing: .1em;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.atlas-toggle:hover,
.atlas-toggle:focus-visible { background: var(--teal-action); border-color: var(--teal-action); color: var(--white); }
.atlas-toggle svg { transition: transform var(--dur) var(--ease); flex: 0 0 auto; }
.atlas-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }

/* --- Long-form lists ------------------------------------------------------
   For body copy that has to carry bullets and a numbered sequence — the
   Atlas section on the roof replacement page is the only place so far.
   Deliberately plain: these sit inside running prose, so they should read
   as part of it rather than as cards. */
.checklist { list-style: none; margin: 0 0 var(--sp-5); padding: 0; display: grid; gap: .55rem; }
.checklist li { display: flex; gap: .7em; align-items: flex-start; }
.checklist li::before {
  content: ''; flex: 0 0 auto;
  width: 7px; height: 7px; margin-top: .62em;
  border-radius: 50%;
  background: var(--teal-light);
}

/* A real <ol>, so a screen reader announces the count and the position.
   The numeral is drawn by a counter rather than the browser's marker, so it
   can carry the brand weight and color. */
.steps-list { list-style: none; margin: 0 0 var(--sp-5); padding: 0; counter-reset: atlas; }
.steps-list > li {
  counter-increment: atlas;
  position: relative;
  padding-left: 2.6rem;
  margin-bottom: var(--sp-4);
}
.steps-list > li::before {
  content: counter(atlas);
  position: absolute; left: 0; top: -0.1em;
  width: 1.8rem; height: 1.8rem;
  display: grid; place-items: center;
  border-radius: 50%;
  background: rgba(78,195,206,.14);
  color: var(--teal-light);
  font-variation-settings: 'wdth' var(--wd-ui), 'wght' 700;
  font-size: var(--step--1);
  line-height: 1;
}
/* On a light section the teal wash needs to be the action teal instead —
   teal-light on white is 2.4:1 and would fail. Only used on dark today, but
   the rule stops the next person from being caught out. */
.section:not(.section--dark):not(.section--process) .steps-list > li::before {
  background: rgba(31,117,126,.10);
  color: var(--teal-action);
}
.section:not(.section--dark):not(.section--process) .checklist li::before {
  background: var(--teal-action);
}

/* --- Gallery --------------------------------------------------------------
   Two per row, as the client asked, matching the reference they gave.
   One column below 700px — two tiles side by side on a phone are too small
   to show whether a roof was laid well, which is the entire point. */
.gallery {
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.75rem);
  grid-template-columns: 1fr;
}
@media (min-width: 700px) { .gallery { grid-template-columns: repeat(2, 1fr); } }

.gallery__item {
  margin: 0;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--slate-mid);
  box-shadow: 0 1px 2px rgba(35,31,32,.05), 0 14px 34px rgba(31,117,126,.10);
}

/* A button, not a div with a click handler: it is focusable, it works from
   the keyboard, and a screen reader announces it as something you can
   activate. The photograph's alt text is the button's accessible name. */
.gallery__btn {
  display: block; width: 100%; padding: 0; border: 0;
  background: none; cursor: zoom-in;
}
.gallery__btn:focus-visible { outline: 3px solid var(--teal-action); outline-offset: 3px; }

/* aspect-ratio holds the tile's shape before the image arrives, so the grid
   does not reflow as photographs load — the same reason every <img> on this
   site carries width and height. */
.gallery__item img {
  width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover;
  transition: transform 600ms var(--ease);
}
.gallery__btn:hover img { transform: scale(1.04); }

/* --- Gallery reveal, BOTH directions --------------------------------------
   The site-wide .reveal fires once and stays. The client asked for tiles
   that fade as you scroll up as well as down, so these get their own class
   that the observer toggles rather than latches. Transform is a rise, not a
   scale, so it cannot fight the hover scale above. */
.gallery__item {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 620ms var(--ease), transform 620ms var(--ease);
}
.gallery__item.is-visible { opacity: 1; transform: none; }

/* Stagger the second of each pair so a row arrives as two beats, not one. */
.gallery__item:nth-child(even) { transition-delay: 90ms; }

@media (prefers-reduced-motion: reduce) {
  .gallery__item { opacity: 1; transform: none; transition: none; }
  .gallery__btn:hover img { transform: none; }
}

/* --- Lightbox -------------------------------------------------------------
   A <dialog>, so the browser supplies the modal semantics, the focus trap
   and Escape-to-close rather than us reimplementing all three badly. */
.lightbox {
  padding: 0; border: 0; max-width: 100%; max-height: 100%;
  width: 100%; height: 100%;
  background: rgba(20,18,19,.94);
  color: var(--white);
}
.lightbox::backdrop { background: rgba(20,18,19,.94); }
.lightbox__inner {
  height: 100%;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(1rem, 4vw, 3rem);
}
.lightbox img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  border-radius: var(--radius-sm);
}
.lightbox__close {
  position: absolute; top: clamp(.75rem, 2vw, 1.25rem); right: clamp(.75rem, 2vw, 1.25rem);
  width: 44px; height: 44px;           /* full touch target, not a 24px x */
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.10); color: var(--white);
  border: 0; border-radius: 50%; cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.35);
}
.lightbox__close:hover { background: var(--teal-action); box-shadow: inset 0 0 0 1px var(--teal-action); }
.lightbox__close:focus-visible { outline: 3px solid var(--teal-light); outline-offset: 3px; }

/* --- Print ---------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   20. PRINT
   -------------------------------------------------------------------------- */

@media print {
  .site-header, .mobile-bar, .hero__controls, .cta-band { display: none !important; }
  body { color: #000; background: #fff; }
}
