/* ==========================================================================
   Proficient Plumbing and Drain — homepage ("Refined" direction)
   ========================================================================== */

:root {
  --green:        #1E3A24;  /* brand green — bars, buttons, headings on dark */
  --green-deep:   #16281A;  /* hero + estimate band */
  --green-darkest:#0f1c12;  /* footer */
  --gold:         #E2A32B;  /* primary accent / CTA */
  --gold-rgb:     226, 163, 43;  /* same colour, unpacked for rgba() — keep in sync */
  --gold-dark:    #C8901F;  /* hover fill for solid gold buttons */
  --rust:         #B3341F;  /* eyebrows, "learn more", emergency */

  --ink:          #1b1e22;  /* body text on light */
  --body:         #4a5049;  /* long-form paragraphs */
  --muted:        #5c635b;  /* secondary text */
  --muted-warm:   #3c433c;  /* mega-menu links */

  --cream:        #fbfaf7;  /* tinted section background */
  --line:         #eae7df;  /* card + section borders */
  --line-header:  #e8e6e0;  /* header borders */
  --line-input:   #ddd9d0;  /* form field borders */

  --sans:  'Public Sans', system-ui, sans-serif;
  --display: 'Archivo', sans-serif;
  --mono: ui-monospace, Menlo, monospace;

  --wide: 1240px;
  --narrow: 900px;
}

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */

* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; background: #fff; }

body {
  background: #fff;
  font-family: var(--sans);
  color: var(--ink);
}

/* `clip` rather than `hidden`: it contains the hero watermark's horizontal
   overflow without creating a scroll container, which would stop the sticky
   header and mobile call bar from sticking. */
.page { overflow-x: clip; }

a { color: inherit; text-decoration: none; }
a:hover { color: var(--gold); }

/* Solid gold buttons are anchors, so the rule above would paint gold text on a
   gold fill — invisible. Hold the label colour and darken the fill instead.
   Any new gold-background link needs adding here. */
.btn-gold:hover,
.btn-gold-lg:hover,
.mega__promo-phone:hover,
.mobilenav__cta:hover,
.sticky-cta__call:hover {
  background: var(--gold-dark);
  color: var(--green);
}

@keyframes slideDown {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}

/* Container queries drive every clamp() below, so the page scales off its own
   width rather than the viewport. */
.frame { container-type: inline-size; width: 100%; }

/* --------------------------------------------------------------------------
   Shared primitives
   -------------------------------------------------------------------------- */

.container { max-width: var(--wide); margin: 0 auto; padding-left: 24px; padding-right: 24px; }
.container--narrow { max-width: var(--narrow); }

.section       { padding-top: clamp(52px, 7cqw, 84px); padding-bottom: clamp(52px, 7cqw, 84px); }
.section--tall { padding-top: clamp(52px, 7cqw, 88px); padding-bottom: clamp(52px, 7cqw, 88px); }

.band--cream { background: var(--cream); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.band--cream-top { background: var(--cream); border-top: 1px solid var(--line); }
.band--green { background: var(--green); color: #fff; }
.band--deep  { background: var(--green-deep); color: #fff; }

.eyebrow {
  font-size: 12px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--rust);
  font-weight: 800;
}

.h2 {
  font-family: var(--display);
  font-size: clamp(26px, 3.4cqw, 38px);
  letter-spacing: -.02em;
  margin: 0;
}

.link-arrow {
  font-weight: 700;
  color: var(--rust);
  border-bottom: 2px solid var(--gold);
  padding-bottom: 2px;
}
.link-arrow--sm { font-size: 14.5px; }

.note {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .05em;
  color: #7c8479;
}

/* Icons — Material Symbols (fonts.google.com/icons), inlined as an SVG sprite
   in index.html rather than loaded as an icon font. No second render-blocking
   request, and no flash of the literal ligature text ("water_heater") while a
   font downloads. `fill: currentColor` means each icon inherits its context.
   To add one: grab the 24px SVG, add a <symbol id="i-name">, reference it with
   <use href="#i-name">. Keep the 0 -960 960 960 viewBox — Material Symbols
   sit on a baseline-relative grid and will vanish with a 0 0 24 24 viewBox. */
.icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.service-card__icon {
  width: 24px;
  height: 24px;
  fill: currentColor;
  color: var(--rust);
}

/* Inline arrows. Sized in em so they track whatever text they sit in — 14px
   "Learn more" and 15px "View all services" both get a proportionate glyph. */
.icon-arrow {
  width: 1.05em;
  height: 1.05em;
  fill: currentColor;
  vertical-align: -.2em;
}

.footer__icon {
  width: 16px;
  height: 16px;
  fill: currentColor;
  color: var(--gold);
  vertical-align: -3px;
  margin-right: 7px;
}

/* The phone line is already white and emphasised; let the icon match it. */
.footer__icon--phone { width: 19px; height: 19px; vertical-align: -4px; color: inherit; }

.badge__icon {
  width: 19px;
  height: 19px;
  fill: currentColor;
  color: var(--gold);
  flex-shrink: 0;
  /* .badge aligns its children to the baseline, which an SVG has no useful
     notion of — centre this one instead so it sits with the text. */
  align-self: center;
}

/* Panels toggled by JS (mega menu, mobile nav) */
.panel { display: none; }
.panel.open { display: block; animation: slideDown .18s ease; }

/* --------------------------------------------------------------------------
   Top utility bar
   -------------------------------------------------------------------------- */

.topbar { background: var(--green); color: #cfd8cb; font-size: 13px; }

.topbar__inner {
  padding-top: 9px;
  padding-bottom: 9px;
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
}

.topbar__tagline { letter-spacing: .02em; }

.topbar__links {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
}

.topbar__link { color: #cfd8cb; }
.topbar__phone { color: var(--gold); font-weight: 700; }

/* --------------------------------------------------------------------------
   Header
   -------------------------------------------------------------------------- */

.header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: #fff;
  border-bottom: 1px solid var(--line-header);
  box-shadow: 0 1px 12px rgba(0, 0, 0, .05);
}

.header__inner {
  padding-top: 14px;
  padding-bottom: 14px;
  display: flex;
  align-items: center;
  gap: 28px;
}

/* The logo overhangs the header on purpose — the negative margin lets it sit
   proud of the bar without growing the bar's height. */
.logo {
  display: flex;
  align-items: flex-start;
  flex-shrink: 0;
  position: relative;
  z-index: 2;
  align-self: stretch;
}

.logo__img {
  height: clamp(72px, 11cqw, 140px);
  width: auto;
  display: block;
  margin-bottom: clamp(-52px, -5cqw, -24px);
  filter: drop-shadow(0 10px 22px rgba(20, 32, 20, .28));
  transition: height .25s ease, margin-bottom .25s ease;
}

/* 1px tracer just above the header. Once it scrolls out of view the header is
   stuck, and JS mirrors that as .header--stuck. */
.header-sentinel { height: 1px; }

/* Shrink-on-stick. The stuck height is deliberately the sum of the resting
   height and its own negative margin — i.e. exactly the space the logo already
   occupies in flow. The bar's height therefore never changes, so nothing below
   the sticky header reflows when the class flips. Do not replace this calc with
   a fixed px value; that reintroduces the jump. */
.header--stuck .logo__img {
  height: calc(clamp(72px, 11cqw, 116px) + clamp(-52px, -5cqw, -24px));
  margin-bottom: 0;
  filter: drop-shadow(0 4px 10px rgba(20, 32, 20, .18));
}

/* With the overhang gone there is nothing to clear, so drop the reserved space
   back to the panel's normal top padding (see .mega__inner). */
.header--stuck .mega__inner { padding-top: 28px; }

@media (prefers-reduced-motion: reduce) {
  .logo__img { transition: none; }
}

.nav {
  display: flex;
  align-items: center;
  /* Tightens as the bar narrows so the long "Free On-Site Estimate" CTA still
     fits alongside the three menus and the dispatch number just above the
     900px breakpoint, where the desktop nav is at its most cramped. */
  gap: clamp(16px, 2.2cqw, 26px);
  margin-left: auto;
  font-weight: 600;
  font-size: 15px;
  /* Fill the bar's full height. Without this the nav is only as tall as its own
     content and sits centred in the taller (logo-driven) header, so a dropdown
     at top:100% would start above the header's bottom edge. Children stay
     vertically centred via align-items above, so nothing moves visually. */
  align-self: stretch;
}

/* Stretched to the full header height so a dropdown anchored at top:100% drops
   from the bar rather than from the middle of the text. */
.nav__item {
  position: relative;
  align-self: stretch;
  display: flex;
  align-items: center;
}

.nav__trigger {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 6px;
}
.nav__trigger[aria-expanded="true"] { color: var(--rust); }

.nav__caret { width: 18px; height: 18px; fill: currentColor; transition: transform .18s ease; }
.nav__trigger[aria-expanded="true"] .nav__caret { transform: rotate(180deg); }

.nav__emergency { color: var(--rust); }

.nav__phone {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  padding-left: 8px;
}

.nav__phone-label {
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #7b8177;
  font-weight: 700;
}

.nav__phone-number { font-size: 21px; font-weight: 800; color: var(--green); }

.btn-gold {
  background: var(--gold);
  color: var(--green);
  padding: 13px clamp(13px, 1.6cqw, 20px);
  border-radius: 6px;
  font-weight: 800;
  letter-spacing: .01em;
  /* Two words on two lines would make the button taller than the bar. */
  white-space: nowrap;
}

/* Mobile header controls */
.nav-mobile {
  margin-left: auto;
  align-items: center;
  gap: 10px;
}

.btn-call-sm {
  background: var(--green);
  color: #fff;
  padding: 0 16px;
  height: 46px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  font-weight: 700;
  font-size: 15px;
}

.btn-hamburger {
  width: 46px;
  height: 46px;
  border-radius: 6px;
  border: 1px solid #d8d5cd;
  background: #fff;
  font-size: 20px;
  cursor: pointer;
}

/* --------------------------------------------------------------------------
   Mega menu
   -------------------------------------------------------------------------- */

/* Overlays the page rather than sitting in flow — on hover, pushing the hero
   down every time the pointer crosses the trigger reads as a broken layout.
   Positioned against .header, which is sticky and therefore a containing block. */
.mega {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  border-top: 1px solid var(--line-header);
  background: var(--cream);
  box-shadow: 0 14px 28px rgba(20, 32, 20, .10);
}

.mega__inner {
  /* The logo hangs below the header bar and paints over this panel (.logo has
     z-index 2). Reserve exactly its overhang up top so it lands in the panel's
     whitespace instead of covering the first column. The clamp mirrors the
     negative margin on .logo__img; the 14px is .header__inner's bottom padding,
     which the overhang does not clear. Keeps the original 28px above the text. */
  padding-top: calc(28px + clamp(24px, 5cqw, 52px) - 14px);
  padding-bottom: 32px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 28px 32px;
}

.mega__title {
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold);
  font-weight: 800;
  margin-bottom: 12px;
}

.mega__links { display: flex; flex-direction: column; gap: 9px; }
.mega__link { font-size: 14.5px; color: var(--muted-warm); }

/* Full-width row under the group columns — the trigger is a <button>, so this
   is the only route to the /plumbing-services/ hub page itself. */
.mega__all {
  grid-column: 1 / -1;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

.mega__promo {
  background: var(--green);
  color: #fff;
  border-radius: 10px;
  padding: 22px;
}

.mega__promo-eyebrow {
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold);
  font-weight: 800;
}

.mega__promo-line { font-size: 19px; font-weight: 700; margin: 8px 0 14px; line-height: 1.3; }

.mega__promo-phone {
  display: inline-block;
  background: var(--gold);
  color: var(--green);
  padding: 11px 18px;
  border-radius: 6px;
  font-weight: 800;
}

/* --------------------------------------------------------------------------
   Simple dropdowns (Water Heaters, Water Treatment)
   Anchored to their own trigger rather than spanning the header — these hold
   4–5 links, which does not earn the full-width mega treatment.
   -------------------------------------------------------------------------- */

.dropdown {
  /* Negative offset equals the inset, so the links sit flush with the trigger
     text above them rather than stepping in by the panel's padding. */
  --dropdown-inset: 22px;

  position: absolute;
  /* 100% is the bottom of .header__inner's content box; +14px clears its bottom
     padding, landing the top edge on the header's border — the same plane .mega
     drops from, so both menus hang off the bar identically. */
  top: calc(100% + 14px);
  left: calc(var(--dropdown-inset) * -1);
  z-index: 3;
  min-width: 250px;
  padding: 20px var(--dropdown-inset);
  background: var(--cream);
  border: 1px solid var(--line-header);
  box-shadow: 0 14px 28px rgba(20, 32, 20, .10);

  flex-direction: column;
  gap: 9px;
}

/* .panel.open sets display:block and outranks .dropdown on specificity. Equal
   specificity here, declared later, so the column layout survives opening. */
.dropdown.open { display: flex; }

/* Deliberately identical to .mega__link — weight 400 from the body default, not
   the 600 the nav bar sets. Hover colour comes from the global a:hover, same as
   the mega's links. */
.dropdown__link {
  font-size: 14.5px;
  font-weight: 400;
  color: var(--muted-warm);
  white-space: nowrap;
}

/* Route to the section's own hub page, which the <button> trigger cannot reach. */
.dropdown__overview {
  font-size: 14.5px;
  font-weight: 700;
  color: var(--ink);
  white-space: nowrap;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}

/* --------------------------------------------------------------------------
   Mobile nav drawer
   -------------------------------------------------------------------------- */

.mobilenav {
  border-top: 1px solid var(--line-header);
  background: #fff;
  padding: 16px 20px 24px;
  /* Expanded sections can outgrow the screen; scroll the drawer rather than
     letting it push the sticky header taller than the viewport. */
  max-height: calc(100vh - 80px);
  max-height: calc(100dvh - 80px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.mobilenav__links { display: flex; flex-direction: column; gap: 2px; }

.mobilenav__link {
  padding: 14px 4px;
  border-bottom: 1px solid #f0eee8;
  font-weight: 600;
  font-size: 16px;
}

/* Collapsible sections — the drawer's stand-in for the mega menu and the two
   desktop dropdowns. */
.drawer-item { border-bottom: 1px solid #f0eee8; }

.drawer-item__btn {
  width: 100%;
  padding: 14px 4px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  background: none;
  border: 0;
  font: inherit;
  font-weight: 600;
  font-size: 16px;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}

.drawer-item__sign { width: 22px; height: 22px; fill: currentColor; color: var(--gold); flex-shrink: 0; }

.drawer-item__panel { padding: 2px 0 14px; }

.drawer-group + .drawer-group { margin-top: 18px; }

.drawer-group__title {
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold);
  font-weight: 800;
  margin: 0 0 6px 12px;
}

.drawer-sublink {
  display: block;
  padding: 11px 12px;
  border-radius: 6px;
  font-size: 15px;
  font-weight: 500;
  color: var(--muted-warm);
}

.drawer-sublink:hover { background: var(--cream); color: var(--ink); }

.drawer-sublink--overview {
  font-weight: 700;
  color: var(--ink);
  margin-bottom: 6px;
  border-bottom: 1px solid #f0eee8;
  border-radius: 0;
}

.mobilenav__cta {
  margin-top: 16px;
  background: var(--gold);
  color: var(--green);
  text-align: center;
  padding: 16px;
  border-radius: 6px;
  font-weight: 800;
}

/* --------------------------------------------------------------------------
   Halftone — shared decorative screen
   -------------------------------------------------------------------------- */

/* The circle screen Proficient runs on its trucks and print collateral. A real
   halftone ramps dot *radius* across a fixed grid, which no single CSS gradient
   can express, so this approximates it with two fixed-radius tiers — coarse,
   then fine — each masked into its own band of the ramp. Both tiers paint two
   dot grids offset by half a pitch, which lands the lattice on the 45° screen
   angle a printer would use rather than a square grid.

   The tiers live on ::before / ::after and never on the element itself: a mask
   set on .halftone would clip its own pseudo-elements too, so the coarse tier's
   mask would erase the fine one.

   The ruling — --halftone-pitch and the two dot radii — is deliberately not a
   cqw clamp. A halftone is a fixed ruling; scaling it continuously with the
   container makes the dots visibly grow and shrink as the frame resizes, which
   reads as a zoom rather than a texture. It steps once at the breakpoint
   instead, further down.

   --halftone-angle should match whatever gradient the screen is dissolving, so
   the dots break that edge up instead of crossing it at an angle of their own. */
.halftone {
  position: absolute;
  inset: 0;
  pointer-events: none;

  --halftone-pitch: 15px;
  --halftone-coarse: 2.5px;
  --halftone-fine: 1.1px;
  --halftone-feather: .8px;
  --halftone-angle: 90deg;
  /* Brand gold, not white. These dots never run above ~13% alpha, so most of
     what you see is still the green underneath — the gold tints it warm rather
     than printing as gold. Desaturating the ink toward cream first looks like
     the safer hedge and is the wrong move: at this alpha the hue washes out and
     the dots go back to reading as plain white. Saturate the ink and hold the
     alpha down; that is what keeps it warm without getting loud. */
  --halftone-ink: var(--gold-rgb);
  --halftone-strength: 1;
}

.halftone::before,
.halftone::after {
  content: '';
  position: absolute;
  inset: 0;
  background-size: var(--halftone-pitch) var(--halftone-pitch);
  background-position: 0 0, calc(var(--halftone-pitch) / 2) calc(var(--halftone-pitch) / 2);
}

/* Coarse tier — the solid end of the ramp. --halftone-feather is the gap between
   the colour stop and the transparent stop, i.e. the dot's antialiased edge; a
   hard stop stair-steps badly at these radii. */
.halftone::before {
  --halftone-dot: rgba(var(--halftone-ink), calc(.15 * var(--halftone-strength)));
  --halftone-edge: calc(var(--halftone-coarse) + var(--halftone-feather));
  background-image:
    radial-gradient(circle, var(--halftone-dot) var(--halftone-coarse), transparent var(--halftone-edge)),
    radial-gradient(circle, var(--halftone-dot) var(--halftone-coarse), transparent var(--halftone-edge));
  -webkit-mask-image: linear-gradient(var(--halftone-angle), #000 0 26%, transparent 52%);
  mask-image: linear-gradient(var(--halftone-angle), #000 0 26%, transparent 52%);
}

/* Fine tier — carries the screen on past where the coarse dots give out. */
.halftone::after {
  --halftone-dot: rgba(var(--halftone-ink), calc(.11 * var(--halftone-strength)));
  --halftone-edge: calc(var(--halftone-fine) + var(--halftone-feather));
  background-image:
    radial-gradient(circle, var(--halftone-dot) var(--halftone-fine), transparent var(--halftone-edge)),
    radial-gradient(circle, var(--halftone-dot) var(--halftone-fine), transparent var(--halftone-edge));
  -webkit-mask-image: linear-gradient(var(--halftone-angle), transparent 24%, #000 48% 62%, transparent 88%);
  mask-image: linear-gradient(var(--halftone-angle), transparent 24%, #000 48% 62%, transparent 88%);
}

/* Radial flavour — the screen blooms out of a point instead of running across
   an edge. Only the mask changes; the dot tiers above are untouched, and the
   stop percentages are identical to the linear ones so both flavours ramp the
   same way. --halftone-extent is the ellipse's radii measured against the box's
   own width and height, which keeps the falloff proportional as the container
   resizes; --halftone-angle is unused here.

   Both stops have to be restated per tier rather than swapped through one
   variable, since each tier needs its own band of the ramp. */
.halftone--radial {
  --halftone-origin: 24% 52%;
  --halftone-extent: 96% 128%;
}

.halftone--radial::before {
  -webkit-mask-image: radial-gradient(var(--halftone-extent) at var(--halftone-origin), #000 0 26%, transparent 52%);
  mask-image: radial-gradient(var(--halftone-extent) at var(--halftone-origin), #000 0 26%, transparent 52%);
}

.halftone--radial::after {
  -webkit-mask-image: radial-gradient(var(--halftone-extent) at var(--halftone-origin), transparent 24%, #000 48% 62%, transparent 88%);
  mask-image: radial-gradient(var(--halftone-extent) at var(--halftone-origin), transparent 24%, #000 48% 62%, transparent 88%);
}

/* Threshold flavour — the screen off the truck wraps, done properly.
   ------------------------------------------------------------------
   The two-tier flavours above fake a halftone by cross-fading two fixed dot
   radii, so every dot within a tier is the same size. Look at
   img/hero-plumber-van-sacramento.jpg: on the wrap the dots grow steadily as
   they run back toward the gold, which is what a screen actually does.

   This builds that for real. One dot grid is multiplied by a ramp, then
   hard-thresholded with filter: contrast(). Because the threshold cuts the dot
   profile at a different height wherever the ramp is darker, the dot radius
   follows the ramp continuously — a genuine halftone rather than two steps.

   The layer stack on ::before, listed top to bottom as CSS wants it, composites
   bottom-up as:

     dot grid B                        (normal)
     dot grid A          -> lighten    union of the two grids, i.e. the 45°
                                       lattice the other flavours also use
     --halftone-ramp     -> multiply   scales every dot down toward the sparse
                                       end; this is what varies the radius

   This ramps to dots and then to nothing — it never fills to a solid field, and
   it should not. The solid gold on the wrap is not the screen reaching 100%; it
   is a separate vector sunburst that the screen dissolves off the end of. An
   earlier pass lightened a white plateau over this stack to force a solid core
   and it read as a gold blob rising out of the bottom edge. Don't put it back.

   Colour is applied inside the element, not by the gradients: the pattern is
   built in black and white so contrast() has clean channels to threshold (run
   it on gold directly and the three channels clip at different points, which
   swings the hue toward red). ::after then lays flat gold over it with
   multiply — white dots take the gold, the black field stays black — and
   isolation: isolate keeps that multiply from reaching the photo. The element's
   own mix-blend-mode: screen finally drops the black field out, so only the
   gold prints over the hero.

   Both pseudos must clear the mask the base rules set on them, or the tier
   masks above would cut this ramp up as well. */
.halftone--threshold {
  isolation: isolate;
  mix-blend-mode: screen;

  --halftone-origin: 50% 104%;
  --halftone-extent: 60% 40%;
  /* Positions along the ramp: --halftone-hold is how far the dense end runs
     before it starts breaking up, --halftone-run is where it reaches nothing.
     Both are percentages of the lobe's radius, so they scale with the extent. */
  --halftone-hold: 6%;
  --halftone-run: 100%;
  /* Radius of the dot profile as a share of the tile's half-diagonal. At 66%
     the dots just touch at full ramp, which is the coverage the wrap runs. */
  --halftone-spread: 66%;
  /* Threshold position and hardness. --halftone-bias slides the cut up and down
     the dot profile, so it sets how big the dots run at a given ramp value;
     --halftone-hardness is how abruptly the edge falls off. Below about 8 the
     dots keep a soft edge and the screen goes muddy over photography. */
  --halftone-bias: 1.3;
  --halftone-hardness: 16;
  --halftone-ramp: radial-gradient(var(--halftone-extent) at var(--halftone-origin), #fff 0 var(--halftone-hold), #000 var(--halftone-run));
}

/* One lobe per element. The dot grids have to be unioned before the ramp
   multiplies them, and CSS blends background layers strictly bottom-up, so a
   second ramp lobe cannot be folded into this stack — it would end up either
   unramped or un-dotted. Screen is associative, though, so stacking a second
   .halftone--threshold element with its own origin unions the two lobes for
   free. The hero has run two and four elements at various points and is down to
   one now; if it ever needs a second shape, add an element rather than trying to
   fold another ramp in here. */
.halftone--threshold::before {
  background-image:
    var(--halftone-ramp),
    radial-gradient(circle at 50% 50%, #fff 0, #000 var(--halftone-spread)),
    radial-gradient(circle at 50% 50%, #fff 0, #000 var(--halftone-spread));
  background-size:
    100% 100%,
    var(--halftone-pitch) var(--halftone-pitch),
    var(--halftone-pitch) var(--halftone-pitch);
  background-position:
    0 0,
    0 0,
    calc(var(--halftone-pitch) / 2) calc(var(--halftone-pitch) / 2);
  background-blend-mode: multiply, lighten, normal;
  filter: brightness(var(--halftone-bias)) contrast(var(--halftone-hardness));
  -webkit-mask-image: none;
  mask-image: none;
}

.halftone--threshold::after {
  background-image: none;
  background-color: rgb(var(--halftone-ink));
  mix-blend-mode: multiply;
  -webkit-mask-image: none;
  mask-image: none;
}

/* Corner placement — a concave fillet, not a lobe. This replaced a pair of bands
   that ran off the top and bottom edges.

   The distinction is the whole thing. A lobe centred on the corner is convex:
   it bulges out of the corner and reaches furthest along the diagonal, which
   reads as a blob stuck to the frame. This is the inverse — the dots fill the
   corner and their inner boundary is an arc that *cups* it, reaching furthest
   along the two edges and thinnest across the diagonal. That is the swoop, and
   it is the shape a screen makes where it runs off the corner panel of a wrap.

   It is built by inverting the ramp: the ellipse is centred on the *opposite*
   corner and the stops are reversed, so the sparse end is the ellipse's inside
   and the dense end is everything beyond it. The arc you see is the ellipse's
   own edge, which is concave from where the dots are standing.

   Two consequences follow from the dense end being the far field rather than a
   bounded blob, and both are load-bearing:

   1. The element must stay exactly the hero's box — plain inset: 0, no size or
      offset overrides and no transform. Its edges are the only thing bounding
      the dense region, and they land on the frame's edges, so the fillet runs
      off the right and bottom edges the way a screen should. Shrink it, inset
      it or rotate it and that edge cuts a hard straight line through the middle
      of the frame instead. Which is why the swoop is not literally rotated: the
      angle lives in the radii below, not in a transform.
   2. Both radii must stay above 100%, i.e. larger than the box. That is what
      keeps the frame's other two corners inside the ellipse and therefore
      clear: the top-right sits at 1/x-radius from the centre and the
      bottom-left at 1/y-radius, so dropping either below 100% pushes that
      corner past the arc and the screen starts printing up the far edges. The
      x radius runs at 101% now, one point off that floor — this is a big
      fillet, and there is very little room left in that direction.

   The two radii are separate levers, and unlike a lobe's they are *supposed* to
   be percentages: the arc is defined against the frame's own proportions, so it
   has to resolve against them.

     x radius  how far up the right edge the arc climbs. The crossing sits at
               y·sqrt(1 - 1/x²) of the frame height, which goes to zero as x
               approaches 100%, i.e. the arc runs all the way to the top-right
               corner. This is the size lever and it is steep: 101% puts the
               crossing 17% down the edge, 105% puts it at 41%.
     y radius  how far left along the bottom edge it runs, at x·sqrt(1 - 1/y²)
               of the frame width. Bigger holds it further right, which is what
               keeps it off the CTA row.

   The chord between those two crossings is what the eye reads as the angle —
   101%/120% puts it near 40deg off horizontal at desktop proportions. Matched
   radii give a symmetrical quarter-round, which reads as a rounded corner
   rather than a swoop.

   Weight comes from the distance out to the near corner, sqrt(1/x² + 1/y²):
   that is the ramp's far end, so it is the largest dot the screen ever prints.
   It runs about 1.29 wide and 1.17 narrow. Push both radii up until it
   approaches 1.0 and the whole screen goes faint at once. */
.halftone--corner {
  --halftone-origin: 0% 0%;
  --halftone-extent: 101% 160%;
  /* Where the arc sits, and where the dots reach full size, both as multiples
     of the ellipse. --halftone-fill has to stay under the corner distance above
     or nothing in the frame ever reaches the dense end. */
  --halftone-arc: 100%;
  --halftone-fill: 112%;
  --halftone-ramp: radial-gradient(
    var(--halftone-extent) at var(--halftone-origin),
    #000 0 var(--halftone-arc),
    #fff var(--halftone-fill)
  );
}

/* Coarser ruling for the hero once the frame is wide enough to carry it. Scoped
   to the hero on purpose: the estimate band reads better at the base ruling,
   where the dots stay a texture behind the copy rather than becoming a pattern
   in their own right. One shared ruling would be tidier, but it loses on looks.

   A container query, not a media query — everything else in this stylesheet
   sizes off .frame, and the screen should follow the frame's width rather than
   the viewport's. It is a single step rather than a clamp for the reason in the
   note at the top of this section. The hero keeps the base ruling below 900px,
   where the narrow column doesn't have room for dots this size.

   The hero runs the threshold flavour, so the ruling is the only thing this
   block changes — coverage is set by --halftone-spread and --halftone-bias,
   which are ratios against the pitch and therefore already hold as it grows.
   The estimate band keeps the two-tier flavour and the base ruling; do not
   re-unify them.

   The hero's corner fillet steps at this same width too, but that rule lives
   down in the hero section rather than here. It has to: it is keyed on
   .hero__halftone, and the hero's unprefixed values sit at the same specificity
   further down the file, so a @container block up here loses to them on source
   order at every width. That is not hypothetical — it silently pinned the old
   hero bands to their narrow depths for several passes. If you add a width step
   for anything hero-side, put it after the rule it overrides. */
@container (min-width: 900px) {
  .hero__halftone { --halftone-pitch: 22px; }
}

/* --------------------------------------------------------------------------
   Mascot — shared
   -------------------------------------------------------------------------- */

/* One 724×573 SVG (SVG/proficient-plumbing-mascot.svg) used at three sizes.
   It is decorative at every placement — alt="" on each instance — and always
   lazy-loaded: the file is ~290 KB of engraving detail and nothing that uses
   it sits above the fold. Never recolour it with a filter; the red wrench and
   toolbox are the only brand colour in it and grayscale kills them. */
.mascot {
  display: block;
  width: 100%;
  height: auto;
}

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */

/* clip, not hidden — same reason as .page. This was here to contain the photo
   while the drift animation scaled it past 100%; that animation is gone and
   nothing in the hero currently overflows, so it is now a guard rather than a
   fix. Kept because any decorative sibling bled off an edge — which is exactly
   what the halftone fillet is — needs it, and because hidden would create a
   scroll container. .hero holds nothing sticky, so clipping here is safe. */
.hero { position: relative; overflow: clip; background: var(--green-deep); }

/* One screen, filling the bottom-right corner. Earlier passes ran two bands off
   the top and bottom edges, and before that four separate corner lobes; both
   fenced the picture in, and the eye ended up reading the frame rather than the
   screen. One corner leaves the other three open, which is how the wrap runs a
   screen off a single body panel.

   The bottom-right is the only corner it can go in. The copy column owns the
   whole left half — headline, lede and the CTA row — and the top edge is
   squeezed under the header, so the bottom-right is the one corner that holds
   nothing but driveway and van livery.

   What still limits it is a piece of copy, not taste: the outlined "Book a free
   on-site estimate" button, whose border goes hard to pick out under dots. That
   button is the right-hand end of the CTA row, so what matters is where the arc
   meets the *bottom* edge — it has to land to the right of the button. The
   right edge carries nothing at all, so the arc climbs that freely, and the
   difference between the two is where the swoop's angle comes from.

   Narrow is the tighter of the two. The buttons stack and run most of the
   frame's width, so the y radius goes way out to 160% to hold the arc down
   against the bottom edge: by the time it passes the outlined button's right
   end it is only ~2% of the frame height up, and it does not meet the bottom
   edge until 79% across. The x radius still pins it high on the right edge, so
   what you get is a tall crescent hugging the right side and thickening into
   the corner. */
.hero__halftone { --halftone-extent: 101% 160%; --halftone-fill: 112%; }

/* Wide, the hero is short and the copy no longer fills it, so the fillet can
   take a real share of the frame. The x radius stays where it is — it is
   already a point off its floor — and the y radius comes back in from 160% to
   120%, which walks the arc's bottom crossing left from 79% to 56% of the
   width. That is still clear of the outlined button, which ends near 45% at the
   1240px container, and it roughly doubles the painted area: the arc runs from
   17% down the right edge to 56% across the bottom, a chord about 37deg off
   horizontal, and reaches 23% of the way in along the diagonal.

   The corner distance, sqrt(1/x² + 1/y²), comes out at 1.29 here against 1.17
   narrow, so the screen runs a little heavier wide as well as bigger.
   --halftone-fill has to stay under that number; 120% leaves the last fifth of
   the ramp at full dot size, which is a patch roughly 85px across in the corner
   itself.

   This block has to stay below the rule above: it is the same specificity, so
   source order is what decides it. Moved up into the halftone section's
   @container block it loses at every width and the hero silently keeps its
   narrow values. */
@container (min-width: 900px) {
  .hero__halftone { --halftone-extent: 101% 120%; --halftone-fill: 120%; }
}

/* Source frame is 24-17, cropped 2:1 at centering (0.5, 0.10). Both
   object-position values are tuned to that crop and both are load-bearing.

   The hero box is a roughly fixed height set by the copy, while the image scales
   with viewport width — so `cover` crops harder and harder vertically as the
   window widens: ~15% of the image at 1440, 36% at 1920, 52% at 2560, all of it
   off the top and bottom.

   x=76% — the tech stands at ~78% across and the export is already full source
   width, so he cannot be moved by re-cropping. Wide viewports show the whole 2:1
   image and x is inert; narrow ones scale to fill height and show only a ~35%
   slice, and 58% (tuned to the previous hero photo) put that slice left of him.

   y=18% — pulls the crop window toward the top so the vertical loss comes off
   the driveway rather than his head. The export is cut to match: his head sits at
   ~19% of frame height, which survives the 26% top crop at 2560.

   A full-length figure does not fit a 2:1 hero. He spans 76% of the source frame
   and the 2:1 crop has only 75% of its height to give, so he is cut below the
   knee deliberately, behind the CTA row. Centring him instead puts his head on
   the top edge and wide viewports then remove it. If this photo is ever swapped,
   re-derive both numbers rather than inheriting them. */
.hero__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 76% 18%;
}

/* Weighted left, where the headline, lede and buttons sit. The photo's left
   third is the van's gold halftone panel and its own wordmark — white text needs
   this to stay legible, and the .95 stop is also what stops the wrap's
   "Proficient" lettering competing with the H1 sitting on top of it. Measured
   against the current crop: 9.6:1 under the headline, 6.4:1 under the CTA row —
   the CTA row is the tighter of the two because the bottom of the frame is
   sunlit concrete, so check that one first if the photo changes. */
.hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    rgba(22, 40, 26, .95) 0%,
    rgba(22, 40, 26, .88) 34%,
    rgba(22, 40, 26, .62) 62%,
    rgba(22, 40, 26, .32) 100%
  );
}

.hero__inner {
  position: relative;
  padding-top: clamp(56px, 9cqw, 110px);
  padding-bottom: clamp(56px, 9cqw, 104px);
}

.hero__content { max-width: 720px; }

.hero__title {
  font-family: var(--display);
  font-weight: 800;
  color: #fff;
  font-size: clamp(38px, 7.2cqw, 64px);
  line-height: 1.03;
  letter-spacing: -.025em;
  margin: 0;
  text-wrap: balance;
}

.hero__lede {
  color: #c6d2c2;
  font-size: clamp(16px, 1.7cqw, 19px);
  line-height: 1.6;
  margin: 20px 0 0;
  max-width: 600px;
  text-wrap: pretty;
}

.hero__actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }

.btn-gold-lg {
  background: var(--gold);
  color: var(--green);
  padding: 18px 28px;
  border-radius: 7px;
  font-weight: 800;
  font-size: 17px;
}

.btn-outline-lg {
  border: 1.5px solid rgba(255, 255, 255, .35);
  color: #fff;
  padding: 18px 28px;
  border-radius: 7px;
  font-weight: 700;
  font-size: 17px;
}

/* --------------------------------------------------------------------------
   Trust badges
   -------------------------------------------------------------------------- */

.badges { background: var(--green); border-top: 1px solid rgba(255, 255, 255, .08); }

/* The min track is 272px because the widest badge ("20+ years of plumbing
   experience") measures ~265px on one line. The old 160px let auto-fit keep
   four columns long after they had stopped fitting, so between roughly 430px
   and 900px every badge broke across two ragged lines — the key wrapping
   separately from its description, nothing aligning to anything. Sizing the
   track to the content instead makes the grid drop a column rather than squeeze
   one, so it steps 4 → 3 → 2 → 1 and each badge always stays on a single line.

   Desktop is unaffected: at the 1240px container the four tracks come out at
   277px, still above the minimum.

   min(100%, …) is the guard for containers narrower than the track itself —
   without it a sub-272px viewport gets a track wider than the grid and the
   strip overflows horizontally. */
.badges__inner {
  padding-top: 20px;
  padding-bottom: 20px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 272px), 1fr));
  gap: 16px 28px;
}

.badge {
  display: flex;
  align-items: baseline;
  gap: 9px;
  color: #cfd8cb;
  font-size: 14px;
}

/* Never let the gold key break mid-phrase — "20+ years" and "Lic #1144407" both
   split in two before this, which read as separate badges. The track sizing
   above should keep it on one line anyway; this is the backstop for longer copy. */
.badge__key {
  color: var(--gold);
  font-weight: 800;
  font-size: 15px;
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Intro + "first call" card
   -------------------------------------------------------------------------- */

.intro__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: clamp(32px, 4cqw, 64px);
  align-items: start;
}

.intro__title {
  font-family: var(--display);
  font-size: clamp(28px, 3.6cqw, 40px);
  line-height: 1.12;
  letter-spacing: -.02em;
  margin: 14px 0 0;
  max-width: 18ch;
}

.intro__para {
  font-size: 16.5px;
  line-height: 1.7;
  color: var(--body);
  margin: 20px 0 0;
  text-wrap: pretty;
}
.intro__para + .intro__para { margin-top: 16px; }


.steps-card {
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 28px;
}

.steps-card__title { font-family: var(--display); font-size: 20px; font-weight: 700; }

.steps { display: flex; flex-direction: column; gap: 18px; margin-top: 20px; }

.step { display: flex; gap: 16px; align-items: flex-start; }

/* White tile, not cream — .steps-card is already cream, so a cream tile would
   disappear into it. Not overflow:hidden either: the number badge hangs off
   the corner. */
.step__media {
  position: relative;
  flex: none;
  width: 56px;
  height: 56px;
  border-radius: 12px;
  background: #fff;
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
}

.step__icon {
  width: 28px;
  height: 28px;
  fill: var(--green);
}

.step__num {
  position: absolute;
  top: -9px;
  left: -9px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--green);
  color: var(--gold);
  border: 2px solid var(--cream);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: 12px;
}

.step__title { font-weight: 700; font-size: 15.5px; }
.step__desc { font-size: 14.5px; color: var(--muted); line-height: 1.55; margin-top: 3px; }

.btn-green-block {
  display: block;
  text-align: center;
  margin-top: 24px;
  background: var(--green);
  color: #fff;
  padding: 15px;
  border-radius: 7px;
  font-weight: 800;
}

/* --------------------------------------------------------------------------
   Services
   -------------------------------------------------------------------------- */

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}

.services__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
  gap: 18px;
  margin-top: 32px;
}

.service-card {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  min-height: 200px;
  overflow: hidden;
}
.service-card:hover { border-color: var(--gold); }

.service-card__media {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--cream);
}

.service-card__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .5s ease;
}
.service-card:hover .service-card__img { transform: scale(1.05); }

.service-card__body {
  padding: 0 26px 26px;
  display: flex;
  flex-direction: column;
  flex: 1;
}

/* Pulled up over the photo's bottom edge so the icon reads as one designed
   element with the image rather than a second thing stacked above the title.
   The card clips its corners, but .service-card__body is not the clipped box,
   so the overlap survives. */
.service-card__badge {
  width: 46px;
  height: 46px;
  margin: -23px 0 14px;
  /* Positioned so it paints above the photo. Replaced content (the <img>)
     paints in a later phase than plain block backgrounds, so without this the
     image would cover the badge despite coming first in the DOM. */
  position: relative;
  z-index: 1;
  border-radius: 50%;
  background: #fff;
  border: 1px solid var(--line);
  box-shadow: 0 3px 10px rgba(27, 30, 34, .12);
  display: flex;
  align-items: center;
  justify-content: center;
}

.service-card__title {
  font-family: var(--display);
  font-size: 21px;
  font-weight: 700;
  margin: 0 0 10px;
}

.service-card__desc { font-size: 14.5px; color: var(--muted); line-height: 1.55; }

.service-card__more {
  margin-top: auto;
  padding-top: 16px;
  font-weight: 700;
  font-size: 14px;
  color: var(--rust);
}

/* --------------------------------------------------------------------------
   Service area
   -------------------------------------------------------------------------- */

.areas__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: clamp(28px, 4cqw, 56px);
}

.areas__title {
  font-family: var(--display);
  font-size: clamp(26px, 3.4cqw, 36px);
  letter-spacing: -.02em;
  margin: 14px 0 16px;
}

.areas__lede { font-size: 16px; line-height: 1.65; color: var(--body); max-width: 44ch; }

.areas__pills { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; }

.area-pill {
  border: 1px solid #e2ded4;
  background: var(--cream);
  padding: 8px 14px;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 600;
  color: var(--muted-warm);
}
.area-pill:hover { border-color: var(--gold); background: #fff; }

/* The canvas is only faded in once the Maps API has actually drawn something, so
   a missing key, a blocked script, or no JS at all leaves the fallback line
   showing rather than an empty grey box. */
.areas__map {
  position: relative;
  min-height: 300px;
  aspect-ratio: 4 / 3;
  border: 1px solid #e2ded4;
  border-radius: 12px;
  overflow: hidden;
  background: #eceae3;
}

.areas__map-canvas {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity .4s ease;
}
.areas__map--ready .areas__map-canvas { opacity: 1; }

.areas__map-fallback {
  position: absolute;
  inset: 50% 0 auto;
  transform: translateY(-50%);
  margin: 0;
  padding: 22px;
  text-align: center;
  font-size: 15px;
  line-height: 1.6;
  color: var(--muted);
}
.areas__map-fallback a { color: var(--rust); font-weight: 700; }
.areas__map--ready .areas__map-fallback { display: none; }

/* The shop pin — the only marker. The shaded service area is a Maps API
   polygon and takes its colours from --gold in js/main.js, not from here. The
   pin is green because it sits inside that gold wash, where a gold dot would
   disappear.

   Advanced Markers take a DOM node, so the pin is styled here rather than
   configured through the Maps API. The marker's box is the dot and nothing
   else: Google anchors the content's bottom-centre on the coordinate, so
   anything in flow widens the box and drags the dot off its point — a label in
   flow put the shop ~70px west of Carmichael. The label hangs off the side
   absolutely instead, and translateY(50%) drops the box by half its height to
   centre the dot on the point rather than rest it on top. */
.map-pin {
  position: relative;
  transform: translateY(50%);
}

.map-pin__dot {
  display: block;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--green);
  border: 2px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
}

.map-pin__label {
  position: absolute;
  left: 100%;
  top: 50%;
  margin-left: 6px;
  transform: translateY(-50%);
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 700;
  color: var(--ink);
  white-space: nowrap;
  text-shadow: 0 0 3px #fff, 0 0 3px #fff, 0 0 3px #fff;
}

/* --------------------------------------------------------------------------
   Why us
   -------------------------------------------------------------------------- */

/* The crew-and-vans frame sits behind the whole band under a heavy green wash —
   it reads as texture and context, not as a photo you look at. Two gradient
   stops rather than a flat overlay so the top stays darker, where the heading
   sits. Declared after .band--green so it wins the background shorthand;
   background-color stays as the fallback if the file is missing. */
.why-band {
  background-color: var(--green);
  background-image:
    linear-gradient(rgba(22, 40, 26, .86), rgba(15, 28, 18, .79)),
    url("../img/why-band-crew.jpg");
  background-size: cover;
  background-position: center 42%;
  background-repeat: no-repeat;
}

/* No mascot watermark in this band, deliberately — the photograph already has
   one. The van in it carries the elephant on its door at roughly a third the
   frame height, so a ghosted second elephant put two of them in one band, and
   the corner it would have to sit in is the busiest part of the picture. */

.why__title { margin-bottom: 34px; }

.why__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 28px 36px;
}

.why__item { border-top: 2px solid var(--gold); padding-top: 18px; }
.why__item-title { font-family: var(--display); font-size: 18px; font-weight: 700; }
.why__item-desc { font-size: 15px; line-height: 1.6; color: #bfcbbb; margin-top: 8px; }

/* --------------------------------------------------------------------------
   Reviews
   -------------------------------------------------------------------------- */

.reviews__head { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }

.reviews__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 18px;
  margin-top: 30px;
}

.review {
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 26px;
  background: #fff;
}

.review__stars { color: var(--gold); letter-spacing: .1em; font-size: 14px; }

.review__headline {
  font-family: var(--display);
  font-size: 17.5px;
  font-weight: 700;
  line-height: 1.35;
  margin: 12px 0 10px;
}

.review__body { font-size: 15px; line-height: 1.6; color: var(--muted); }
.review__name { font-size: 13.5px; font-weight: 700; color: var(--green); margin-top: 16px; }

/* --------------------------------------------------------------------------
   FAQ
   -------------------------------------------------------------------------- */

.faq__title {
  font-family: var(--display);
  font-size: clamp(26px, 3.4cqw, 36px);
  letter-spacing: -.02em;
  margin: 0 0 26px;
}

.faq-list { display: flex; flex-direction: column; gap: 10px; }

.faq {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}

/* The <h3> exists only to give the question a place in the heading outline —
   the accordion is still the <button> inside it. This is the pattern the ARIA
   authoring practices specify, and it is what lets an answer engine tell that
   the question is a heading rather than a control. All type comes from
   .faq__btn, so this element must add nothing of its own. */
.faq__q { margin: 0; font-size: inherit; font-weight: inherit; }

.faq__btn {
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  padding: 20px 22px;
  font-family: var(--display);
  font-size: 17px;
  font-weight: 700;
  color: var(--ink);
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  gap: 16px;
  align-items: center;
}

.faq__sign { width: 22px; height: 22px; fill: currentColor; color: var(--gold); flex-shrink: 0; }

.faq__answer { padding: 0 22px 22px; font-size: 15.5px; line-height: 1.65; color: var(--muted); }

/* --------------------------------------------------------------------------
   Estimate
   -------------------------------------------------------------------------- */

/* clip, not hidden — same reason as .page. Nothing in this band is sticky, so
   clipping the halftone and the mascot's overhang here is safe. */
.estimate {
  position: relative;
  overflow: clip;
}

/* Runs down from the top edge so the cream FAQ band above breaks into the deep
   green through the screen instead of meeting it on a hard line.

   Still the two-tier flavour, deliberately. The hero was moved to
   .halftone--threshold to match the truck wraps, and the obvious next step is
   to bring this band along — don't. Those dots are opaque gold and want a
   photograph to dissolve into plus corners with no copy in them. This band is
   flat deep green edge to edge with the form sitting in the middle of it, and
   the two-tier tint is what keeps the dots behind the copy rather than in front
   of it. --halftone-strength has no effect on the threshold flavour; it is what
   holds the weight down here. */
.estimate__halftone {
  --halftone-angle: 180deg;
  --halftone-strength: .85;
}

/* Watermark, not an illustration — it reads as tone on the deep green, and the
   red wrench all but disappears at this opacity. Anchored below the bottom edge
   so only the top of the figure shows and .estimate's clip does the cropping. */
.estimate__mascot {
  position: absolute;
  left: clamp(-70px, -4cqw, -16px);
  bottom: clamp(-110px, -8cqw, -44px);
  width: clamp(300px, 42cqw, 540px);
  opacity: .13;
}

/* Positioned so the grid paints over the halftone and the mascot, both of which
   are absolutely positioned earlier in the section. */
.estimate__grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: clamp(32px, 4cqw, 60px);
}

.estimate__title {
  font-family: var(--display);
  font-size: clamp(28px, 3.6cqw, 40px);
  letter-spacing: -.02em;
  margin: 0;
  line-height: 1.1;
}

.estimate__lede {
  color: #bfcbbb;
  font-size: 16.5px;
  line-height: 1.65;
  margin: 16px 0 0;
  max-width: 44ch;
}

.estimate__phone { display: inline-flex; flex-direction: column; margin-top: 28px; line-height: 1.2; }

.estimate__phone-label {
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold);
  font-weight: 800;
}

.estimate__phone-number {
  font-family: var(--display);
  font-size: clamp(28px, 3.6cqw, 38px);
  font-weight: 800;
}

.estimate__panel { background: #fff; border-radius: 12px; padding: 28px; }

.form { display: flex; flex-direction: column; gap: 14px; }
/* main.js swaps the form out for .sent by setting the hidden attribute, and a
   class selector outranks the UA's [hidden] { display: none }. Without this the
   success message just stacks on top of a still-visible form. */
.form[hidden] { display: none; }

.form__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 14px;
}

.form__input,
.form__textarea {
  padding: 14px;
  border: 1px solid var(--line-input);
  border-radius: 7px;
  font-size: 15px;
  font-family: inherit;
  color: var(--ink);
  width: 100%;
}

.form__textarea { resize: vertical; }

.form__submit {
  background: var(--gold);
  color: var(--green);
  border: 0;
  padding: 17px;
  border-radius: 7px;
  font-weight: 800;
  font-size: 16px;
  font-family: inherit;
  cursor: pointer;
}

.form__submit[disabled] { opacity: .6; cursor: progress; }

/* The honeypot. Moved off-screen rather than display:none, because the bots
   worth catching skip fields that are not rendered. */
.form__trap {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Cloudflare Turnstile. Its iframe is a fixed 300px when it does show, so it
   must be allowed to shrink in a narrow panel. Empty, it takes no room. */
.form__turnstile { max-width: 100%; overflow: hidden; }
.form__turnstile:empty { display: none; }

.form__error {
  font-size: 14px;
  line-height: 1.5;
  color: var(--rust);
}
.form__error a { color: inherit; font-weight: 700; }

.form__fineprint { font-size: 12px; color: #8a9086; line-height: 1.5; }
.form__fineprint + .form__fineprint { margin-top: 10px; }
.form__fineprint a { color: #b8bfb4; text-decoration: underline; }

.sent { padding: 40px 8px; text-align: center; }
/* Full colour and full opacity here — the success state is the one place on the
   page with nothing to compete with, so the mascot gets to be the illustration
   rather than a watermark. */
.sent__mascot { width: 148px; margin: 0 auto 16px; }
.sent__title { font-family: var(--display); font-size: 22px; font-weight: 800; color: var(--green); }
.sent__desc { font-size: 15px; color: var(--muted); margin-top: 10px; line-height: 1.6; }

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */

.footer { background: var(--green-darkest); color: #9fae9b; font-size: 14.5px; }

.footer__grid {
  padding-top: 52px;
  padding-bottom: 28px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 36px;
}

.footer__logo { height: 72px; width: auto; display: block; }
.footer__address { margin-top: 16px; line-height: 1.7; }

.footer__col-title {
  color: var(--gold);
  font-weight: 800;
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
  margin-bottom: 12px;
}

.footer__links { display: flex; flex-direction: column; gap: 8px; }

.footer__hours { line-height: 1.8; }
.footer__hours-emphasis { color: #fff; font-weight: 700; }

.footer__phone {
  display: inline-block;
  margin-top: 14px;
  color: #fff;
  font-weight: 800;
  font-size: 19px;
}

.footer__bottom { border-top: 1px solid rgba(255, 255, 255, .09); }

.footer__bottom-inner {
  padding-top: 18px;
  padding-bottom: 18px;
  font-size: 13px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 28px;
}

/* Separated by rules rather than punctuation so "Do Not Sell or Share My
   Personal Information" — wording fixed by the CPRA regulations, hence the
   length — can wrap onto its own line without stranding a bullet. */
.footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 18px;
}

.footer__legal a {
  color: #cfd8cb;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.footer__legal a:hover { color: var(--gold); }

/* The consent.js preferences trigger. A button made to read as one of the
   legal links beside it. No display is set, so the UA [hidden] rule keeps it
   hidden until js/main.js reveals it. */
.footer__consent {
  appearance: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: #cfd8cb;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.footer__consent:hover { color: var(--gold); }

.footer__copyright { color: #8d968a; }

/* --------------------------------------------------------------------------
   Mobile sticky call bar
   -------------------------------------------------------------------------- */

.sticky-cta {
  position: sticky;
  bottom: 0;
  z-index: 60;
  /* Not an even split: "Call now" is short, and the estimate label needs the
     slack to stay on one line down to ~320px. */
  grid-template-columns: 0.8fr 1.2fr;
  gap: 1px;
  background: var(--green-darkest);
  border-top: 1px solid rgba(255, 255, 255, .12);
  transition: opacity .2s ease, visibility .2s;
}

/* Set from js/main.js while the drawer is open or the page is at the very top.
   A fade, not a slide: the bar is sticky and sits in flow at the end of the
   page, so translating it down would push it past the document's end whenever
   it is hidden down there, and add that much blank scroll. visibility takes it
   out of the tab order and the accessibility tree as well. Without JS the class
   never lands and the bar simply stays. */
.sticky-cta--hidden {
  opacity: 0;
  visibility: hidden;
}

.sticky-cta__call,
.sticky-cta__estimate {
  text-align: center;
  padding: 17px 12px;
  font-size: clamp(13px, 3.4cqw, 15px);
  font-weight: 800;
}

.sticky-cta__call {
  background: var(--gold);
  color: var(--green);
}

.sticky-cta__estimate {
  background: var(--green);
  color: #fff;
}

/* --------------------------------------------------------------------------
   Interior pages
   Everything a page that is not the homepage needs and the homepage does not:
   the breadcrumb trail, the compact hero that stands in for the photographic
   one, and the long-form copy column. Rendered by includes/breadcrumbs.php and
   the page templates.
   -------------------------------------------------------------------------- */

/* Deep green rather than a light band because the header's logo hangs proud of
   the header bar by up to 52px (see .logo__img) and lands on whatever is
   underneath — on the homepage that is the dark hero, and the badge is drawn to
   read against it. A pale interior hero would strand it. */
.page-hero {
  position: relative;
  background: var(--green-deep);
  color: #fff;
  /* Top padding has to clear that same overhang, or the breadcrumb trail runs
     underneath the badge at the left edge of the container. */
  padding-top: clamp(56px, 6cqw, 76px);
  padding-bottom: clamp(44px, 6cqw, 72px);
}

.page-hero__title {
  font-family: var(--display);
  font-size: clamp(30px, 4.4cqw, 50px);
  line-height: 1.06;
  letter-spacing: -.025em;
  font-weight: 800;
  margin: 0;
  max-width: 22ch;
}

.page-hero__lede {
  margin: clamp(14px, 1.8cqw, 20px) 0 0;
  max-width: 62ch;
  font-size: clamp(15.5px, 1.4cqw, 18px);
  line-height: 1.62;
  color: #cfd8cb;
}

.page-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: clamp(22px, 2.6cqw, 30px);
}

/* .eyebrow is rust, which is too dark to read on the green. */
.eyebrow--on-dark { color: var(--gold); }

/* --- Breadcrumbs ---
   Sits inside .page-hero, so the colours assume a dark ground. Same trail as
   the BreadcrumbList node — both come from breadcrumb_trail(). */
.breadcrumbs { margin-bottom: clamp(14px, 1.8cqw, 22px); }

.breadcrumbs__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 0;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 13.5px;
}

.breadcrumbs__item {
  display: flex;
  align-items: center;
}

/* Generated rather than typed so the slash never lands in a link's accessible
   name or gets announced as content. */
.breadcrumbs__item + .breadcrumbs__item::before {
  content: "/";
  margin: 0 10px;
  color: rgba(255, 255, 255, .3);
}

.breadcrumbs__link { color: #cfd8cb; }
.breadcrumbs__link:hover { color: var(--gold); }
.breadcrumbs__current { color: rgba(255, 255, 255, .6); }

/* --- Long-form copy ---
   Element selectors rather than a class per tag: this is the one place in the
   stylesheet where the markup is prose, and giving every <p> a class would make
   the page files unreadable. Scoped under .prose so it cannot leak. */
.prose {
  color: var(--body);
  font-size: clamp(15.5px, 1.3cqw, 17px);
  line-height: 1.72;
}

.prose > * + * { margin-top: 1.1em; }

.prose h2 {
  font-family: var(--display);
  font-size: clamp(24px, 3cqw, 32px);
  line-height: 1.15;
  letter-spacing: -.02em;
  font-weight: 700;
  color: var(--ink);
  margin-top: 1.9em;
  margin-bottom: 0;
}

.prose h3 {
  font-family: var(--display);
  font-size: clamp(18px, 2cqw, 21px);
  font-weight: 700;
  color: var(--ink);
  margin-top: 1.7em;
  margin-bottom: 0;
}

/* The first heading in the column already has the section's padding above it. */
.prose > h2:first-child,
.prose > h3:first-child { margin-top: 0; }

/* <ol> as well as <ul>: the sewer-cleaning page runs its three-step process as a
   numbered list, and a browser's default 40px indent next to the 1.15em one
   here reads as a different component. */
.prose ul,
.prose ol { padding-left: 1.15em; }

/* Nested lists — /site-map/ renders the whole site three deep. Without this the
   child list butts straight against its parent's text. */
.prose li > ul,
.prose li > ol { margin-top: .5em; }
.prose li + li { margin-top: .5em; }
.prose strong { color: var(--ink); font-weight: 700; }

.prose a {
  color: var(--rust);
  font-weight: 600;
  border-bottom: 2px solid var(--gold);
}
.prose a:hover { color: var(--gold-dark); }

/* --- Ad opt-out switch (/do-not-sell/) ---
   js/main.js fills the status and labels the button. The button starts
   [hidden] so no-JS visitors, for whom no tag ever loads, see no dead control.
   Neither class sets display, so the UA [hidden] rule still works. */
.optout {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--cream);
  padding: 22px;
}

.optout__status { font-weight: 600; color: var(--ink); }

.optout__btn {
  margin-top: 14px;
  background: var(--green);
  color: #fff;
  border: 0;
  padding: 13px 22px;
  border-radius: 7px;
  font: inherit;
  font-weight: 800;
  cursor: pointer;
}

/* --- Child service grid ---
   The hub-to-child links. Text-only on purpose: .service-card is photographic
   and there is no per-service photography, so reusing it would leave ~50 empty
   image frames across the site. */
.child-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 14px;
  margin-top: 32px;
}

/* Subhead for the grouped form, where one band carries several categories. */
.child-group__title {
  font-family: var(--display);
  font-size: clamp(15px, 1.5cqw, 17px);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: clamp(30px, 3.4cqw, 40px) 0 0;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}
.child-group__title + .child-grid { margin-top: 18px; }

.child-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: clamp(17px, 2cqw, 21px) clamp(18px, 2.2cqw, 22px);
  background: #fff;
  border: 1px solid var(--line);
  border-left: 3px solid var(--gold);
  border-radius: 8px;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(15px, 1.4cqw, 16.5px);
  line-height: 1.3;
}

/* The global `a:hover { color: var(--gold) }` would paint the whole card label
   gold, which is unreadable on white. Hold the label and move the arrow. */
.child-card:hover { color: var(--ink); border-color: var(--gold); }

.child-card__arrow {
  flex: none;
  color: var(--rust);
  transition: transform .25s ease;
}
.child-card:hover .child-card__arrow { transform: translateX(4px); }

/* A pulled-out call to action between prose blocks — the "call us at …" line
   that the source copy repeats every few sections. */
.prose-cta {
  margin-top: 1.6em;
  padding: clamp(18px, 2.2cqw, 24px);
  background: var(--cream);
  border: 1px solid var(--line);
  border-left: 3px solid var(--gold);
  font-size: clamp(15.5px, 1.3cqw, 17px);
  line-height: 1.6;
  color: var(--ink);
}

/* --- Inline photography ---
   The one photograph on a service detail page, rendered by
   includes/service-figure.php between two prose sections.

   It is optional by design: docs/photo-library.md maps a real frame to about
   two dozen of the service pages and nothing at all to the pumps, the tankless
   heaters, commercial, trenchless or CIPP. Those pages stay text-only, for the
   same reason .child-card is text-only - a photograph of a different job is
   worse than no photograph.

   Full width of the narrow column so it breaks the copy rather than floating in
   it. Both of these rules match .prose > * + * on specificity (one class each),
   so they only win by sitting below it in the file. Keep them here. */
.service-figure {
  margin: clamp(30px, 3.4cqw, 42px) 0 0;
}

/* Every export is 1600x900 and the <img> carries those as attributes, but the
   ratio is restated here so the box is reserved before the file starts to load
   - otherwise the caption and everything under it jump on arrival. */
.service-figure__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 12px;
  background: var(--cream);
}

/* The gold rule rather than centred italics: .prose-cta and .child-card both
   mark an aside that way, so the caption reads as part of the page's system
   instead of a browser default. */
.service-figure__caption {
  margin-top: 14px;
  padding-left: 14px;
  border-left: 3px solid var(--gold);
  font-size: clamp(13.5px, 1.15cqw, 14.5px);
  line-height: 1.55;
  color: var(--muted);
}

/* --- Contact details ---
   The three ways to reach the business, on /contact-us/. Card, not prose,
   because a phone number someone is looking up in a hurry should not be a
   sentence. Same white-card-with-a-gold-edge treatment as .prose-cta and
   .child-card so it reads as part of the same family.

   The whole card is the link where there is something to link to (tel:, the
   service area), so the tap target is the card rather than the text inside it. */
.contact-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
  margin-top: clamp(28px, 3.2cqw, 38px);
}

.contact-card {
  display: block;
  background: #fff;
  border: 1px solid var(--line);
  border-left: 3px solid var(--gold);
  border-radius: 8px;
  padding: clamp(18px, 2.2cqw, 24px);
}

/* The global `a:hover { color: var(--gold) }` would wash out the whole card.
   Hold the text and move the border, the way .child-card does. */
a.contact-card:hover { color: inherit; border-color: var(--gold); }

.contact-card__label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--display);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

.contact-card__icon {
  flex: none;
  width: 16px;
  height: 16px;
  fill: currentColor;
  color: var(--gold);
}

.contact-card__value {
  margin-top: 10px;
  font-family: var(--display);
  font-size: clamp(17px, 1.7cqw, 20px);
  font-weight: 700;
  line-height: 1.3;
  color: var(--ink);
}

.contact-card__note {
  margin-top: 7px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted);
}

/* --------------------------------------------------------------------------
   Desktop / mobile switch
   -------------------------------------------------------------------------- */

.nav-desktop { display: flex; }
.nav-mobile  { display: none; }
.sticky-cta  { display: none; }

@media (max-width: 900px) {
  .nav-desktop { display: none; }
  .nav-mobile  { display: flex; }
  .sticky-cta  { display: grid; }
  /* The mega menu is a desktop-only affordance; the drawer covers mobile. */
  #mega.panel.open { display: none; }
}

/* --------------------------------------------------------------------------
   Motion
   Everything here is opt-in twice over: the reveal state only applies once JS
   has added .motion on <html> (so a JS failure can never leave the page
   invisible), and the whole block is inside prefers-reduced-motion.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  .motion .reveal {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity .55s ease, transform .55s cubic-bezier(.22, .61, .36, 1);
  }

  .motion .reveal.is-in {
    opacity: 1;
    transform: none;
  }

  /* Stagger by position within the row. Done with nth-child rather than a JS
     delay so no inline styles are needed. */
  .motion .reveal:nth-child(2) { transition-delay: 70ms; }
  .motion .reveal:nth-child(3) { transition-delay: 140ms; }
  .motion .reveal:nth-child(4) { transition-delay: 210ms; }
  .motion .reveal:nth-child(5) { transition-delay: 280ms; }
  .motion .reveal:nth-child(6) { transition-delay: 350ms; }
}
