/* ============================================================================
   RINGO ROOFING RESTORATION EXPERT LLC — "Storm Light on an American House"
   Shared design system. See DESIGN.md for the doctrine this implements.

   The conventional local-contractor page, built with conviction: a cinematic
   storm hero, then full-bleed white / navy / red bands stacked in rhythm.
   ========================================================================= */

:root {
  /* --- Navy: authority --- */
  --navy-950: #061428;
  --navy-900: #0a1e3f;
  --navy-800: #102b52;
  --navy-700: #1b4079;

  /* --- Red: action & urgency --- */
  --red-600: #c31824;   /* red TEXT on white — clears 4.5:1 */
  --red-500: #d42130;   /* red SURFACES + primary action */
  --red-400: #ee3b47;   /* hover / lightning only */

  /* --- Light --- */
  --white:  #ffffff;
  --bone:   #f4f6f9;
  --bone-2: #e6eaf1;

  /* --- Ink --- */
  --ink:      #16202f;
  --ink-2:    #54617a;
  --steel:    #9db0cd;
  --steel-2:  #6d81a4;
  /* The lightest ink that still clears 4.5:1 on white — placeholders and
     disabled text. The value this replaced (#7c88a0) measured 3.57:1, which
     means every placeholder on the site was failing on the one screen most
     likely to be read in daylight glare. */
  --ink-3: #6b7791;
  --hairline: #dbe1ea;
  --hairline-dark: rgba(157, 176, 205, .20);

  /* --- Type --- */
  --display: "Barlow Condensed", "Haettenschweiler", Impact, sans-serif;
  --body:    "Barlow", system-ui, -apple-system, sans-serif;

  /* --- Space --- */
  --hair: 4px; --xs: 8px; --sm: 14px; --md: 22px; --lg: 36px; --xl: 60px;
  --band: clamp(72px, 8.5vw, 132px);
  --gutter: clamp(20px, 5vw, 56px);
  --shell: 1240px;
  --header-h: 124px;
  /* How far the logo plate hangs below the header. The hero adds this to its
     own top padding so the plate never lands on the eyebrow. */
  --brand-drop: 46px;
  /* Distance from the viewport's left edge to the shell's content edge. The
     logo plate uses this to bleed off the side of the screen instead of
     floating in from the gutter. */
  --bleed-left: calc((100vw - min(var(--shell), 100vw)) / 2 + var(--gutter));

  /* --- Depth --- */
  --card-rest: 0 2px 4px rgba(6, 20, 40, .05), 0 12px 28px -14px rgba(6, 20, 40, .22);
  --card-lift: 0 4px 8px rgba(6, 20, 40, .06), 0 22px 44px -16px rgba(6, 20, 40, .30);

  /* --- Motion --- */
  --ease: cubic-bezier(.16, 1, .3, 1);
  --flash: 0;

  /* --- Layers --- */
  --z-video: 0;
  --z-rain: 1;
  --z-dawn: 2;
  --z-scrim: 3;
  --z-light: 4;
  --z-hero: 5;
  --z-header: 80;
  --z-callbar: 85;
  --z-menu: 90;
}

/* ===== Icons — one grammar =====
   <use> against index.html sprite. 24 grid, square caps, mitre joins.
   non-scaling-stroke pins stroke to device px, so the value below IS rendered weight
   at every size. Set on shapes, NOT svg — does not inherit. Without it old inline
   icons ran 1.5px–4px across three viewBoxes. Component rules outrank .ic; weight
   tiers vary optically (52px needs more stroke than 17px). */

.ic {
  width: 24px; height: 24px; flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: square;
  stroke-linejoin: miter;
}
.ic * { vector-effect: non-scaling-stroke; }
/* The trust mark is the real flag, not a drawn one: a 4:3 field, 14px tall, with a
   hairline so its white stripes have an edge against the navy. Square corners —
   a radius on a flag reads as a sticker. It is the one full-colour mark in the
   header, and it harmonises because the palette is already red/white/navy. */
.flag-us {
  width: auto; height: 14px; flex: none;
  border: 1px solid rgba(255, 255, 255, .22);
}

/* The sprite itself never renders. Not display:none — that can stop referenced
   symbols painting in some engines; a zero box that is out of flow is safe. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ==========================================================================
   Reset & base
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
          text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Full-bleed bands + deliberate horizontal bleeds (logo plate, marquee, truck).
     overflow-x on body alone does not hold: html stays the scrolling box, so the page
     still dragged sideways and showed a white gutter right.
     clip, not hidden, declared after it as a progressive upgrade: hidden on one axis
     forces the other to auto → scroll container → breaks position:sticky.
     `overflow: visible clip` creates no scroll container. */
  overflow-x: hidden;
  overflow-x: clip;
  /* Includes the logo plate's overhang — without it, anchor jumps park section
     headings underneath the plate. */
  scroll-padding-top: calc(var(--header-h) + var(--brand-drop) + 14px);
}

body {
  font-family: var(--body);
  font-size: 1.05rem;
  line-height: 1.65;
  color: var(--ink);
  background: var(--white);
  overflow-x: hidden;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
}

img, svg, video, iframe { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
a { color: inherit; }
ul { list-style: none; }

:focus-visible { outline: 3px solid var(--red-400); outline-offset: 3px; }
/* Engines without :focus-visible would otherwise show no focus ring at all.
   Scoped by @supports so browsers that have it keep the mouse-quiet behaviour. */
@supports not selector(:focus-visible) {
  :focus { outline: 3px solid var(--red-400); outline-offset: 3px; }
}

.skip {
  position: absolute; left: var(--gutter); top: -100px; z-index: 200;
  background: var(--red-500); color: #fff;
  font-family: var(--display); font-weight: 800; font-size: 1rem;
  text-transform: uppercase; letter-spacing: .06em; text-decoration: none;
  padding: 14px 22px; border-radius: 2px;
  transition: top .18s var(--ease);
}
.skip:focus { top: 12px; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ==========================================================================
   Type roles
   ========================================================================== */

.display {
  font-family: var(--display); font-weight: 900;
  font-size: clamp(3rem, 6.6vw, 5.6rem);
  line-height: .86; letter-spacing: -.018em;
  text-transform: uppercase; text-wrap: balance;
}
.headline {
  font-family: var(--display); font-weight: 900;
  font-size: clamp(2rem, 4.2vw, 3.4rem);
  line-height: .92; letter-spacing: -.012em;
  text-transform: uppercase; text-wrap: balance;
}
.title {
  font-family: var(--display); font-weight: 800;
  font-size: 1.42rem; line-height: 1.08; letter-spacing: .002em;
  text-transform: uppercase;
}
.label {
  font-family: var(--display); font-weight: 700;
  font-size: .82rem; line-height: 1.2; letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--red-600);
}
.prose { max-width: 68ch; text-wrap: pretty; }
.prose + .prose { margin-top: var(--md); }

/* ==========================================================================
   Bands & shell
   ========================================================================== */

.shell { width: min(var(--shell), 100%); margin: 0 auto; padding: 0 var(--gutter); }

.band { padding-block: var(--band); }
.band--white { background: var(--white); color: var(--ink); }
.band--bone  { background: var(--bone);  color: var(--ink); }
.band--navy {
  background: var(--navy-900); color: var(--steel);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06);
}
.band--navy-deep { background: var(--navy-950); color: var(--steel); }
.band--red { background: var(--red-500); color: #fff; }

.band--navy .headline, .band--navy-deep .headline,
.band--navy .title,    .band--navy-deep .title { color: #fff; }
.band--navy .label,    .band--navy-deep .label { color: var(--steel); }
.band--red .label { color: rgba(255, 255, 255, .82); }

/* Section head: eyebrow, heading, the one red rule, optional deck. */
.sec-head { margin-bottom: var(--xl); }
.sec-head--center { text-align: center; }
.sec-head--center .sec-head__rule { margin-inline: auto; }
.sec-head--center .sec-head__deck { margin-inline: auto; }
.sec-head__rule {
  width: 56px; height: 3px; background: var(--red-500);
  margin-top: var(--md);
}
.band--red .sec-head__rule { background: #fff; }
.sec-head .headline { margin-top: var(--sm); }
.sec-head__deck { margin-top: var(--md); color: var(--ink-2); max-width: 62ch; }
.band--navy .sec-head__deck, .band--navy-deep .sec-head__deck { color: var(--steel); }
.band--red .sec-head__deck { color: rgba(255, 255, 255, .9); }

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 11px;
  font-family: var(--display); font-weight: 800; font-size: 1.42rem;
  text-transform: uppercase; letter-spacing: .002em;
  text-decoration: none; border-radius: 2px;
  transition: background .18s ease, color .18s ease, border-color .18s ease, transform .18s var(--ease), box-shadow .18s ease;
}
.btn svg { width: .95em; height: .95em; flex: none; }

.btn--primary { background: var(--red-500); color: #fff; padding: 19px 34px; }
.btn--primary:hover { background: var(--red-400); transform: translateY(-2px); }
/* only in the hero does the primary catch the lightning */
.hero .btn--primary {
  box-shadow: 0 0 calc(16px + var(--flash) * 48px) rgba(238, 59, 71, calc(.18 + var(--flash) * .66));
}

.btn--outline {
  padding: 18px 32px; border: 2px solid var(--ink); color: var(--ink); background: transparent;
}
.btn--outline:hover { background: var(--ink); color: #fff; transform: translateY(-2px); }

.btn--outline-light {
  padding: 18px 32px; border: 2px solid rgba(255, 255, 255, .40); color: #fff; background: transparent;
}
.btn--outline-light:hover { background: #fff; color: var(--navy-950); transform: translateY(-2px); }

.btn--white { background: #fff; color: var(--red-600); padding: 19px 34px; }
.btn--white:hover { background: var(--bone); transform: translateY(-2px); }

.actions { display: flex; flex-wrap: wrap; gap: var(--sm); }

/* An inline red text link with a sliding arrow. */
.more {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--display); font-weight: 800; font-size: 1.02rem;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--red-600); text-decoration: none;
}
.more svg { width: 15px; height: 15px; transition: transform .18s var(--ease); }
.band--navy .more, .band--navy-deep .more { color: #fff; }

/* ==========================================================================
   Header & nav
   ========================================================================== */

.site-header {
  position: fixed; top: 0; left: 0; right: 0; z-index: var(--z-header);
  height: var(--header-h);
  background: transparent;
  filter: drop-shadow(0 6px 18px rgba(6, 20, 40, .16));
  transition: height .34s var(--ease), filter .34s ease;
}
.utility-bar {
  height: 36px;
  background: #050c1d;
  color: #fff;
  border-top: 1px solid rgba(255, 255, 255, .16);
  border-bottom: 1px solid rgba(255, 255, 255, .10);
  overflow: hidden;
  transition: height .34s var(--ease), opacity .26s ease, border-width 0s .34s;
}
.utility-bar__in {
  height: 100%; width: min(var(--shell), 100%); margin: 0 auto;
  padding: 0 var(--gutter);
  display: flex; align-items: center; justify-content: space-between; gap: var(--lg);
}
.utility-bar__trust,
.utility-bar__contact,
.utility-bar__contact span,
.utility-bar__contact a {
  display: inline-flex; align-items: center;
}
.utility-bar__trust {
  gap: 12px;
  font-family: var(--display); font-weight: 800; font-size: .82rem;
  line-height: 1; text-transform: uppercase; letter-spacing: .04em;
}
/* The separator was a ★ glyph set in the body font — a different shape language
   from everything around it. It is the marquee's red diamond instead. */
.utility-bar__trust .sep {
  width: 5px; height: 5px; flex: none;
  background: var(--red-500);
  transform: rotate(45deg);
}
.utility-bar__contact {
  gap: clamp(16px, 2.2vw, 30px);
  font-family: var(--display); font-weight: 800; font-size: .88rem;
  line-height: 1; text-transform: uppercase; letter-spacing: .03em;
}
.utility-bar__contact span,
.utility-bar__contact a {
  gap: 8px;
  white-space: nowrap;
  text-decoration: none;
}
.utility-bar__contact svg {
  width: 17px; height: 17px; flex: none;
}
.utility-bar__contact a svg { color: var(--red-500); }
/* The utility bar carries the standing phone number and the service area only.
   The one Free-Inspection action lives in the nav row below it. */
.main-nav {
  position: relative;
  height: 88px;
  background: #fff;
  border-bottom: 1px solid rgba(6, 20, 40, .12);
  transition: height .34s var(--ease);
}
.site-header__in {
  height: 100%; width: min(var(--shell), 100%); margin: 0 auto;
  padding: 0 var(--gutter);
  display: flex; align-items: center; gap: clamp(18px, 2.2vw, 36px);
}

/* The logo sits in a proud white plate, clipped like the reference header and
   extended slightly into the hero without adding any borrowed badge language. */
.brand {
  position: relative;
  align-self: stretch;
  flex: 0 0 clamp(210px, 18.5vw, 296px);
  /* start-aligned, so the logo stays clear of the angled corner the clip-path
     cuts out of the bottom right */
  display: grid; place-items: center start;
  /* Runs off the left edge of the screen so it reads as a plate anchored to
     the header, not a card floating in the middle of it. */
  flex-basis: calc(clamp(210px, 18.5vw, 296px) + var(--bleed-left));
  margin: 0 14px calc(var(--brand-drop) * -1) calc(var(--bleed-left) * -1);
  padding: 8px 46px 10px var(--bleed-left);
  background: #fff;
  clip-path: polygon(0 0, 100% 0, 100% 67%, 88% 100%, 0 100%);
  transition: margin-bottom .34s var(--ease);
}
.brand::before {
  content: "";
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0) 62%, rgba(6, 20, 40, .06) 100%),
    linear-gradient(90deg, rgba(244, 246, 249, .65), rgba(255, 255, 255, 0) 42%);
  pointer-events: none;
}
/* Bound by an explicit HEIGHT, not by width and not by a percentage. The logo
   is 548x390: sized by width it grew taller than the plate's content box and
   the clip-path sheared the bottom off the wordmark, and `max-height: 100%`
   doesn't help because the grid area's height is indefinite, so the percentage
   resolves to none and the image falls back to width-driven sizing. */
.brand img {
  position: relative;
  height: clamp(80px, 6.4vw, 98px);
  width: auto;
  max-width: 100%;
  filter: drop-shadow(0 5px 10px rgba(6, 20, 40, .12));
}

/* Condensed header — the focal motion. Once the reader is past the hero the
   dark utility bar collapses, the nav row tightens, and the logo plate lifts,
   reclaiming ~40px of fixed real estate. One class toggle from the scroll
   substrate (site.js), with hysteresis, drives all of it. Scoped to the width
   where the full two-row desktop header actually exists. */
@media (min-width: 761px) {
  .site-header.is-condensed { height: 74px; filter: drop-shadow(0 10px 24px rgba(6, 20, 40, .24)); }
  .site-header.is-condensed .utility-bar {
    height: 0; opacity: 0; border-top-width: 0; border-bottom-width: 0;
  }
  .site-header.is-condensed .main-nav { height: 74px; }
  .site-header.is-condensed .nav a { padding: 24px 0 21px; }
  .site-header.is-condensed .nav a::after { bottom: 12px; }
  .site-header.is-condensed .brand { margin-bottom: -16px; }
  /* Sized against the rest state, not in isolation. The rest logo is
     clamp(80px, 6.4vw, 98px); at 1440px that is 92px, and the old 52px landing
     made the condense a 43% collapse where the design intends roughly a third.
     The condensed plate is 74px tall with a -16px lift, so its content box is
     72px — 66px lands inside it with air and keeps the mark legible once the
     utility bar's phone number is gone. */
  .site-header.is-condensed .brand img { height: clamp(58px, 4.6vw, 66px); transition: height .34s var(--ease); }

  /* Navy, not red — the red Free-Inspection button is still the one red action in view.
     Number carries on weight/size; Red Pressed on hover. Icon stays Ink 2 both states:
     red is never an icon fill on light ground. min-height clears the 44px target the
     utility bar's 17px link missed.
     Appears, not transitions: display can't animate, and reserving width at rest would
     shove nav links the whole time the bar already carries the number. Fade rides the
     same 340ms as the collapse. */
  .site-header.is-condensed .header-tel {
    display: inline-flex; align-items: center; gap: 10px;
    min-height: 44px; padding: 0 2px;
    font-family: var(--display); font-weight: 800; font-size: 1.1rem;
    letter-spacing: .02em; text-transform: uppercase; white-space: nowrap;
    color: var(--navy-950); text-decoration: none;
    transition: color .16s ease;
    animation: telIn .32s var(--ease) both;
  }
  .site-header.is-condensed .header-tel svg { width: 19px; height: 19px; color: var(--ink-2); }
  .site-header.is-condensed .header-tel:hover { color: var(--red-600); }
}
@keyframes telIn { from { opacity: 0; transform: translateY(-6px); } }

.nav { margin-left: auto; display: flex; align-items: center; gap: clamp(24px, 3.1vw, 56px); }
.nav a {
  position: relative;
  font-family: var(--display); font-weight: 800; font-size: .93rem;
  letter-spacing: .03em; text-transform: uppercase;
  color: var(--ink); text-decoration: none; white-space: nowrap;
  padding: 37px 0 34px; border-bottom: 0;
  transition: color .16s ease, padding .34s var(--ease);
}
.nav a::after {
  content: "";
  position: absolute; left: 50%; bottom: 22px;
  width: 52px; height: 3px;
  background: var(--red-500);
  transform: translateX(-50%) scaleX(0);
  transform-origin: center;
  transition: transform .2s var(--ease), bottom .34s var(--ease);
}
.nav a:hover,
/* Both values: "page" is the homepage marking Home, "true" is a service page
   marking Services as the current section without claiming to be that URL. */
.nav a[aria-current] { color: var(--navy-950); }
.nav a:hover::after,
.nav a[aria-current]::after { transform: translateX(-50%) scaleX(1); }

.header-cta { display: flex; align-items: center; gap: var(--md); }

/* The standing call, second half. The utility bar carries the number at rest; the
   moment the header condenses and collapses that bar, this takes its place, so
   there is no scroll depth above 760px without a tap-to-call. Exactly one of the
   two is ever on screen, which is what keeps it from being the duplicate the
   utility bar's Free-Inspection link was. Hidden by default and switched on only
   inside the condensed block below, so it can never appear alongside the bar. */
.header-tel { display: none; }
.header-btn {
  min-width: clamp(190px, 16vw, 240px);
  background: var(--red-500); color: #fff; text-decoration: none;
  font-family: var(--display); font-weight: 800; font-size: 1.1rem;
  text-transform: uppercase; letter-spacing: .02em;
  padding: 17px 24px; border-radius: 2px; white-space: nowrap;
  display: inline-flex; align-items: center; justify-content: center; gap: 18px;
  transition: background .16s ease, transform .18s var(--ease);
}
.header-btn svg { width: 24px; height: 18px; flex: none; }
.header-btn:hover { background: var(--red-400); transform: translateY(-1px); }

.menu-toggle {
  display: none; margin-left: auto;
  font-family: var(--display); font-weight: 800; font-size: 1rem;
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink);
  border: 2px solid rgba(6, 20, 40, .24); border-radius: 2px;
  padding: 10px 15px;
}

/* The panel comes down out of the header and goes back up into it — the menu
   button is where it lives when closed. visibility (not display) so the
   movement is animatable and closed links stay out of the a11y tree. */
.menu-panel {
  position: fixed; inset: var(--header-h) 0 0 0; z-index: var(--z-menu);
  background: var(--navy-950);
  padding: var(--lg) var(--gutter) var(--xl);
  overflow-y: auto;
  visibility: hidden; opacity: 0; pointer-events: none;
  transform: translateY(-14px);
  transition: opacity .2s var(--ease), transform .2s var(--ease), visibility 0s .2s;
}
.menu-panel.is-open {
  visibility: visible; opacity: 1; pointer-events: auto;
  transform: none;
  transition: opacity .28s var(--ease), transform .34s var(--ease), visibility 0s 0s;
}
.menu-panel a {
  display: block; padding: 17px 0;
  border-bottom: 1px solid var(--hairline-dark);
  color: #fff; text-decoration: none;
  font-family: var(--display); font-weight: 800; font-size: 1.6rem;
  text-transform: uppercase; letter-spacing: .01em;
  opacity: 0; transform: translateY(-10px);
  transition: opacity .2s var(--ease), transform .2s var(--ease);
}
.menu-panel.is-open a { opacity: 1; transform: none; }
.menu-panel.is-open a:nth-child(1) { transition-delay: .04s; }
.menu-panel.is-open a:nth-child(2) { transition-delay: .07s; }
.menu-panel.is-open a:nth-child(3) { transition-delay: .10s; }
.menu-panel.is-open a:nth-child(4) { transition-delay: .13s; }
.menu-panel.is-open a:nth-child(5) { transition-delay: .16s; }
.menu-panel.is-open a:nth-child(6) { transition-delay: .19s; }
.menu-panel.is-open a:nth-child(7) { transition-delay: .22s; }
.menu-panel .btn { margin-top: var(--lg); width: 100%; }

/* ==========================================================================
   Hero — the cinematic storm. Media is confined to this section.
   ========================================================================== */

/* --dawn runs 0 -> 1 as the visitor scrolls the first viewport: the storm
   passes. JS is the single driver (see site.js); with JS off it stays 0 and
   the hero is simply the storm, which is a complete state on its own. */
.hero {
  --dawn: 0;
  position: relative;
  isolation: isolate;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex; align-items: center;
  padding-top: calc(var(--header-h) + var(--brand-drop) + clamp(24px, 5vh, 62px));
  padding-bottom: clamp(48px, 9vh, 110px);
  background: var(--navy-950);
  overflow: hidden;
}
.hero__video, .hero__rain, .hero__dawn, .hero__scrim, .hero__light {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none;
}
/* The rain canvas paints the video itself, so it covers this element whenever
   the effect runs; the filter here is what the no-WebGL fallback sees, and
   site.js applies the identical grade to the canvas source. */
.hero__video {
  object-fit: cover; z-index: var(--z-video);
  filter: brightness(calc(.9 + var(--dawn) * .34)) saturate(calc(.86 + var(--dawn) * .30));
}
.hero__rain { z-index: var(--z-rain); }

/* The sky clearing. Screen-blended so it reads as light arriving rather than
   a wash laid on top, weighted to the upper right and away from the copy. */
.hero__dawn {
  z-index: var(--z-dawn);
  opacity: var(--dawn);
  mix-blend-mode: screen;
  background:
    radial-gradient(118% 54% at 74% 92%, rgba(255, 186, 128, .30) 0%, rgba(255, 186, 128, 0) 64%),
    linear-gradient(198deg, rgba(171, 203, 240, .46) 0%, rgba(171, 203, 240, .13) 44%, rgba(171, 203, 240, 0) 72%);
}

/* Fades only slightly — the headline's contrast must survive the clearing. */
.hero__scrim {
  z-index: var(--z-scrim);
  opacity: calc(1 - var(--dawn) * .28);
  background:
    linear-gradient(94deg, rgba(4, 11, 26, .92) 0%, rgba(4, 11, 26, .72) 38%, rgba(4, 11, 26, .26) 72%, rgba(4, 11, 26, .48) 100%),
    linear-gradient(0deg,  rgba(4, 11, 26, .80) 0%, rgba(4, 11, 26, .10) 46%, rgba(4, 11, 26, .40) 100%);
}

/* Lightning belongs to the storm; it goes out as the storm does. */
.hero__light {
  z-index: var(--z-light);
  opacity: calc(var(--flash) * .26 * (1 - var(--dawn)));
  background: radial-gradient(122% 82% at 68% 8%, rgba(205, 224, 255, .92), rgba(205, 224, 255, 0) 62%);
}

.hero .shell { position: relative; z-index: var(--z-hero); }

/* The copy rises and recedes as the storm lifts — it has already been read by
   the time it dims, and the header CTA and mobile call bar both stay solid. */
.hero__copy {
  max-width: 780px; color: #fff;
  transform: translateY(calc(var(--dawn) * -44px));
  opacity: calc(1 - var(--dawn) * .72);
}
.hero__eyebrow {
  display: inline-flex; align-items: center; gap: 12px;
  font-family: var(--display); font-weight: 700; font-size: .88rem;
  letter-spacing: .22em; text-transform: uppercase; color: #fff;
  margin-bottom: var(--md);
}
.hero__eyebrow i { display: block; width: 34px; height: 3px; background: var(--red-500); flex: none; }

.hero__h1 { color: #fff; text-shadow: 0 6px 34px rgba(0, 0, 0, .55); }
.hero__h1 em { font-style: normal; color: var(--red-400); }

.hero__lede {
  margin-top: var(--md);
  font-size: clamp(1.05rem, 1.5vw, 1.22rem);
  color: #dbe4f1;
  max-width: 54ch;
  text-shadow: 0 2px 18px rgba(0, 0, 0, .6);
}
.hero__actions { margin-top: var(--lg); }

/* Three plain facts, ruled — no badges, no icons, nothing unearned. */
.hero__facts {
  margin-top: clamp(32px, 5vh, 56px);
  display: flex; flex-wrap: wrap; gap: var(--md) 0;
  border-top: 1px solid rgba(255, 255, 255, .22);
  padding-top: var(--md);
}
.hero__fact { padding-right: clamp(20px, 3vw, 44px); margin-right: clamp(20px, 3vw, 44px); border-right: 1px solid rgba(255, 255, 255, .22); }
.hero__fact:last-child { border-right: none; margin-right: 0; padding-right: 0; }
.hero__fact dt {
  font-family: var(--display); font-weight: 700; font-size: .76rem;
  letter-spacing: .2em; text-transform: uppercase; color: var(--steel);
  margin-bottom: 4px;
}
.hero__fact dd { font-family: var(--display); font-weight: 800; font-size: 1.32rem; text-transform: uppercase; color: #fff; }

.hero__scroll {
  position: absolute; left: 50%; bottom: 22px; transform: translateX(-50%);
  z-index: var(--z-hero);
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  font-family: var(--display); font-weight: 700; font-size: .72rem;
  letter-spacing: .2em; text-transform: uppercase; color: rgba(255, 255, 255, .6);
  text-decoration: none;
  opacity: calc(1 - var(--dawn) * 3);
}
.hero__scroll svg { width: 15px; height: 15px; animation: nudge 2.2s ease-in-out infinite; }
.hero__scroll:hover { color: #fff; }
/* The cue has done its job the moment the visitor starts scrolling; the loop
   stops rather than running invisibly behind a faded element. */
.hero.is-cleared .hero__scroll svg { animation-play-state: paused; }
@keyframes nudge { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(5px); } }

/* ==========================================================================
   Tagline marquee — the seam between the storm and the page
   ========================================================================== */

.marquee {
  background: var(--navy-950);
  border-top: 1px solid var(--hairline-dark);
  border-bottom: 1px solid var(--hairline-dark);
  overflow: hidden;
  padding-block: clamp(14px, 1.7vw, 24px);
}
.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee 32s linear infinite;
}
/* Let people stop it to read it. */
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track { animation-play-state: paused; }

.marquee__seq {
  display: flex; align-items: center;
  gap: clamp(24px, 3.2vw, 56px);
  padding-right: clamp(24px, 3.2vw, 56px);
}
.marquee__word {
  font-family: var(--display); font-weight: 900;
  font-size: clamp(1.5rem, 3.3vw, 2.7rem);
  line-height: 1; letter-spacing: .005em;
  text-transform: uppercase; white-space: nowrap;
  color: #fff;
}
/* Every other word is outlined, so the strip has rhythm instead of being a
   wall of solid caps. */
.marquee__word--ghost {
  /* Filled, not transparent, by default. An engine without -webkit-text-stroke
     would otherwise render this word invisible — the one place on the page where
     an unsupported property could delete copy rather than just flatten it. */
  color: var(--steel-2);
}
@supports (-webkit-text-stroke: 1px currentColor) {
  .marquee__word--ghost {
    color: transparent;
    -webkit-text-stroke: 1.4px var(--steel-2);
  }
}
.marquee__dot {
  width: 10px; height: 10px; flex: none;
  background: var(--red-500);
  transform: rotate(45deg);
}
@keyframes marquee { to { transform: translateX(-50%); } }

@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none !important; }
}

/* ==========================================================================
   Service cards
   ========================================================================== */

/* ==========================================================================
   Services index

   Not a card grid. Six ruled rows set at near-heading scale, read as an index
   rather than a menu of tiles. The reveal is pure CSS on :hover / :focus-within,
   so it has no JavaScript to fail: on touch every row is simply open.
   ========================================================================== */

.svc-index { border-top: 1px solid var(--hairline); }

.svc-row {
  position: relative;
  isolation: isolate;
  display: grid;
  grid-template-columns: 46px 1fr auto;
  grid-template-areas:
    "no name go"
    ".  desc desc";
  gap: 8px clamp(14px, 2vw, 28px);
  align-items: center;
  padding: clamp(20px, 2.4vw, 30px) clamp(10px, 1.6vw, 22px);
  border-bottom: 1px solid var(--hairline);
  text-decoration: none;
  color: var(--ink);
}

.svc-row__no {
  grid-area: no;
  font-family: var(--display); font-weight: 800; font-size: .86rem;
  letter-spacing: .2em; color: var(--ink-2);
  align-self: center;
}
.svc-row__name {
  grid-area: name;
  font-family: var(--display); font-weight: 900;
  font-size: clamp(1.7rem, 3.1vw, 2.8rem);
  line-height: .98; letter-spacing: -.012em; text-transform: uppercase;
}
.svc-row__desc {
  grid-area: desc;
  color: var(--ink-2); font-size: .97rem; line-height: 1.5; max-width: 62ch;
}
.svc-row__icon { display: none; color: var(--red-500); }
.svc-row__go {
  grid-area: go;
  width: 26px; height: 26px; color: var(--red-600);
  justify-self: end; align-self: center;
}
.svc-row:active { background: var(--bone); }

/* Pointer devices get the index behaviour: the row sweeps navy, the name steps
   aside, and its description and diagram arrive in the space that was already
   reserved for them. Nothing animates a layout property. */
@media (hover: hover) and (pointer: fine) {
  .svc-row {
    grid-template-columns: 56px minmax(0, 1.05fr) minmax(0, 1fr) 56px 34px;
    grid-template-areas: "no name desc icon go";
    align-items: center;
    min-height: 118px;
    transition: color .28s var(--ease);
  }
  .svc-row::before {
    content: "";
    position: absolute; inset: 0; z-index: -1;
    /* A gradient, not a flat wash: fully solid navy under the name and number
       on the left (white type clears 4.5:1 there), clearing to a light tint on
       the right so the photograph actually reads instead of being crushed to
       black. A row with no photo just gets solid navy the whole way. */
    background: linear-gradient(90deg,
      var(--navy-900) 0%, var(--navy-900) 34%,
      rgba(6, 20, 40, .82) 52%, rgba(6, 20, 40, .34) 100%);
    transform: scaleX(0); transform-origin: left center;
    transition: transform .42s var(--ease);
  }
  /* Rows without a photograph keep the ledger flat — solid navy edge to edge. */
  .svc-row:not(.svc-row--shot)::before { background: var(--navy-900); }

  /* Photo fills the row behind the navy, not a thumbnail. Declared as a background
     INSIDE this media query on purpose: a background only referenced by a non-matching
     rule is never fetched, so phones download none of it.
     Paths live here, NOT in a custom property. url() in a custom property resolves
     against the consuming stylesheet in Chrome and the declaring one elsewhere, so an
     inline --shot silently became assets/css/assets/media/... */
  /* Present at rest, not just hover — hidden photo is never seen; band read as
     type-only. Mask keeps it right-half where no type sits, eases down at far right so
     the red arrow has clean ground. Mask constant, only opacity animates (mask-image
     not interpolable). Black in the gradient is an alpha stencil, not a palette colour;
     detector flags both stops as undocumented colours = false positive. */
  .svc-row__media {
    position: absolute; inset: 0; z-index: -2;
    background-size: cover;
    background-position: center 62%;
    opacity: .72;
    transform: scale(1.06);
    -webkit-mask-image: linear-gradient(90deg, transparent 46%, #000 76%, rgba(0, 0, 0, .5) 100%);
    mask-image: linear-gradient(90deg, transparent 46%, #000 76%, rgba(0, 0, 0, .5) 100%);
    transition: opacity .5s var(--ease), transform .7s var(--ease);
  }
  /* WebP first, JPEG behind it — 1.1MB of photography down to 332KB, which on a
     phone-tethered connection after a storm is the difference between the band
     arriving with the page and arriving after it. Declared as a plain url() and
     then overridden by image-set(), the same progressive-upgrade shape used for
     overflow-x above: a browser that doesn't parse image-set() keeps the JPEG
     rather than losing the photograph. Only one file per row is ever fetched. */
  .svc-row--roofing     .svc-row__media {
    background-image: url("../media/services/roofing.jpg");
    background-image: image-set(url("../media/services/roofing.webp") type("image/webp"),
                                url("../media/services/roofing.jpg")  type("image/jpeg"));
  }
  .svc-row--repair      .svc-row__media {
    background-image: url("../media/services/roof-repair.jpg");
    background-image: image-set(url("../media/services/roof-repair.webp") type("image/webp"),
                                url("../media/services/roof-repair.jpg")  type("image/jpeg"));
  }
  .svc-row--storm       .svc-row__media {
    background-image: url("../media/services/storm-damage.jpg");
    background-image: image-set(url("../media/services/storm-damage.webp") type("image/webp"),
                                url("../media/services/storm-damage.jpg")  type("image/jpeg"));
  }
  .svc-row--siding      .svc-row__media {
    background-image: url("../media/services/siding.jpg");
    background-image: image-set(url("../media/services/siding.webp") type("image/webp"),
                                url("../media/services/siding.jpg")  type("image/jpeg"));
  }
  .svc-row--gutters     .svc-row__media {
    background-image: url("../media/services/gutters.jpg");
    background-image: image-set(url("../media/services/gutters.webp") type("image/webp"),
                                url("../media/services/gutters.jpg")  type("image/jpeg"));
  }
  .svc-row--restoration .svc-row__media {
    background-image: url("../media/services/restoration.jpg");
    background-image: image-set(url("../media/services/restoration.webp") type("image/webp"),
                                url("../media/services/restoration.jpg")  type("image/jpeg"));
  }
  .svc-row:hover .svc-row__media,
  .svc-row:focus-visible .svc-row__media { opacity: 1; transform: none; }
  .svc-row__name { transition: transform .42s var(--ease); }
  .svc-row__no,
  .svc-row__go { transition: color .28s var(--ease), transform .34s var(--ease); }
  .svc-row__desc,
  .svc-row__icon {
    opacity: 0;
    transform: translateX(-12px);
    transition: opacity .34s var(--ease) .05s, transform .34s var(--ease) .05s, color .28s var(--ease);
  }
  .svc-row__icon { grid-area: icon; display: block; width: 52px; height: 52px; justify-self: center; stroke-width: 2.1; }

  .svc-row:hover::before,
  .svc-row:focus-visible::before { transform: scaleX(1); }
  .svc-row:hover,
  .svc-row:focus-visible { color: #fff; }
  .svc-row:hover .svc-row__name,
  .svc-row:focus-visible .svc-row__name { transform: translateX(10px); }
  .svc-row:hover .svc-row__no,
  .svc-row:focus-visible .svc-row__no { color: var(--red-400); }
  .svc-row:hover .svc-row__desc,
  .svc-row:focus-visible .svc-row__desc { opacity: 1; transform: none; color: var(--steel); }
  .svc-row:hover .svc-row__icon,
  .svc-row:focus-visible .svc-row__icon { opacity: 1; transform: none; color: var(--red-400); }
  .svc-row:hover .svc-row__go,
  .svc-row:focus-visible .svc-row__go { color: #fff; transform: translateX(8px); }

  /* The rest of the index recedes while one row is being read. Without :has()
     support nothing dims and the section is still completely fine. */
  .svc-index:has(.svc-row:hover) .svc-row:not(:hover),
  .svc-index:has(.svc-row:focus-visible) .svc-row:not(:focus-visible) {
    opacity: .42;
  }
  .svc-row { transition: color .28s var(--ease), opacity .28s var(--ease); }
}

@media (prefers-reduced-motion: reduce) {
  .svc-row::before { transition: none; }
  .svc-row__name, .svc-row__desc, .svc-row__icon, .svc-row__go { transform: none !important; }
}

/* ==========================================================================
   Storm damage plates (navy band)
   ========================================================================== */

.plates { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--lg) var(--xl); }
.plate { display: grid; grid-template-columns: 104px 1fr; gap: var(--md); align-items: start; }
.plate__fig { width: 104px; }
.plate__name { color: #fff; margin-bottom: 8px; }
.plate__desc { color: var(--steel); font-size: .99rem; }
.plate__sev {
  display: inline-flex; align-items: center; gap: 8px; margin-top: 11px;
  font-family: var(--display); font-weight: 700; font-size: .76rem;
  letter-spacing: .18em; text-transform: uppercase; color: var(--red-400);
}
.plate__sev i { display: block; width: 9px; height: 9px; background: var(--red-500); flex: none; }
.plate__sev--watch { color: var(--steel); }
.plate__sev--watch i { background: var(--steel-2); }

.diagram .ln { fill: none; stroke-width: 1.75; stroke-linecap: square; stroke-linejoin: miter; }
.diagram .ln--steel { stroke: var(--steel); }
.diagram .ln--dim   { stroke: var(--steel-2); }
.diagram .ln--mark  { stroke: var(--red-400); stroke-width: 2.25; }
.diagram .ln--dash  { stroke-dasharray: 4 4; }
.diagram .fill--red { fill: var(--red-500); }
.diagram .fill--steel { fill: var(--steel-2); }

/* ==========================================================================
   The inspection — what we check when we go up

   Deliberately static: the section it replaced made the reader hunt pins on a
   photograph to read one finding at a time. Everything here is legible at once.
   The mark is a 9px red square, and dd is indented by mark + gap so the prose
   hangs off the same optical edge as the area name.
   ========================================================================== */

.insp {
  display: grid;
  grid-template-columns: .8fr 1.45fr;
  column-gap: clamp(32px, 5vw, 80px);
  row-gap: var(--lg);
}
/* The rail is shorter than the list, so the link drops to the foot of the
   column and the band composes on four corners instead of trailing off into
   half a screen of empty navy. Stacked, it sits under the deck as normal. */
.insp__intro { display: flex; flex-direction: column; }
/* The heading is the left rail here, not a full-width header, so it loses the
   standard section-head gap and the link carries the space instead. */
.insp .sec-head { margin-bottom: var(--md); }

/* Ringo comes up over the ridge in the band's bottom-right corner, flush with
   both edges. The band is full-bleed, so right: 0 is the screen edge. He sits
   behind the copy (the grid is lifted a layer) and space is reserved for him on
   narrow screens so he can never land on the closing line. */
/* The band grows a foot so Ringo clears the checklist's last hairline instead of
   having a rule run through his mane. */
.insp-band { position: relative; padding-bottom: calc(var(--band) + 130px); }
.insp-band .insp { position: relative; z-index: 1; }
.insp__ringo {
  position: absolute; z-index: 0;
  right: 0; bottom: 0;
  width: clamp(240px, 26vw, 400px); height: auto;
  pointer-events: none;
}

.insp__link {
  display: inline-flex; align-items: center; gap: 9px;
  /* The rail is a flex column, which would stretch the link and turn its red
     underline into a full-width divider. */
  align-self: flex-start;
  margin-top: var(--lg);
  padding-bottom: 3px;
  color: #fff;
  /* Title step — the ramp's size for buttons and card titles. */
  font-family: var(--display); font-weight: 800; font-size: 1.42rem;
  letter-spacing: .002em; text-transform: uppercase; text-decoration: none;
  border-bottom: 2px solid var(--red-500);
  transition: color .18s ease, border-color .18s ease;
}
.insp__link:hover { color: var(--red-400); border-bottom-color: var(--red-400); }
.insp__link svg { width: .85em; height: .85em; flex: none; }
/* Must follow the rule above: a media query adds no specificity, so placed
   earlier it would lose the margin-top to source order. */
/* Wide: the plate hugs the foot of the rail and its bottom edge lands exactly on
   the closing line's rule, so it stands on a line instead of hovering above one. */
@media (min-width: 981px) {
  .insp__link { margin-top: auto; }
}

.insp__item {
  display: grid; gap: 5px;
  padding: 16px 0;
  border-bottom: 1px solid var(--hairline-dark);
}
.insp__item:first-child { padding-top: 0; }
.insp__item:last-child { border-bottom: none; padding-bottom: 0; }
/* dt carries .title in the markup, so the type comes from the ramp rather than
   from a size invented here. This rule only lays the mark out beside it. */
.insp__item dt {
  display: flex; align-items: center; gap: 12px;
}
.insp__item dt::before {
  content: "";
  flex: none; width: 9px; height: 9px;
  background: var(--red-500);
}
/* 21px = the 9px mark plus its 12px gap, so the prose hangs off the same
   optical edge as the area name. Body size is inherited, not restated. */
.insp__item dd {
  padding-left: 21px;
  color: var(--steel);
}

.insp__close {
  grid-column: 1 / -1;
  padding-top: var(--md);
  border-top: 1px solid var(--hairline-dark);
  max-width: 62ch;
  color: var(--steel);
}
.insp__close strong { color: #fff; font-weight: 600; }

/* ===== Band shot — photograph that terminates a band =====
   Full-bleed, flush with the band's bottom edge. A photo here is flush to a section
   edge or it is not on the page: one floating mid-band reads as pasted on (what the
   mascot plate did pre-rework). Outside .shell, so width = the section's. No 100vw
   trickery, no scrollbar. */

.band-shot {
  display: block;
  width: 100%;
  height: clamp(200px, 24vw, 340px);
  margin-top: var(--band);
  /* cancels the band's bottom padding so the image lands on its edge */
  margin-bottom: calc(var(--band) * -1);
  object-fit: cover;
  object-position: center 58%;
}

/* ==========================================================================
   Process
   ========================================================================== */

.process { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; }
.step { padding: 0 var(--md); border-left: 1px solid var(--hairline); }
.step:first-child { padding-left: 0; border-left: none; }
.step:last-child { padding-right: 0; }
.band--navy .step, .band--navy-deep .step { border-left-color: var(--hairline-dark); }
/* Bone 2 filled on Bone = 1.11:1 — an absence, not a ghost; band read as four
   unlabelled columns. Bone 2 is for fills/placeholders, never text; Two Grounds Rule
   allows only Ink and Ink 2 on light ground.
   Outlined, not refilled: solid Ink 2 at 3.2rem/900 would outweigh the step name.
   Uses the marquee's ghost device (transparent + text-stroke). Ink 2 stroke 5.76:1 on
   Bone, Steel 2 4.2:1 on navy. Filled first for engines without text-stroke. */
.step__no {
  font-family: var(--display); font-weight: 900; font-size: 3.2rem; line-height: 1;
  color: var(--ink-2); margin-bottom: 10px;
}
.band--navy .step__no, .band--navy-deep .step__no { color: var(--steel-2); }

@supports (-webkit-text-stroke: 1px currentColor) {
  .step__no {
    color: transparent;
    -webkit-text-stroke: 1.6px var(--ink-2);
  }
  .band--navy .step__no, .band--navy-deep .step__no {
    color: transparent;
    -webkit-text-stroke: 1.6px var(--steel-2);
  }
}
.step__name { margin-bottom: 9px; }
.step__desc { color: var(--ink-2); font-size: .99rem; }
.band--navy .step__desc, .band--navy-deep .step__desc { color: var(--steel); }

/* ==========================================================================
   About
   ========================================================================== */

.about { display: grid; grid-template-columns: 1.15fr .85fr; gap: clamp(32px, 5vw, 80px); align-items: center; }
.about__panel {
  background: var(--navy-900); color: var(--steel);
  border-radius: 2px; padding: clamp(28px, 3.4vw, 44px);
}
.about__panel h3 { color: #fff; margin-bottom: var(--md); }
.about__list li {
  display: flex; gap: 14px; padding: 15px 0;
  border-bottom: 1px solid var(--hairline-dark);
}
.about__list li:last-child { border-bottom: none; padding-bottom: 0; }
.about__list svg { width: 21px; height: 21px; flex: none; margin-top: 3px; color: var(--red-400); }
.about__list strong { display: block; color: #fff; font-family: var(--display); font-weight: 800; font-size: 1.14rem; text-transform: uppercase; letter-spacing: .01em; }
.about__list span { font-size: .97rem; }

/* --- Company truck ---
   Only photo unambiguously Ringo's own → set piece, not a column slot. Transparent
   cut-out, which is what allows flush placement: no rectangle, box or shadow. Wheels
   on the band's bottom edge; deliberately wider than the viewport so the tailgate is
   cut by the screen, not framed by it.
   Foot reserved from the truck's own aspect (2854/1083 = 2.635) so copy never
   collides at any width. */
/* Every figure below is derived from the file's alpha channel, measured rather
   than guessed (2876 × 1025): the art occupies 32.337% of the width in height,
   sits 2.469% of the width above the canvas bottom, and 1.113% in from the right.
   Re-measure if the asset is ever replaced — the last version had different
   padding and the truck floated. */
.about-band {
  position: relative;
  /* the truck is wider than the viewport on purpose — contain it here, at the
     source of the bleed, rather than relying on a page-level guard */
  overflow-x: clip;
  /* Sized so the whole vehicle reads inside the band. At 108vw it was cut by the
     left edge and stood ~450px tall, which put more weight on the truck than on
     the argument the section is making. The rear stays flush to the right screen
     edge, so one edge is still engaged and the phone number sits at the margin.
     Below 620px the floor exceeds the viewport and it is cut at the left again,
     deliberately: at phone widths a whole truck is too small to read, and the bed
     with the phone number is the part worth keeping in frame. */
  --truck-w: clamp(620px, 76vw, 1180px);
  --truck-art-h: calc(var(--truck-w) * .32337);
  --truck-pad-b: calc(var(--truck-w) * .02469);
  /* A plinth, not a band. At 170px it read as an arbitrary colour bar; at ~86px
     it reads as the ground the truck is standing on. */
  --ground-h: clamp(56px, 6vw, 96px);
  /* Room for the ground, the truck standing on it, and air above. --xl rather than
     --band: a full band of padding on top of the truck's own height made the
     section's footprint larger than its content warranted. */
  padding-bottom: calc(var(--ground-h) + var(--truck-art-h) + var(--xl));
}
.about-band .about { position: relative; z-index: 2; }

/* The one red surface on the page. Red means act everywhere else, so this is
   deliberately the single exception: it is the ground, not a call to action, and
   it exists because the truck needed something to stand on. */
.about__ground {
  position: absolute; z-index: 0;
  left: 0; right: 0; bottom: 0;
  height: var(--ground-h);
  background: var(--red-500);
}

.about__truck {
  position: absolute; z-index: 1;
  /* tyres land exactly on the red's top edge */
  bottom: calc(var(--ground-h) - var(--truck-pad-b));
  /* exactly the art's own right padding, so the tailgate sits flush to the screen
     edge rather than being cut by it */
  right: calc(var(--truck-w) * -.01113);
  display: block;
  width: var(--truck-w);
  pointer-events: none;
}
.about__truck img { display: block; width: 100%; height: auto; }

/* Drives in along plinth, right→left = nose direction. Other way reads as reversing.
   Tyres stay on plinth top edge. Translate, not clip-wipe. Scroll-scrubbed, NO
   transition — scroll IS the timing. Page's only reversible motion: deliberate second
   grammar, not drift. No [data-rise] — would fight the slide. Opacity full by 1/3
   travel. .js-scoped, so dead script leaves truck parked. */
.js .about__truck {
  transform: translateX(calc((1 - var(--truck-p, 0)) * 68%));
  opacity: calc(var(--truck-p, 0) * 3);
  will-change: transform, opacity;
}

/* ==========================================================================
   Gallery
   ========================================================================== */

/* Editorial grid — varied tile sizes, not four equal squares. */
.gallery {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: clamp(150px, 15vw, 208px);
  gap: var(--sm);
}
.gallery > *:nth-child(1) { grid-column: span 2; grid-row: span 2; }
.gallery > *:nth-child(4) { grid-column: span 2; }

.tile {
  position: relative;
  border-radius: 2px; overflow: hidden;
  background: var(--navy-800);
  display: flex; align-items: center; justify-content: center;
  padding: 0; border: 0;
}
a.tile, button.tile { cursor: pointer; }
a.tile:hover img, button.tile:hover img { transform: scale(1.04); }
.tile img { transition: transform .4s var(--ease); }
/* transform, not padding — padding is a layout property and animating it
   forces reflow on every frame of the hover. */
a.tile:hover .tile__cap, button.tile:hover .tile__cap { transform: translateY(-6px); }
.tile__cap { transition: transform .22s var(--ease); }
.tile img { width: 100%; height: 100%; object-fit: cover; }
.tile__cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 40px 20px 16px;
  background: linear-gradient(0deg, rgba(6, 20, 40, .92), rgba(6, 20, 40, 0));
  color: #fff;
  font-family: var(--display); font-weight: 800; font-size: 1.08rem;
  text-transform: uppercase; letter-spacing: .02em;
}

/* Honest placeholder: no stock photo, no fake before/after. */
.tile--empty {
  background: repeating-linear-gradient(135deg, var(--navy-800) 0 12px, #0d2549 12px 24px);
  border: 1px solid var(--hairline-dark);
  flex-direction: column; gap: 10px; text-align: center; padding: var(--md);
}
.tile--empty svg { width: 34px; height: 34px; color: var(--steel-2); stroke-width: 1.9; }
.tile--empty span {
  font-family: var(--display); font-weight: 700; font-size: .76rem;
  letter-spacing: .18em; text-transform: uppercase; color: var(--steel-2);
}

/* --- Lightbox (activates once real job photos are added) --- */
.lightbox {
  position: fixed; inset: 0; z-index: 120;
  display: none;
  align-items: center; justify-content: center;
  gap: clamp(8px, 2vw, 24px);
  padding: clamp(16px, 4vw, 48px);
  background: rgba(4, 11, 26, .94);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
}
.lightbox.is-open { display: flex; }
.lightbox__figure { max-width: min(1100px, 100%); max-height: 100%; margin: 0; }
.lightbox__img { max-height: 78vh; width: auto; border-radius: 2px; }
.lightbox__cap {
  margin-top: var(--sm); color: var(--steel);
  font-family: var(--display); font-weight: 700; font-size: 1rem;
  text-transform: uppercase; letter-spacing: .1em; text-align: center;
}
.lightbox__close {
  position: absolute; top: clamp(10px, 2vw, 22px); right: clamp(10px, 2vw, 22px);
  width: 48px; height: 48px;
  color: #fff; font-size: 2rem; line-height: 1;
  border: 1px solid var(--hairline-dark); border-radius: 2px;
}
.lightbox__nav {
  width: 52px; height: 52px; flex: none;
  color: #fff; font-size: 2rem; line-height: 1;
  border: 1px solid var(--hairline-dark); border-radius: 2px;
}
.lightbox__close:hover, .lightbox__nav:hover { background: var(--red-500); border-color: var(--red-500); }
.lightbox__count {
  position: absolute; bottom: clamp(10px, 2vw, 22px); left: 0; right: 0;
  text-align: center; color: var(--steel-2);
  font-family: var(--display); font-weight: 700; font-size: .82rem; letter-spacing: .2em;
}

@media (max-width: 620px) {
  .lightbox__nav { position: absolute; bottom: 52px; }
  .lightbox__nav--prev { left: 16px; }
  .lightbox__nav--next { right: 16px; }
}

/* ==========================================================================
   CTA band + contact form
   ========================================================================== */

.cta { display: grid; grid-template-columns: 1.05fr .95fr; gap: clamp(32px, 5vw, 72px); align-items: start; }
.cta__tel {
  display: inline-block; margin-top: var(--sm);
  font-family: var(--display); font-weight: 900;
  font-size: clamp(2.4rem, 5vw, 3.6rem); line-height: 1;
  letter-spacing: -.015em; color: #fff; text-decoration: none;
}
.cta__tel:hover { text-decoration: underline; text-underline-offset: 6px; }
.cta__note { margin-top: var(--md); color: rgba(255, 255, 255, .9); }

/* ===== Close, part 1 — the statement =====
   Bookend to the storm hero, past tense. Storm is over and the headline says so, so
   this band does NOT re-stage it: no rain, no cloud layer, no --clear. A section that
   argues with its own copy reads as decoration. Short on purpose — hands the visitor
   to the form. */

.say {
  /* Declared here, not on the figure, because the band's own height is derived
     from them: the section can never be shorter than the part of him that
     shows, or the bottom crop would start eating his head instead of his boots. */
  --ringo-w: clamp(228px, 29vw, 448px);
  --ringo-h: calc(var(--ringo-w) * 1.294118);   /* 792 / 612 */
  --ringo-vis: calc(var(--ringo-h) * .824);     /* what survives the bottom cut */

  position: relative;
  isolation: isolate;
  /* he is cut by both of this section's edges, so the section has to be the
     clipping box. clip, not hidden — hidden on one axis forces the other to
     auto and would make this a scroll container. */
  overflow: clip;
  display: grid; align-content: center;
  min-height: calc(var(--ringo-vis) + var(--lg));
  padding-block: clamp(48px, 5.2vw, 82px);
  color: #fff;
  /* The one warm note on the page's dark end: a low band of light sitting where
     the white work order begins, so the two sections meet on a horizon rather
     than on a seam. Its top is deliberately lighter than the gallery's
     navy-950 above it — two navy bands may only abut when the value break
     itself reads as the section change. */
  background:
    linear-gradient(180deg,
      rgba(10, 28, 54, 0) 50%,
      rgba(78, 54, 40, .5) 82%,
      rgba(146, 96, 60, .46) 100%),
    linear-gradient(180deg, #12294a 0%, #0b1d38 58%, #0a1a31 100%);
}

.say__sky { position: absolute; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }

/* A suggestion of the house behind him, never a photograph. Same downscaled
   file the old close used — 33KB, and at this grade a larger source would be
   pixels nobody can resolve. */
.say__roof {
  position: absolute; inset: 0;
  background: url("../media/roof-texture.jpg") center / cover no-repeat;
  background-image: image-set(url("../media/roof-texture.webp") type("image/webp"),
                              url("../media/roof-texture.jpg")  type("image/jpeg"));
  /* Atmosphere, not a subject. At .42 it read as a large grey gable parked in
     the middle of the band and competed with the type; blurred and dropped to
     .3 it is what it was always meant to be — the house he is standing on. */
  filter: brightness(.24) saturate(.4) contrast(1.06) blur(2px);
  opacity: .3;
  /* the blur would otherwise show a soft edge inside the section */
  transform: scale(1.03);
}
/* The break in the cloud, over the corner he stands in. */
.say__light {
  position: absolute; inset: 0;
  background:
    radial-gradient(44% 54% at 78% 74%, rgba(255, 218, 166, .34), rgba(255, 206, 148, 0) 70%),
    /* the horizon: the band's foot lit where the white work order begins, so
       the two sections meet on a line of light rather than on a seam */
    linear-gradient(180deg, rgba(255, 196, 132, 0) 62%, rgba(255, 188, 124, .17) 92%, rgba(255, 206, 150, .26) 100%);
  mix-blend-mode: screen;
}
/* Weighted to the left, where the type is: the copy keeps its contrast no
   matter how bright the corner gets. */
.say__scrim {
  position: absolute; inset: 0;
  background:
    linear-gradient(97deg, rgba(4, 12, 26, .74) 0%, rgba(4, 12, 26, .32) 44%, rgba(4, 12, 26, 0) 70%),
    radial-gradient(132% 122% at 50% 6%, rgba(4, 12, 26, 0) 32%, rgba(3, 9, 20, .48) 100%);
}

.say__in { position: relative; z-index: 1; }
/* px and a percentage, deliberately not ch. `ch` here resolves against the
   container's inherited body size, which capped this column at ~377px and left
   the headline sitting in a third of the band with a 650px void beside it. The
   measure belongs on the prose, where the unit means something. */
.say__copy { max-width: min(640px, 56%); }

.say__marks {
  display: flex; align-items: center; gap: 10px;
  margin-bottom: var(--md);
  color: var(--red-500);
}
.say__marks i { width: 52px; height: 3px; background: currentColor; }
.say__marks b { width: 9px; height: 9px; background: currentColor; transform: rotate(45deg); }

/* Headline scale, not Display scale. The One Shout Rule gives the page exactly
   one display line and the hero has it; this is the answer to it, not a second
   shout. */
.say__headline {
  font-size: clamp(2.1rem, 4vw, 3.4rem);
  line-height: .9;
  letter-spacing: -.014em;
}
/* One red-emphasised phrase inside the line, the same device the hero uses on
   "we start." — the two ends of the page marked the same way. red-400 because
   red-500 on this navy is 2.8:1 and would fail even the large-text floor. */
.say__headline em { font-style: normal; color: var(--red-400); }

.say__lead {
  margin-top: var(--md);
  max-width: 46ch;
  color: var(--steel);
  font-size: clamp(1rem, 1.3vw, 1.14rem);
  line-height: 1.6;
}

/* The other way out. The form is the primary path and it carries the page's one
   red button, so the phone is a text link here — the One Red Button Rule, and
   also the truth: an anxious homeowner reaching for a phone does not need a
   button to find it. */
.say__or {
  margin-top: var(--lg);
  color: var(--steel);
  font-size: .98rem;
}
.say__tel {
  display: inline-flex; align-items: center; gap: 9px;
  margin-left: var(--xs);
  color: #fff; text-decoration: none;
  font-family: var(--display); font-weight: 800;
  font-size: 1.44rem; letter-spacing: .01em;
  border-bottom: 2px solid var(--red-500);
  padding-bottom: 2px;
  transition: color .16s ease, border-color .16s ease;
}
.say__tel svg { width: 19px; height: 19px; color: var(--red-400); flex: none; }
.say__tel:hover { color: var(--red-400); border-color: var(--red-400); }

/* --- Ringo, flush bottom-right ---
   Measured off ringomascot.png alpha, not guessed: 612×792, art 11.765% of WIDTH in
   from right, 6.692% of HEIGHT above bottom. Cancel both → drawn figure on the edges,
   not its padding. RE-MEASURE IF ASSET REPLACED (old truck taught this).
   Extra 9% past bottom = deliberate cut through boots. Flush-Photograph Rule.
   No idle loop (clipped — boots would slide through crop). No rim-light: zero-offset
   shadow is decoration; only glow is the hero button. */
.say__ringo {
  position: absolute; z-index: 0;
  width: var(--ringo-w); height: var(--ringo-h);
  right: calc(var(--ringo-w) * -0.11765);   /* art flush with the right edge */
  bottom: calc(var(--ringo-h) * -0.17592);  /* 6.692% padding + 10.9% of cut */
  pointer-events: none;
}
.say__ringo img {
  display: block; width: 100%; height: auto;
  /* one cast shadow, thrown down and to the left, away from the corner light */
  filter: drop-shadow(-16px 14px 22px rgba(2, 7, 18, .52));
}

/* ===== Close, part 2 — request form =====
   Category canon straight: labelled fields, white card, Bone band. Earlier pass built
   a two-part work order (ruled rows, perforation, live carbon copy, tear-off); owner
   chose the conventional form. STAYS conventional — craft in details, not concept.
   Sized so card + button + fine print fit one 900px desktop viewport with the
   condensed header. Predecessor missed by 50px. */

.form-band {
  background: var(--bone);
  color: var(--ink);
  padding-block: clamp(36px, 3.6vw, 42px);
}

.form-card {
  position: relative;
  width: min(760px, 100%);
  margin-inline: auto;
  padding: clamp(26px, 2.8vw, 38px) clamp(24px, 3.4vw, 46px) clamp(22px, 2.4vw, 28px);
  background: var(--white);
  /* 2px, not the 8px this card used to carry. Nothing in this system rounds
     more than 2px. */
  border-radius: var(--edge, 2px);
  box-shadow: var(--card-rest);
}

.form-card__head { margin-bottom: clamp(14px, 1.6vw, 20px); }
.form-card__title {
  font-size: clamp(1.7rem, 2.8vw, 2.3rem);
  line-height: .98;
  color: var(--ink);
}
/* The One Marked Phrase Rule: "free" is the price, and it is the third and last
   marked phrase on the page after the hero's "we start." and the statement's
   "Let's look." Red Pressed, because red-500 on white is 4.0:1 for text. */
.form-card__title em { font-style: normal; color: var(--red-600); }
.form-card__head .sec-head__deck { margin-top: var(--sm); max-width: 44ch; }

.field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(14px, 1.6vw, 18px) clamp(16px, 2vw, 24px);
}
.field--wide { grid-column: 1 / -1; }

.field > label { display: block; margin-bottom: 6px; color: var(--ink); }
.field__opt { text-transform: none; letter-spacing: .02em; color: var(--ink-2); font-weight: 400; }
.field__control { position: relative; display: block; }

/* The documented input: white fill, 1px Hairline, 2px corners, 14px 16px, Body.
   No icon inside the box. An icon here cost 56px of left padding and 46px of
   right padding and rendered the phone placeholder as "Phone Nun" — and the
   label above the field already says what the field is. */
.field input, .field select, .field textarea {
  display: block; width: 100%;
  font-family: var(--body); font-size: 1rem; color: var(--ink);
  background: var(--white);
  border: 1px solid var(--hairline); border-radius: 2px;
  padding: 14px 16px;
  transition: border-color .16s ease, box-shadow .16s ease;
  -webkit-appearance: none; appearance: none;
}
.field input, .field select { padding-right: 42px; }
.field textarea { resize: vertical; min-height: 64px; padding-right: 16px; }
.field select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%2354617a' stroke-width='1.7'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 16px center; background-size: 12px 8px;
}
.field input::placeholder, .field textarea::placeholder { color: var(--ink-3); }

/* Border grows inward via the padding, so focus never reflows the field. */
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none;
  border-color: var(--red-500); border-width: 2px;
  padding: 13px 15px;
  box-shadow: 0 0 0 3px rgba(212, 33, 48, .2);
}
.field input:focus, .field select:focus { padding-right: 41px; }
.field textarea:focus { padding-right: 15px; }

/* Confirmed and logged. Navy, because red is reserved for problems — and a bare
   tick rather than a filled disc, which was the roundest object in a 2px-corner
   system and read as an app badge. The select has no tick: its chosen option is
   already the confirmation, and a tick would crowd the chevron. */
.field__ok {
  position: absolute; right: 13px; top: 50%; z-index: 2;
  display: grid; place-items: center;
  width: 20px; height: 20px;
  color: var(--navy-700);
  opacity: 0; transform: translateY(-50%) scale(.5);
  transition: opacity .18s ease, transform .34s var(--ease);
  pointer-events: none;
}
.field__ok svg { width: 18px; height: 18px; stroke-width: 2.4; }
.field.is-valid .field__ok { opacity: 1; transform: translateY(-50%); }
.field.is-valid input { border-color: var(--navy-700); }

/* Sent. Their own answers stay on screen and locked, because a "Received" seal
   over a form that has just been emptied is a stamp on nothing — the record is
   what they typed. Disabled fields keep full ink: a greyed-out value the visitor
   cannot read is not a receipt. */
.field input:disabled, .field select:disabled, .field textarea:disabled {
  background: var(--bone);
  color: var(--ink); -webkit-text-fill-color: var(--ink);
  border-color: var(--hairline);
  opacity: 1;
}
.btn:disabled { cursor: default; }
.btn.is-sent { background: var(--navy-800); box-shadow: none; transform: none; }

.field__err { display: none; margin-top: 7px; font-size: .88rem; font-weight: 500; color: var(--red-600); }
.field.is-invalid .field__err { display: block; }
.field.is-invalid input, .field.is-invalid select, .field.is-invalid textarea {
  border-color: var(--red-500); border-width: 2px; padding: 13px 15px;
}
.field.is-invalid input, .field.is-invalid select { padding-right: 41px; }

.form-card__submit {
  width: 100%;
  margin-top: clamp(18px, 2vw, 24px);
  min-height: 60px; gap: 20px;
  box-shadow: 0 12px 26px -8px rgba(212, 33, 48, .44);
}
.form-card__submit svg { width: 22px; height: 18px; }

.form-fine {
  margin-top: var(--sm);
  display: flex; align-items: center; justify-content: center; gap: 9px;
  font-size: .86rem; color: var(--ink-2); text-align: center;
}
.form-fine svg { width: 16px; height: 16px; flex: none; }

/* The seal. The one authored moment in this band, and it thuds down on a
   genuinely delivered request only — not on a validation pass, not on a dry run
   with no endpoint configured. Exponential ease-out, not an elastic curve: the
   compression is already in the keyframes (1.9 -> .92 -> 1), and an overshooting
   curve on top of an overshooting scale gave it a rubber-ball rebound. A stamp
   hits and stops. */
.form-card__stamp {
  position: absolute; z-index: 6;
  /* Low and right, over the optional-notes area. At the top of the card it
     printed straight across "REQUEST YOUR FREE INSPECTION" and took the heading
     with it; down here it lands on the part of the form most likely to be blank
     and never covers a value the visitor wants to check. */
  top: 62%; left: 61%;
  padding: 5px 20px;
  border: 4px solid var(--red-500); border-radius: 2px;
  color: var(--red-500);
  font-family: var(--display); font-weight: 900;
  font-size: clamp(1.6rem, 3vw, 2.3rem); line-height: 1;
  text-transform: uppercase; letter-spacing: .08em;
  opacity: 0; pointer-events: none;
  transform: translate(-50%, -50%) rotate(-11deg) scale(1.7);
  mix-blend-mode: multiply;
}
.form-card__stamp span { display: block; }
.form-card.is-sealed .form-card__stamp { animation: stampThud .5s var(--ease) forwards; }
@keyframes stampThud {
  0%   { opacity: 0; transform: translate(-50%, -50%) rotate(-11deg) scale(1.9); }
  55%  { opacity: 1; transform: translate(-50%, -50%) rotate(-11deg) scale(.92); }
  100% { opacity: 1; transform: translate(-50%, -50%) rotate(-11deg) scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .field__ok { transition: none; }
  .form-card.is-sealed .form-card__stamp {
    animation: none; opacity: 1;
    transform: translate(-50%, -50%) rotate(-11deg) scale(1);
  }
}

.form-status {
  --status-accent: var(--hairline);
  display: none;
  margin-top: var(--sm);
  font-size: .95rem; line-height: 1.5;
  padding: 13px 16px;
  background: var(--bone);
  /* A 1px border in the state's colour, not a 3px coloured stripe along one
     edge: that stripe is the most recognisable tell in the category, and the 3px
     weight belongs to the section rule. The message text is the signal that does
     not depend on colour — it names what happened either way. */
  border: 1px solid var(--status-accent);
}
.form-status.is-shown { display: block; animation: statusIn .3s var(--ease) both; }
@keyframes statusIn {
  from { opacity: 0; transform: translateY(-7px); }
  to   { opacity: 1; transform: none; }
}
.form-status.is-ok  { --status-accent: var(--navy-700); }
.form-status.is-bad { --status-accent: var(--red-500); color: var(--red-600); }
.form-status:focus-visible { outline: 3px solid var(--red-400); outline-offset: 3px; }

/* Submitting is the one action on this page that costs the visitor something to
   get wrong, so it acknowledges itself. A determinate bar would be a lie — we
   do not know how long the POST takes — so this is honestly indeterminate. */
.btn.is-sending { position: relative; overflow: hidden; pointer-events: none; }
.btn.is-sending::after {
  content: "";
  position: absolute; left: 0; bottom: 0; height: 3px; width: 38%;
  background: rgba(255, 255, 255, .9);
  animation: sending 1.05s cubic-bezier(.5, 0, .5, 1) infinite;
}
@keyframes sending {
  from { transform: translateX(-110%); }
  to   { transform: translateX(320%); }
}

/* ===== Subpage components =====
   Homepage sets the system; subpages inherit. Below = only what they need extra: a
   non-video hero, breadcrumb, long-form copy, plain Q&A, way out to sibling services.
   Storm footage + WebGL rain stay on the homepage's first viewport — Media
   Confinement, not omission. Subpage hero earns atmosphere from one scrimmed photo. */

/* --- Page hero ------------------------------------------------------------ */
.page-hero {
  position: relative;
  isolation: isolate;
  overflow: clip;
  padding-block: calc(var(--header-h) + var(--brand-drop) + clamp(30px, 4vw, 58px))
                 clamp(44px, 5vw, 76px);
  color: #fff;
  background: var(--navy-950);
}
/* The service photograph, scrimmed hard. Illustration under the
   Illustration-Not-Evidence Rule: it sets a subject, it never claims to be
   Ringo's own work, and it is never captioned as such. */
.page-hero__shot {
  position: absolute; inset: 0; z-index: -2;
  background-position: center 38%; background-size: cover; background-repeat: no-repeat;
  filter: saturate(.72) contrast(1.04);
}
.page-hero__scrim {
  position: absolute; inset: 0; z-index: -1;
  background:
    linear-gradient(96deg, rgba(4, 12, 26, .95) 0%, rgba(5, 14, 30, .82) 44%, rgba(6, 18, 38, .58) 100%),
    linear-gradient(180deg, rgba(4, 11, 24, .72) 0%, rgba(4, 11, 24, .32) 40%, rgba(5, 14, 30, .86) 100%);
}
/* Wide enough that the two hero actions sit on one line at desktop. At 60ch the
   column was ~490px, the pair needs ~600, and they wrapped into a stack. The
   prose measure lives on the lede, where the unit means something. */
.page-hero__in { position: relative; z-index: 1; max-width: min(690px, 62%); }
.page-hero .display {
  margin-top: var(--sm);
  font-family: var(--display); font-weight: 900;
  font-size: clamp(2.6rem, 5.6vw, 4.6rem); line-height: .88;
  letter-spacing: -.018em; text-transform: uppercase;
}
/* One marked phrase per page, the same device the homepage uses. */
.page-hero .display em { font-style: normal; color: var(--red-400); }
.page-hero__lede {
  margin-top: var(--md);
  max-width: 54ch;
  color: var(--steel);
  font-size: clamp(1.04rem, 1.4vw, 1.2rem); line-height: 1.6;
}
.page-hero .actions { margin-top: var(--lg); }

/* --- Breadcrumb ----------------------------------------------------------- */
.crumbs {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0 10px;
  font-family: var(--display); font-weight: 700; font-size: .8rem;
  text-transform: uppercase; letter-spacing: .16em;
  color: var(--steel);
}
.crumbs ol { display: flex; flex-wrap: wrap; align-items: center; gap: 0 10px; list-style: none; }
.crumbs a { color: var(--steel); text-decoration: none; transition: color .16s ease; }
.crumbs a:hover { color: #fff; }
.crumbs li[aria-current] { color: #fff; }
/* A red diamond, the marquee's separator — not a slash and not a chevron glyph
   in the body face, which would be a third shape language. */
.crumbs li + li::before {
  content: ""; display: inline-block; vertical-align: middle;
  width: 6px; height: 6px; margin-right: 10px;
  background: var(--red-500); transform: rotate(45deg);
}

/* --- Editorial body: a heading rail on the left, prose on the right -------- */
.editorial {
  display: grid;
  grid-template-columns: minmax(0, .34fr) minmax(0, 1fr);
  gap: clamp(26px, 4vw, 64px);
}
.editorial__rail .sec-head { margin-bottom: 0; }
.editorial__body > * + * { margin-top: var(--md); }
.editorial__body h3 {
  margin-top: var(--lg);
  font-family: var(--display); font-weight: 800; font-size: 1.32rem;
  text-transform: uppercase; letter-spacing: .04em;
}
.band--navy .editorial__body h3, .band--navy-deep .editorial__body h3 { color: #fff; }
.editorial__body p { max-width: 68ch; text-wrap: pretty; }
.band--navy .editorial__body p { color: var(--steel); }
.editorial__body strong { font-weight: 600; }
.band--navy .editorial__body strong { color: #fff; }

/* A plain ruled list — the same grammar as the inspection checklist, because it
   is the same job: named things with one sentence each, all visible at once. */
.rules-list { margin-top: var(--md); }
.rules-list > div {
  padding-block: 14px;
  border-top: 1px solid var(--hairline);
}
.band--navy .rules-list > div, .band--navy-deep .rules-list > div { border-top-color: var(--hairline-dark); }
.rules-list > div:last-child { border-bottom: 1px solid var(--hairline); }
.band--navy .rules-list > div:last-child { border-bottom-color: var(--hairline-dark); }
.rules-list dt {
  position: relative;
  padding-left: 21px;
  font-family: var(--display); font-weight: 800; font-size: 1.06rem;
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--ink);
}
.band--navy .rules-list dt, .band--navy-deep .rules-list dt { color: #fff; }
/* 9px square, 12px gap — the inspection band's mark, at the inspection band's
   measurements, so the two lists are visibly one component. */
.rules-list dt::before {
  content: ""; position: absolute; left: 0; top: .42em;
  width: 9px; height: 9px; background: var(--red-500);
}
.rules-list dd {
  margin-top: 5px; padding-left: 21px;
  max-width: 66ch; color: var(--ink-2);
}
.band--navy .rules-list dd, .band--navy-deep .rules-list dd { color: var(--steel); }

/* --- Q&A -----------------------------------------------------------------
   Every answer is open. No accordion: this is plain information, and the
   No-Interaction Rule says plain information does not get hidden behind a
   mechanism the visitor has to operate. It is also what makes the page
   quotable — an answer folded inside a <details> is an answer a search
   summary has to guess at. -------------------------------------------------- */
.qa { margin-top: clamp(22px, 2.6vw, 34px); }
.qa__item {
  padding-block: clamp(18px, 2vw, 26px);
  border-top: 1px solid var(--hairline);
}
.qa__item:last-child { border-bottom: 1px solid var(--hairline); }
.qa__q {
  font-family: var(--display); font-weight: 800;
  font-size: clamp(1.16rem, 1.7vw, 1.42rem); line-height: 1.14;
  text-transform: uppercase; letter-spacing: .02em;
  color: var(--ink);
}
.qa__a { margin-top: var(--xs); max-width: 68ch; color: var(--ink-2); text-wrap: pretty; }
.qa__a + .qa__a { margin-top: var(--sm); }
.qa__a strong { color: var(--ink); font-weight: 600; }

/* --- Sibling services ----------------------------------------------------
   Not the homepage's services index: that component is a showcase with
   photographs and a hover reveal, and its job is to introduce the six. This is
   navigation between siblings, so it is a plain ruled list of names. Same
   grammar, a fifth of the weight, and no photographs to download. ---------- */
.also { margin-top: clamp(20px, 2.4vw, 30px); border-top: 1px solid var(--hairline); }
.also__row {
  display: flex; align-items: baseline; gap: var(--md);
  padding-block: clamp(14px, 1.7vw, 20px);
  border-bottom: 1px solid var(--hairline);
  color: var(--ink); text-decoration: none;
  transition: background-color .18s ease;
}
.also__no {
  flex: none;
  font-family: var(--display); font-weight: 800; font-size: .82rem;
  letter-spacing: .16em; color: var(--ink-2);
}
.also__name {
  flex: 1 1 auto;
  font-family: var(--display); font-weight: 900;
  font-size: clamp(1.24rem, 2.1vw, 1.72rem); line-height: 1.06;
  text-transform: uppercase; letter-spacing: -.004em;
}
.also__go { flex: none; width: 22px; height: 16px; color: var(--red-500); transition: transform .18s var(--ease); }
.also__row:hover, .also__row:focus-visible { background: var(--white); }
.also__row:hover .also__go, .also__row:focus-visible .also__go { transform: translateX(6px); }

/* --- The subpage close --------------------------------------------------- */
/* The statement band carries a page-level heading here rather than the
   homepage's headline, and Ringo appears once. On the homepage he appears
   twice and each entrance is a different job; on a subpage there is one
   conversion moment, so there is one entrance. */
.say--sub .say__headline { font-size: clamp(1.9rem, 3.4vw, 3rem); }

@media (max-width: 980px) {
  .editorial { grid-template-columns: minmax(0, 1fr); }
  .editorial__rail .sec-head { margin-bottom: var(--sm); }
}
@media (max-width: 760px) {
  .page-hero { padding-block: calc(var(--header-h) + clamp(26px, 8vw, 44px)) clamp(38px, 9vw, 60px); }
  .page-hero .display { font-size: clamp(2.1rem, 10vw, 3.2rem); }
  /* The 62% above exists to keep the hero copy clear of the photograph on a wide
     screen. Below this width the photograph is a full-bleed scrimmed backdrop, so
     the constraint stops buying anything and starts costing: at 390px it squeezed
     the column to ~240px, left 38% of the screen empty, and forced the primary
     call button to break its own phone number across two lines. */
  .page-hero__in { max-width: 100%; }
}
@media (max-width: 620px) {
  .crumbs { font-size: .72rem; letter-spacing: .12em; }
  .also__name { font-size: 1.16rem; }
}

/* ==========================================================================
   Footer
   ========================================================================== */

.site-footer { background: var(--navy-950); color: var(--steel); padding-block: var(--xl) var(--lg); }
.site-footer__grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: var(--xl) var(--lg); padding-bottom: var(--xl); border-bottom: 1px solid var(--hairline-dark); }
.site-footer h3 { color: #fff; font-family: var(--display); font-weight: 800; font-size: 1.06rem; text-transform: uppercase; letter-spacing: .12em; margin-bottom: var(--sm); }
.site-footer li { margin-bottom: 10px; }
.site-footer a { text-decoration: none; transition: color .16s ease; }
.site-footer a:hover { color: #fff; }
.site-footer__logo img { height: 70px; width: auto; margin-bottom: var(--sm); }
.site-footer__legal {
  display: flex; flex-wrap: wrap; gap: var(--xs) var(--lg);
  justify-content: space-between; padding-top: var(--md);
  font-size: .86rem; color: var(--steel-2);
}

/* ==========================================================================
   Persistent call bar (phones)
   ========================================================================== */

.call-bar {
  display: none;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-callbar);
  background: var(--red-500);
  box-shadow: 0 -8px 30px -8px rgba(3, 9, 22, .8);
}
.call-bar a {
  display: flex; align-items: center; justify-content: center; gap: 11px;
  padding: 16px; color: #fff; text-decoration: none;
  font-family: var(--display); font-weight: 800; font-size: 1.32rem;
  text-transform: uppercase; letter-spacing: .02em;
}
.call-bar svg { width: 19px; height: 19px; flex: none; }

/* ==========================================================================
   Motion — one entrance grammar, from an already-legible default
   ========================================================================== */

/* Reveals are scoped to .js — with scripting off nothing is ever hidden, so a
   failed script can't take the page with it. */
/* --- Findings list: service-area pages' authored moment ---
   Copy's point: this damage looks like nothing from the ground. So TEXT is present at
   first paint, never hidden waiting on a callback. The MARK animates — red square
   strokes from the left, in order, like findings marked off on an inspection.
   Reuses [data-rise] only for in-view; opts out of its fade+rise. One idea: scaleX on
   a 9px square, origin left. Nothing on the text. */
.js .rules-list--marked[data-rise],
.js .rules-list--marked[data-rise].is-in {
  opacity: 1; transform: none; transition: none;
}
.js .rules-list--marked dt::before {
  transform: scaleX(0);
  transform-origin: left center;
}
.js .rules-list--marked.is-in dt::before {
  transform: none;
  transition: transform .28s var(--ease);
}
/* Stagger reads as sequence, then stops. Capped at .22s so a longer list lands
   in the same half-second as a short one rather than trailing off down the page. */
.js .rules-list--marked.is-in > div:nth-child(2) dt::before { transition-delay: .05s; }
.js .rules-list--marked.is-in > div:nth-child(3) dt::before { transition-delay: .09s; }
.js .rules-list--marked.is-in > div:nth-child(4) dt::before { transition-delay: .13s; }
.js .rules-list--marked.is-in > div:nth-child(5) dt::before { transition-delay: .17s; }
.js .rules-list--marked.is-in > div:nth-child(6) dt::before { transition-delay: .20s; }
.js .rules-list--marked.is-in > div:nth-child(n+7) dt::before { transition-delay: .22s; }

.js [data-rise] { opacity: 0; transform: translateY(18px); }
.js [data-rise].is-in {
  opacity: 1; transform: none;
  transition: opacity .6s var(--ease), transform .6s var(--ease);
}
/* Stagger only where a group genuinely arrives as a group — service cards,
   process steps, damage plates. Capped at 240ms total; past that a list stops
   reading as a list and starts reading as a wait. */
.js [data-rise][data-delay="1"].is-in { transition-delay: .06s; }
.js [data-rise][data-delay="2"].is-in { transition-delay: .12s; }
.js [data-rise][data-delay="3"].is-in { transition-delay: .18s; }
.js [data-rise][data-delay="4"].is-in { transition-delay: .24s; }
.js [data-rise][data-delay="5"].is-in { transition-delay: .24s; }

/* The system's own mark, drawing itself.

   The 56px red rule under every heading is the single recurring accent in this
   design system. Section headings therefore do NOT rise — they are simply
   there, and the rule strikes under them as the section arrives. That is the
   design system introducing each section, which is a reason; thirty-one
   elements sharing one fade-and-rise is not. */
/* clip-path, deliberately not transform: scaleX(). A scaled-to-zero element
   has a zero-area box, so IntersectionObserver can never satisfy a non-zero
   threshold and the rule would never be told to draw. clip-path leaves the
   layout box intact. */
.js .sec-head__rule { clip-path: inset(0 100% 0 0); }
.js .sec-head--center .sec-head__rule { clip-path: inset(0 50% 0 50%); }
.js .sec-head__rule.is-drawn {
  clip-path: inset(0 0 0 0);
  transition: clip-path .52s var(--ease) .08s;
}

/* ==========================================================================
   Responsive
   ========================================================================== */

@media (max-width: 1180px) {
  :root { --header-h: 124px; --brand-drop: 34px; }
  /* The utility bar STAYS — it carries the phone number. Only the tagline half
     drops, which is the part that isn't load-bearing. */
  .utility-bar__trust { display: none; }
  .utility-bar__in { justify-content: flex-end; }
  .nav { display: none; }
  .header-cta { margin-left: auto; }
  .menu-toggle { display: block; margin-left: var(--sm); }
  .brand {
    flex-basis: calc(clamp(190px, 28vw, 254px) + var(--bleed-left));
    padding-right: 38px;
  }
}

@media (max-width: 980px) {
  .plates { grid-template-columns: 1fr; gap: var(--lg); }
  .about { grid-template-columns: 1fr; }
  .cta { grid-template-columns: 1fr; }
  /* The copy narrows before he does, so the two never meet. */
  .say__copy { max-width: min(42ch, 62%); }
  .insp { grid-template-columns: 1fr; gap: var(--lg); }
  /* Stacked, the copy runs the full width, so the band's foot is sized from his
     own height (the asset is 511×551, so height = width × 1.078) rather than a
     guessed vw value. A guess left the closing line printed across his mane. */
  .insp-band {
    --ringo-w: min(64vw, 300px);
    padding-bottom: calc(var(--band) + var(--ringo-w) * 1.078 + var(--md));
  }
  .insp__ringo { width: var(--ringo-w); }
  .site-footer__grid { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 900px) {
  .process { grid-template-columns: repeat(2, 1fr); row-gap: var(--lg); }
  .step:nth-child(3) { padding-left: 0; border-left: none; }

}

@media (max-width: 760px) {
  /* Below this the utility bar genuinely doesn't fit, so the persistent call
     bar takes over. The two swap at the same breakpoint, so there is never a
     width where the phone number is missing from the page. */
  :root { --header-h: 88px; }
  .utility-bar { display: none; }
  .header-btn { display: none; }
  .call-bar { display: block; }
  body { padding-bottom: 58px; }
  .say__headline { font-size: clamp(1.9rem, 6vw, 2.6rem); }
  .gallery { grid-template-columns: repeat(2, 1fr); }
  .gallery > *:first-child { grid-column: span 2; }
}

@media (max-width: 620px) {
  :root { --header-h: 98px; --brand-drop: 0px; }
  .main-nav { height: var(--header-h); }
  .site-header { filter: drop-shadow(0 4px 14px rgba(6, 20, 40, .16)); }
  .site-header__in { gap: var(--sm); }
  .brand {
    align-self: center;
    flex: 0 0 auto;
    width: calc(184px + var(--bleed-left));
    height: var(--header-h);
    margin: 0 0 0 calc(var(--bleed-left) * -1);
    padding: 6px 14px 6px var(--bleed-left);
  }
  .brand img { height: 80px; width: auto; }
  .menu-toggle {
    font-size: .86rem;
    padding: 8px 12px;
  }

  .process { grid-template-columns: 1fr; row-gap: var(--md); }
  .step { padding: 0; border-left: none; }
  /* The copy runs the full width here, so it can no longer sit beside him. The
     band grows a foot instead, sized from his own rendered height rather than a
     guessed vw value — the same arithmetic the inspection band uses, for the
     same reason: a guess printed a line of type across his mane once. */
  .say {
    --ringo-w: min(52vw, 232px);
    min-height: 0;
    padding-bottom: calc(clamp(48px, 5.2vw, 82px) + var(--ringo-vis));
  }
  .say__copy { max-width: none; }
  .say__lead { font-size: .98rem; line-height: 1.55; max-width: none; }
  .say__or { margin-top: var(--md); }
  .say__tel { font-size: 1.3rem; }

  /* Name and phone stop sharing a line — a half-width field at this size is not
     a field anyone can read what they typed in. */
  .field-grid { grid-template-columns: minmax(0, 1fr); }
  .form-card { padding: clamp(24px, 6vw, 32px) clamp(18px, 5vw, 26px) clamp(24px, 5vw, 30px); }
  .form-card__title { font-size: clamp(1.7rem, 8vw, 2.1rem); }
  .form-card__submit { min-height: 60px; font-size: 1.2rem; }
  .form-fine { align-items: flex-start; text-align: left; }
  .site-footer__grid { grid-template-columns: 1fr; gap: var(--lg); }

  .hero { min-height: 92vh; }
  .hero__fact { border-right: none; margin-right: 0; padding-right: 0; flex: 1 1 100%; }
  .hero__facts { gap: var(--sm) 0; }
  .actions .btn { flex: 1 1 100%; }
  .hero__scroll { display: none; }
  .plate { grid-template-columns: 84px 1fr; }
  .plate__fig { width: 84px; }
}

@media (max-width: 380px) {
  /* The call button is the most important control on this site, and at this width
     the generous 34px inline padding stops reading as confidence and starts
     breaking the phone number across two lines — a control that looks broken at
     exactly the moment someone needs it. Trim the padding, never the type: the
     number stays on one line and the button stays full-width, so nothing about the
     tap target shrinks. --gutter has already clamped to 20px by here, so this is
     the last space available to give back. */
  .btn--primary, .btn--white { padding-inline: 16px; }
  .btn--outline, .btn--outline-light { padding-inline: 14px; }
}

/* ===== Overflow containment — real input, not sample input =====
   Unbroken token (pasted URL, long rural address, unhyphenatable place name) pushes
   its flex/grid parent past the viewport. min-width:0 drops the automatic minimum;
   overflow-wrap:break-word breaks instead of pushes. Both inert when text fits. */

/* Grid children with UNPREDICTABLE text. Without this the automatic minimum wins and
   the container grows instead of wrapping: a 151-char token made a footer column 602px
   inside its 1fr share.
   Do NOT apply to .site-header__in / .utility-bar__in — flex rows of authored labels
   that need the automatic minimum. Header nav must refuse to shrink when .header-tel
   appears on condense; shrinking drew nav links through the phone number. */
.site-footer__grid > *,
.field { min-width: 0; }

/* Break only what cannot fit. `break-word` leaves ordinary wrapping alone,
   unlike `anywhere`, which also lets short words split mid-syllable. */
.prose,
.sec-head__deck,
.svc-row__desc,
.field__err,
.form-status,
.site-footer p,
.site-footer li,
.field textarea { overflow-wrap: break-word; }

/* A select is the one control whose width is set by its longest <option>, and
   "Roof is old — considering replacement" is long. Let it shrink and ellipsise
   rather than widen the field grid past the card. */
.field select { text-overflow: ellipsis; }

/* ==========================================================================
   Reduced motion
   ========================================================================== */

/* Reduced motion stops ambient and entrance motion. It does NOT stop the
   interface from answering: hover, focus, error and submit feedback still
   transition, just quickly. A blanket kill removes the page's replies along
   with its decoration, which is a worse experience, not a calmer one. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
  }
  * { transition-duration: .12s !important; }

  .hero__rain, .hero__light { display: none; }

  .js [data-rise] { opacity: 1 !important; transform: none !important; transition: none !important; }
  /* Every finding is already marked. The list is a checklist, not an event. */
  .js .rules-list--marked dt::before { transform: none !important; transition: none !important; }
  .js .sec-head__rule { clip-path: none !important; transition: none !important; }
  /* The truck is simply parked. A vehicle stopped halfway across the plinth
     would be worse than no arrival at all. */
  .js .about__truck {
    transform: none !important; opacity: 1 !important; will-change: auto;
  }

  /* The submit bar is the one loop worth keeping: it is the only signal that
     the request is still in flight, and its absence reads as a dead button. */
  .btn.is-sending::after { animation-duration: 1.05s !important; animation-iteration-count: infinite !important; }
}

/* ==========================================================================
   Forced colours (Windows High Contrast and friends)

   The system replaces every colour with its own pair. Two things break silently.

   1. A mark drawn only with background-colour disappears — and the 56px red rule
      sits under EVERY heading on this site, so its loss reads as a broken page
      rather than a missing accent. Re-asserted in CanvasText so the system draws it.
   2. background-image and <video> are NOT forced away, so hero copy would keep
      sitting on storm footage while its own colour flipped to a system value.
      Unreadable. The decorative layers are hidden instead and the copy lands on
      Canvas, which is the whole point of the mode.

   Buttons get an explicit border: forced to ButtonFace, a red fill and a white page
   become the same rectangle without one.
   ========================================================================== */
@media (forced-colors: active) {
  /* marks that exist only as a background colour */
  .sec-head__rule,
  .band--red .sec-head__rule { background: CanvasText; }
  .rules-list dt::before { background: CanvasText; }
  .utility-bar__trust .sep { background: CanvasText; }

  /* decoration the mode cannot recolour — remove it so text sits on Canvas */
  .hero__video, .hero__rain, .hero__dawn, .hero__scrim, .hero__light,
  .page-hero__shot, .page-hero__scrim { display: none; }

  /* keep every control a distinguishable shape */
  .btn, .header-btn { border: 1px solid ButtonText; }
  .call-bar { border-top: 1px solid CanvasText; }
  .form-card { border: 1px solid CanvasText; }
  .form-card__stamp { border: 2px solid CanvasText; }
}

@media print {
  .hero__video, .hero__rain, .hero__scrim, .hero__light,
  .site-header, .call-bar, .menu-panel { display: none !important; }
  body { background: #fff; color: #000; padding: 0; }
  .band, .hero { background: #fff !important; color: #000 !important; padding-block: 24px; }
}
