/* ============================================================
   NJ INDOOR OUTDOOR — v2.css
   Hero + header, rebuilt 1:1 from coconutcleaningco.com.

   Numbers marked "reference" were read off the live site with
   getComputedStyle at a 1905px viewport. Numbers marked
   DEVIATION are deliberate departures, each with its reason.
   ============================================================ */

:root {
  /* --- Reference palette, measured --- */
  --bar-bg:      #EEEEEE;   /* .ast-above-header-bar background        */
  --bar-line:    #EAEAEA;   /* its 1px bottom border                   */
  --bar-text:    #7D7D7D;   /* .ast-custom-button colour               */
  --nav-text:    #737373;   /* .main-header-menu > li > a              */
  --nav-hover:   #424242;   /* same, :hover                            */
  --header-bg:   #FFFFFF;
  --surface:     #F1F5F7;   /* the reference's section tint            */

  /* DEVIATION — the reference sets every heading and paragraph in
     #737373. Across a whole page that reads as washed out and gives
     the brand nothing to hold onto, so headings go to a near-black
     navy and body copy to a darker grey. The grey is also a
     contrast win: #737373 on white is 4.6:1, #4C5265 is 7.4:1. */
  --head-ink:    #0B1226;   /* headings                                */
  --body-ink:    #4C5265;   /* paragraphs                              */
  /* Highlight block. The brand blue, carrying WHITE text rather than
     dark. Dark-on-blue measured fine on paper but read as muddy at
     900 weight, where the counters close up. White on #2F6FE5 is
     4.64:1 — past the 3:1 large-text floor with room, and it makes
     the highlight match every button on the page instead of
     introducing a second blue. */
  --hl-blue:     var(--blue);

  /* DEVIATION 1 — the reference's button blue is #1CA9FB (a cyan).
     Swapped for NJ Indoor Outdoor's own accent, already established
     in v1's assets/site.css and taken off the logo's royal blue. */
  --blue:        #2F6FE5;
  /* Sampled from the green ellipse in the NJ logo — the purest green
     pixel in logo.png. Hero accent only for now; the section headings
     keep the blue marker block. */
  --accent-green: #71B745;
  --blue-line:   #2563D6;
  --blue-hover:  #2560CC;

  /* DEVIATION 2 — the reference headline is Mont Heavy (Fontfabric,
     licensed). Montserrat 900 is the closest freely-licensed
     geometric heavy, and the reference already uses Montserrat for
     body/nav so the pairing holds. */
  --font: 'Montserrat', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* DEVIATION 3 — header thickness at 75% of the reference.
     Reference: 51px + 105px = 156px. Here: 38 + 79 = 117px. */
  --bar-h:    38px;
  --header-h: 79px;

  /* Logo stays at its full 62px even though the bar came down —
     the mark is the one thing that should not shrink with it. */
  --logo-h: 62px;

  /* Vertical breathing room inside the hero. Scales off viewport
     HEIGHT so a short screen gets a tighter hero rather than an
     overflowing one. */
  --hero-gap: clamp(16px, 5vh, 60px);

  /* How far the hero COPY sits down the film — the eyebrow, headline,
     lede, CTAs, Google badge and the four trust badges, i.e. the whole
     left column. Nothing else moves: the announcement bar, the nav,
     the film band's height and every size and gap in the hero are
     untouched.

     Zero above 960, where .hero__copy already carries its own
     `top: 44px` nudge for the same purpose. The phone value is set
     with the band layout further down.

     It is a token because the phone breakpoints each carry their OWN
     measured offset against the film's seam — +8, -184, -210, -236 and
     a clamp — and those numbers are calibrated to where the footage is
     dark enough to take white type. Adding one variable into all of
     them shifts the block without disturbing any of those
     relationships; editing five literals by hand would. */
  --hero-drop: 0px;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* ============================================================
   NO SIDEWAYS SCROLL, STRUCTURALLY

   `overflow-x: hidden` on body alone — which is what this file used
   to rely on — does not do the job, for two reasons:

     · Overflow PROPAGATES from body to the viewport when html is
       `visible`. The viewport takes the hidden, and body's own
       overflow computes back to `visible` — so body never actually
       clips anything. It only suppresses the scrollbar.
     · iOS Safari lets you pan horizontally anyway when content
       genuinely sticks out.

   `clip` is the right tool and the reason is specific: unlike
   `hidden`, it does NOT create a scroll container, so the sticky
   header still pins. `hidden` here would silently break it.

   This is a backstop, not the fix. Overflow is a bug every time —
   something is wider than its parent and should not be. Fix the
   cause; this only stops a missed one from reaching a phone.
   Check with:  document.documentElement.scrollWidth  vs  clientWidth

   TWO declarations, and the order matters:

     · `hidden` on the ROOT is the floor. Safari only shipped
       `overflow: clip` in 16.0, so on anything older a lone `clip`
       is an invalid declaration, dropped silently, leaving no guard
       whatsoever. On the root element `hidden` is safe for the
       sticky header — the root's overflow propagates to the viewport
       and the root's own used value becomes `visible`, so no scroll
       container is created on the element itself.
     · `clip` then upgrades it where supported, because `hidden`
       suppresses the scrollbar without actually clipping (see the
       propagation note above) and iOS will still pan.
   ============================================================ */
html { overflow-x: hidden; }
@supports (overflow: clip) {
  html, body { overflow-x: clip; }
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.857;
  color: var(--body-ink);
  background: #FFFFFF;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; display: block; }
a   { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--blue); color: #fff; padding: 12px 18px; border-radius: 0 0 6px 0;
}
.skip-link:focus { left: 0; }

:where(a, button):focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ------------------------------------------------------------
   .hl — marker-pen highlight on the key phrase in a heading

   Modelled on the treatment at 70181.preview.arsenicdigital.com:
   a solid block sitting behind the words, rotated a degree and a
   half so it reads as a hand-drawn swipe rather than a rectangle,
   with the text knocked out dark on top. The block is clipped to
   zero width and wipes open when the section scrolls into view —
   v2.js adds .in.

   The blue is a mid tone, lighter than the brand blue: the block
   has to carry near-black text at 7:1, which #2F6FE5 cannot do.
   ------------------------------------------------------------ */
/* Text is dark until the highlight exists, then white on top of it.
   The switch is deliberately NOT transitioned. A delayed colour
   fade is a trap: anything that throttles or interrupts it — a
   background tab, a dropped frame — leaves white text on a white
   section, i.e. an invisible word. Flipping it in the same instant
   as .in means the only three possible states are all legible:
   dark-on-white before, white-on-blue after, and during the sweep
   the word paints in behind its own block. */
.hl {
  position: relative;
  display: inline-block;
  z-index: 0;
  padding: 0 .14em;
  color: var(--head-ink);
  white-space: nowrap;
  line-height: 1;
}
.hl.in,
.no-js .hl { color: #FFFFFF; }

/* Two things here stop the block from straying onto neighbouring
   lines, which the reference's own values did on our headings:

   1. The vertical bleed is a third of theirs. Their headings run a
      normal line-height; the Coconut-derived ones here go as tight
      as 0.8, so lines nearly touch and there is no room to spill
      into.
   2. Rotating about the CENTRE rather than the left edge. About the
      left edge, a 390px block at -1.6deg swings its far end nearly
      11px — straight into the line above. About the centre at
      -1.2deg it is ±4px, split evenly, and the tilt still reads. */
.hl::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: -.05em -.06em -.04em;
  background: var(--hl-blue);
  transform: rotate(-1.2deg);
  transform-origin: 50% 50%;
  clip-path: inset(0 100% 0 0);
  transition: clip-path .72s cubic-bezier(.16, 1, .3, 1) .12s;
}
.hl.in::before { clip-path: inset(0 0 0 0); }

/* No JS, no observer — show the block rather than hide the effect. */
.no-js .hl::before { clip-path: inset(0 0 0 0); }

@media (prefers-reduced-motion: reduce) {
  /* Block and white text both present from the start, no sweep. */
  .hl { color: #FFFFFF; }
  .hl::before { transition: none; clip-path: inset(0 0 0 0); }
}


/* ============================================================
   ANNOUNCEMENT BAR — static, NOT sticky

   Sits at the very top of the document in normal flow and scrolls
   away with the page. It never overlays the hero, so it keeps the
   reference's own colours at all times.

   Reference: 51px, #EEEEEE, 1px solid #EAEAEA, holding a pill of
   15px/700 uppercase #7D7D7D at padding 10px 20px, radius 30px.
   Scaled to 38px; the pill's type and padding come down with it so
   it still sits centred in the bar.
   ============================================================ */

.above-header {
  position: relative;
  z-index: 70;                  /* above the hero's negative margin */
  height: var(--bar-h);
  background: var(--bar-bg);
  border-bottom: 1px solid var(--bar-line);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Three items now, not one: location, Call Now, email. space-between
   puts the two restored live links on the flanks with Call Now held in
   the middle. */
.above-header { justify-content: space-between; gap: 16px; padding: 0 20px; }

.above-header__side {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1;
  color: var(--bar-text);
  opacity: .82;
  white-space: nowrap;
  transition: opacity .2s linear, color .2s linear;
}
.above-header__side:hover { opacity: 1; color: var(--nav-hover); }
.above-header__side .above-header__ico { width: 14px; height: 14px; fill: currentColor; }
.above-header__side--mail { text-transform: none; }

/* 38px of bar cannot carry three items on a phone. The flanks drop and
   Call Now re-centres; the footer keeps both links at full size. */
@media (max-width: 900px) {
  .above-header { justify-content: center; }
  .above-header__side { display: none; }
}

.above-header__btn {
  display: inline-block;
  padding: 6px 16px;
  border-radius: 30px;
  background: transparent;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  text-transform: uppercase;
  color: var(--bar-text);
  transition: color .2s linear;
}
.above-header__btn:hover { color: var(--nav-hover); }
.above-header__emoji { margin-right: 6px; }


/* ============================================================
   NAV BAR — sticky

   position: sticky means it sits in normal flow directly under the
   announcement bar at scroll top, then pins to y=0 on its own once
   the announcement bar has scrolled past. No JS needed for the
   pinning; v2.js only handles the colour state.

   Transparent over the hero at scroll top; .is-solid fades in the
   reference's real colours from the first pixel of scroll.
   ============================================================ */

.site-header {
  position: sticky;
  top: 0;
  z-index: 60;
  transition: box-shadow .25s ease;
}
.site-header.is-solid { box-shadow: 0 2px 14px rgba(0, 0, 0, .07); }


/* ============================================================
   HEADER — main bar
   Reference: 105px tall, white, 35px side padding. Scaled to 79px.
   ============================================================ */

.main-header {
  position: relative;
  /* The hero below is a positioned element (it carries the photo
     scrims), so without a z-index here the absolutely-positioned
     mobile nav panel paints underneath it. */
  z-index: 50;
  height: var(--header-h);
  background: transparent;
  padding: 0 35px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  transition: background .25s ease;
}
.is-solid .main-header { background: var(--header-bg); }

/* DEVIATION 4 — the reference logo is a 250x35 wordmark (7:1).
   NJ's mark is 800x346 (2.3:1); at 35px tall it would render 81px
   wide and be unreadable. Locked to a height that tracks the bar. */
.brand { position: relative; display: block; }
.brand img { width: auto; height: var(--logo-h); transition: opacity .25s ease; }
.brand__dark { position: absolute; inset: 0; opacity: 0; }
.is-solid .brand__light { opacity: 0; }
.is-solid .brand__dark  { opacity: 1; }

/* There is no scrim behind the header any more, so the white mark has
   to separate itself. On phones especially it lands on open sky — the
   brightest part of the frame — and a single soft 12px shadow washed
   straight out there. Same structure as the headline: a tight pass to
   cut the edge, a wide pass to lift it off the sky. */
.brand__light {
  filter:
    drop-shadow(0 1px 1px rgba(0, 0, 0, .55))
    drop-shadow(0 2px 16px rgba(0, 0, 0, .65));
}
.is-solid .brand__light { filter: none; }

.primary-nav {
  display: flex;
  align-items: center;
  gap: 30px;
}

.primary-nav__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
}

.primary-nav__list > li { position: relative; }

.primary-nav__list > li > a {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 0 30px;              /* reference nav link padding */
  font-size: 17px;              /* reference nav link size    */
  font-weight: 400;
  text-transform: capitalize;
  color: #FFFFFF;

  /* Two soft passes, up from one at .5. The nav floats over the film in
     its transparent state, and the hero crop was pushed up and in so the
     copy would land on the truck's body — which put CLOUD behind this
     row where there used to be mid-blue sky. Measured on the rendered
     page it fell to 1.06:1, the weakest type on the site.

     No offset on either pass, so this is a glow rather than the tight
     outline the hero headline used to carry: the film immediately
     around each glyph darkens and fades, with no edge to see. It cannot
     make 1.06:1 into a passing number — nothing drawn on the TYPE can,
     WCAG scores the backdrop — but it is the difference between reading
     the nav at a glance and squinting at it.

     Left as a known weak point rather than solved: solving it properly
     means a gradient down from the top of the header, and a tint over
     the film is exactly what this hero is not allowed to have. Zeroed
     in .is-solid below, where the bar has a real background. */
  text-shadow:
    0 0 12px rgba(8, 14, 28, .85),
    0 1px 22px rgba(8, 14, 28, .65);
  line-height: var(--header-h);
  transition: color .25s ease, text-shadow .25s ease;
}
.is-solid .primary-nav__list > li > a { color: var(--nav-text); text-shadow: none; }

/* Transparent state: the reference's #424242 hover would vanish
   against the photo, so hover reads as a dim instead. Solid state
   uses the reference's own hover colour. */
.primary-nav__list > li > a:hover          { opacity: .72; }
.is-solid .primary-nav__list > li > a:hover { opacity: 1; color: var(--nav-hover); }

.caret { flex: none; margin-top: 2px; transition: transform .2s ease; }
.has-sub:hover .caret { transform: rotate(180deg); }

.sub-menu {
  position: absolute;
  top: 100%;
  left: 20px;
  min-width: 220px;
  margin: 0;
  padding: 8px 0;
  list-style: none;
  background: #fff;
  border: 1px solid var(--bar-line);
  border-radius: 6px;
  box-shadow: 0 12px 28px rgba(0, 0, 0, .12);
  opacity: 0;
  visibility: hidden;
  transform: translateY(6px);
  transition: opacity .18s ease, transform .18s ease, visibility .18s;
  z-index: 40;
}
.has-sub:hover .sub-menu,
.has-sub:focus-within .sub-menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
.sub-menu a {
  display: block;
  padding: 9px 22px;
  font-size: 16px;
  color: var(--nav-text);
  text-shadow: none;
  transition: color .15s linear, background .15s linear;
}
.sub-menu a:hover { color: var(--nav-hover); background: #F7F7F7; }

/* BOOK NOW pill — reference values, unscaled: 15px/700 uppercase,
   padding 10px 40px, radius 30px. It reads the same on the photo
   as on white, so it does not change between states. */
.header-cta {
  display: inline-block;
  padding: 10px 40px;
  border-radius: 30px;
  background: var(--blue);
  color: #FFFFFF;
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  transition: background .2s linear;
}
.header-cta:hover { background: var(--blue-hover); }

.nav-toggle {
  display: none;
  width: 44px; height: 38px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, .7);
  border-radius: 6px;
  /* Reads against open sky, which .18 did not. This is the control's
     own chrome, not an overlay on the film. */
  background: rgba(11, 18, 38, .32);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  transition: background .25s ease, border-color .25s ease;
}
.is-solid .nav-toggle { background: #fff; border-color: var(--bar-line); }

.nav-toggle__bar {
  display: block;
  width: 20px; height: 2px;
  background: #FFFFFF;
  border-radius: 2px;
  transition: transform .2s ease, opacity .2s ease, background .25s ease;
}
.is-solid .nav-toggle__bar { background: var(--nav-text); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }


/* ============================================================
   HERO

   Reference geometry that survives unchanged:
     background  cover, centred, no full-frame overlay
     rail        1361px text column, centred
     h1          900 weight, 91px ceiling, line-height .8, -1px
     subhead     17px / 1.3 + text-shadow 1px 1px 28px rgb(30,0,0)
     button      18px/700, padding 12px 24px, radius 50px

   DEVIATION 5 — the reference hero is a fixed 80vh sitting BELOW a
   156px header. Here it is one exact fold (100svh) with the header
   floating on top, and every size inside it is driven by
   min(vw, vh) so the content scales down on short screens instead
   of overflowing them.
   ============================================================ */

.hero {
  position: relative;
  overflow: hidden;
  isolation: isolate;

  /* Pulled up under the sticky nav bar so the film runs behind it
     while it is transparent. */
  margin-top: calc(-1 * var(--header-h));

  /* Underneath the film, so there is never a flash of white before the
     first frame paints. A two-stop gradient rather than a flat fill:
     on phones the video becomes a band and this is exposed as a large
     dark area, and a perfectly flat dark field is what the display
     pipeline spatial-dithers into a visible diagonal. The stops are
     only a few luminance steps apart — it does not need to read as a
     gradient, only to stop being flat. See .hero__grain below. */
  background: linear-gradient(180deg, #131B2E 0%, #0F172A 100%);
}

/* Second half of the same treatment. Sits above the film but under
   the copy, so on desktop (where the film covers everything) it is
   imperceptible, and on phones it textures the exposed dark area. */
.hero__grain {
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: .035;
  mix-blend-mode: overlay;
  pointer-events: none;
}

/* Background film. object-fit: cover crops it to the fold the same
   way a background-image would. */
.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;

  /* 58%, not centred. The hero is ~2.2:1 and the clip is 16:9, so
     cover crops roughly a fifth of the height; biasing the window
     down keeps the whole truck in frame instead of shearing its
     wheels off, while still cropping the empty road in front of it. */
  object-position: 50% 58%;
  z-index: -2;
  pointer-events: none;
}

/* ZOOM AND SHIFT — desktop only.

   object-position alone cannot do this. The hero is about 1.85:1 and
   the clip is 1.78:1, so cover scales to width and crops only ~40px of
   height: there is nothing spare in the other axis, and object-position
   X has literally no effect at these sizes. To move the picture at all
   the element has to be bigger than the box it sits in.

   So it is oversized 30% and pulled up and left. Two things come out of
   that, and both were asked for:

   1. The picture shifts LEFT. The element is 130% wide and offset
      -24%, so the frame's left eighth runs off-screen and the cab and
      body come inboard instead of sitting hard against the right edge.
      The empty road that used to fill the left third is gone.

   2. The copy lands on the TRUCK. Pulled up 21%, the body rises into
      the top half of the fold, so the headline and lede sit on dark
      blue-grey steel rather than on open sky. This is what pays for
      dropping the tight shadow passes — legibility comes from what is
      behind the type, not from a plate drawn around it.

   The numbers are not free choices — they were solved for. Measuring
   the rendered page, the truck's top edge sits at 50% of the frame's
   height, and the headline occupies 30-45% of the fold. Putting the
   one behind the other means the offset and the zoom are locked
   together: to land the truck's edge at hero-height y, the element
   needs top = y - H/2 at scale H, and it still has to cover the box,
   which needs top >= 100 - H. Solve those together and y = 27.5%
   requires H = 145. At 130 the truck edge landed at 44%, which is why
   the first pass left the headline on sky and only the buttons and the
   second lede line on steel.

   The cost is resolution: a 1920 clip drawn at 145% is upscaled about
   1.45x on a 1920 screen. It holds because this is a locked-off shot,
   the enlarged region is a flat truck body with no fine detail, and it
   is a background behind type rather than the subject of the page. It
   is nonetheless the ceiling — past this, re-encode the clip already
   cropped instead of scaling it here.

   145 is also the exact coverage limit for a -45% offset: the frame's
   bottom edge lands on the fold's bottom edge with nothing to spare.
   Raising the offset without raising the zoom will show background
   through the bottom of the hero.

   Scoped above 960px. Below that the hero is a band, the film is
   already scaled up hard to fill it, and .hero__video is re-laid-out
   entirely by the rule further down. */
@media (min-width: 961px) {
  /* Nudged down so the eyebrow clears the truck's top edge too. A
     visual offset, not a layout one: `top` on a relative box moves what
     is painted without changing the grid track, so the fold's height
     and the form's centring are untouched. */
  .hero__copy { position: relative; top: 44px; }

  .hero__video {
    width: 152%;
    height: 152%;
    left: -24%;
    top: -52%;
    right: auto;
    bottom: auto;
  }
}

/* SCRIM — behind the copy only.

   This block used to say NO SCRIM, on the strength of an ffmpeg
   measurement of the clip's left 45%: Y=108.5 of 255, stable across
   the loop, "about 5.3:1". That number was not wrong, it was measuring
   the wrong thing. It measures the FILE. What the visitor reads is the
   LAYOUT, and the two only agree at one viewport.

   The reason is object-fit. At 1920 wide the hero is ~2.2:1 against a
   16:9 clip, so cover throws away a fifth of the frame and the copy
   lands low, on the truck's body. At 1440x900 the hero is ~1.8:1, cover
   throws away almost nothing, and the same copy lands on open sky. Same
   file, same 5.3:1 on paper, completely different pixels.

   tools/luma-v2.js measures the layout instead: it takes the real
   bounding box of each piece of hero copy, hides the copy, steps the
   video through its loop, and reads the p90 luminance actually behind
   the glyphs at five viewport sizes. Untinted, that reported 9 of 20
   checks passing — the headline down at 1.30:1 on desktop against the
   3:1 it needs, and the lede at 1.18:1 against 4.5:1. The clip is not
   at fault and swapping it does not fix it; a bright, evenly lit clip
   is exactly what the reference look asks for, and open sky is bright.

   A gradient scrim behind the copy column WAS built and measured, and
   it worked: 19 of 20, the one remaining miss being the green accent
   at laptop width. It was then removed by explicit decision — the film
   is to read clear, and a tint over it is not wanted at any strength.
   Do not re-add one without asking. Recorded here so the next person
   does not rediscover the same numbers and reach for the same fix.

   What is left carrying the type is the glyph shadow on .hero__title
   and .hero__accent below, which WCAG does not score but the eye does.
   If this needs revisiting without a tint, the levers are the ASSET and
   the CROP — a clip whose left third is genuinely dark, or an
   object-position that puts the copy on the truck's body rather than on
   sky. tools/luma-v2.js measures either in about two minutes. */

.hero__inner {
  position: relative;
  z-index: 1;

  /* Exactly the rest of the fold: the full small-viewport height
     minus the announcement bar sitting above it. svh = the viewport
     at its SMALLEST (mobile browser chrome fully expanded), so the
     hero can never be clipped and never resizes mid-scroll the way
     dvh does. The first line is the fallback for older engines. */
  min-height: calc(100vh - var(--bar-h));
  min-height: calc(100svh - var(--bar-h));

  /* 1720, not the 1440 the rest of the page uses. Every other section
     is a measured column of text and cards, and 1440 is right for
     those. The hero is not that — it is one line of display type over
     a full-bleed film, and at 1440 on a 1920 screen the copy started
     260px in from the left edge while the film ran to both edges. That
     reads as a centred block floating on the footage rather than as a
     headline set ON it. At 1720 the same copy starts at 120px, close
     enough to the edge to belong to the frame.

     The cap stays, rather than going full-bleed: past ~1720 the gap
     between the headline and the form opens far enough that the two
     stop reading as one composition. */
  max-width: 1720px;
  margin: 0 auto;
  padding: calc(var(--header-h) + var(--hero-gap)) 20px var(--hero-gap);

  display: grid;

  /* The form column was a flat 420px. It is now fluid to 600 — half
     again as wide at the top end, which is the "1.5x" it was asked to
     grow by, with .hero__form scaling its own padding and controls to
     match so it reads bigger rather than just wider.

     clamp, not a second flat number, because 600 only fits above about
     1500px. Below that a fixed 600 would eat the left column and push
     the headline back to four short lines, which is the problem the
     widening was meant to solve. At 1180 this resolves to ~448, near
     the old value. */
  grid-template-columns: minmax(0, 1fr) minmax(0, clamp(420px, 38vw, 600px));
  gap: 48px;
  align-items: center;
}

/* --- left: copy --- */

/* 780px, not a ch-based cap: at 42ch the headline broke into five
   short lines, which is the "too much going on" the centred version
   had.

   The history is worth keeping, because each number was bought by a
   different constraint. 620 ran the headline to four lines and split
   "in" from "Central NJ". 680 came with the badge row — the four
   badges need 665px and were wrapping 3 + 1, stranding "Same-Day
   Service" on its own line — and it fixed the headline at the same
   time, three lines with the accent phrase intact.

   780 is the current number and it is set by the LEDE, not the
   headline: "Hauling out of Aberdeen since 2000..." wants 644px at
   17px and 678px at the 18px cap, and it is meant to sit on one line
   (see .hero__text). 780 clears that with room for the glyph shadows.

   The old note here said widening past ~700 crowds the form. That was
   true against a 1440px .hero__inner, where the left column had only
   932px to give at that width. At 1720 it has 1212px, so 780 leaves
   more clearance than 680 did before. Do not raise this without
   raising .hero__inner too — the two numbers move together.

   RAISED AGAIN to 900. At 780 the headline stopped about 370px short
   of the form and left a dead vertical strip down the middle of the
   fold, which read as the copy being huddled in the corner rather than
   laid across the frame. 900 runs the two headline lines out into that
   strip and closes it.

   It is a cap, not a width — the grid column is the real constraint,
   and below about 1500px the column is narrower than 900 and wins. So
   this only takes effect on the wide screens where the gap existed. */
.hero__copy { max-width: 900px; }

/* Cut back hard from the old centred block. The headline sizes off
   min(vw, vh) so HEIGHT governs on short and landscape screens,
   which is what stops the form being pushed off the fold. */
.hero__title {
  margin: 0;
  font-family: var(--font);
  font-weight: 900;
  font-size: clamp(30px, min(4.2vw, 7vh), 62px);
  line-height: 1.04;
  letter-spacing: -1px;
  color: #FFFFFF;

  /* ONE soft shadow, not the three-pass stack this had.

     That stack was 0 1px 2px / 0 2px 14px / 0 0 42px, and each pass was
     defensible on its own: a hard 2px edge for pale asphalt, a mid pass
     for mid-tones, a wide pass for bright patches. Together they read
     as an outline with a dark cloud behind it — worst on the green
     accent, where the 42px pass pooled into a visible smudge that made
     the word look like it had been pasted on.

     The tight pass is the one that reads as an outline, so it is gone.
     What is left is a single wide, low-opacity shadow: it separates the
     glyph from the film the way depth does, and at 28px of blur there
     is no edge for the eye to find. Slightly blue-black rather than
     pure black, matching the charcoal the rest of the page grounds on,
     because pure black over blue sky goes grey and dirty.

     This is a deliberate trade. The three-pass stack measured better;
     this one looks better, and the film being clear is the point. What
     buys the contrast back is the CROP — see the zoom on .hero__video
     below, which puts this type on the truck's body instead of on open
     sky. Re-run tools/luma-v2.js if either changes. */
  text-shadow: 0 2px 28px rgba(8, 14, 28, .55);
}

/* Sampled from the green ellipse in logo.png (#71B745), so the accent
   is a brand colour rather than an invented one.

   This used to carry a HEAVIER tight-shadow stack than the white type,
   on the argument that the halo was load-bearing: green-on-film
   measures about 2.2:1 raw, and darkening the pixels immediately around
   each glyph to roughly Y=49 took the local ratio past 5:1.

   The argument was sound and the result still looked wrong. Three
   passes pooling under a 62px word produced a dark blob roughly the
   shape of the text, so "Central NJ" read as pasted onto the film
   rather than set on it — the single most-noticed problem in the hero.

   It now takes the same single soft shadow as .hero__title, one step
   stronger because green sits lower against sky than white does. The
   contrast that the halo used to buy is bought by the crop instead —
   the zoom on .hero__video puts this phrase on the truck's dark body.
   That is the right place to spend it: the type stays clean and the
   film stays untouched. */
.hero__accent {
  color: var(--accent-green);
  text-shadow: 0 2px 26px rgba(8, 14, 28, .62);
}

/* Eyebrow. Mirrors .about__eyebrow's geometry — same weight, same .1em
   tracking, same uppercase — so the two read as one device, but sized
   off vh like the rest of the hero rather than off vw, because the hero
   is height-constrained where the About section is not. White rather
   than accent green, for the contrast reason given in index.html. */
/* Sized UP from 12-15px, and this is a legibility fix rather than a
   styling preference. The eyebrow is the topmost thing in the copy, so
   it is the one element the crop cannot rescue: pushing the truck up
   far enough to sit behind the headline still leaves this line at or
   above the body's top edge, and at 12px it was measuring 1.2:1.

   Two things move it. Size and weight, because 800-weight type at
   18.66px or over is LARGE text to WCAG and needs 3:1 rather than
   4.5:1 — and because bigger simply reads better on a moving
   background. And a glow: 14px of blur at high opacity with no offset,
   which is the opposite device to the tight 1-2px pass that made the
   old headline look outlined. There is no edge in it; it darkens the
   film immediately around the glyphs and fades out. */
.hero__eyebrow {
  /* inline-block so the box hugs the words. As a block <p> it spanned
     the full 900px column, which meant two-thirds of it was empty space
     — and that empty space was over cloud, so it read as a contrast
     failure for pixels no glyph ever touches. Shrink-wrapping makes the
     measurement describe the ink, which is the only thing anyone has to
     read. */
  display: inline-block;
  margin: 0 0 14px;
  font-size: clamp(15px, 1.75vh, 19px);
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #FFFFFF;
  text-shadow:
    0 0 14px rgba(8, 14, 28, .92),
    0 1px 26px rgba(8, 14, 28, .7);
}

.hero__text {
  margin: 18px 0 0;

  /* 62ch, not the 40ch this had. 40ch is the right measure for a
     paragraph, and this is not a paragraph — it is a single 76-character
     line that says where the company works, how long it has worked, and
     what it carries. Broken across two lines it read as a block of body
     copy under the headline; on one line it reads as a strapline, which
     is what it is.

     In ch rather than px so it tracks the font, which is sized off vh
     and moves between 15px and 18px: 644px at 17px, 678px at the cap.
     62ch covers the cap with margin and still sits inside .hero__copy.

     This holds to roughly 1180px wide. Below that the left column is
     narrower than the line needs and it wraps to two — which is fine,
     and is what the ≤960 rule already assumes when it releases the cap
     entirely. */
  max-width: 62ch;
  font-size: clamp(15px, 1.9vh, 18px);
  line-height: 1.55;

  /* Full white, not .88 — at body size the alpha was costing more
     legibility over untinted footage than it was buying in softness.

     Single soft shadow, matching the headline. At this size the old
     tight 2px pass was not reading as an outline the way it did on the
     display type, but it was the same device, and leaving it here would
     have left the lede looking harder-edged than the headline above
     it. 18px of blur rather than 28 because the glyphs are smaller and
     a wide blur on small type just greys the background out. */
  color: #FFFFFF;
  text-shadow: 0 1px 18px rgba(8, 14, 28, .62);
}

/* Second lede line: what actually gets hauled. Steps down a size from
   the line above so the two read as a primary and a secondary rather
   than as one paragraph that happened to break, and sits tighter to it
   than the 18px that separates the first line from the headline.

   Still full white, not an alpha — same reasoning as the line above,
   and more so at this size. Kept to one line by the same 62ch cap it
   inherits; if this copy is ever lengthened past about 80 characters it
   will wrap, and it should be re-cut rather than allowed to. */
.hero__text--services {
  margin-top: 8px;
  font-size: clamp(14px, 1.7vh, 16.5px);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: clamp(18px, 3vh, 32px);
}

.hero__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding: 0 30px;
  border-radius: 100px;
  font-size: clamp(15px, 1.9vh, 17px);
  font-weight: 700;
  line-height: 1;
  transition: background .2s linear, border-color .2s linear, color .2s linear;
}
.hero__btn--solid {
  background: var(--blue);
  border: 1px solid var(--blue-line);
  color: #FFFFFF;
}
.hero__btn--solid:hover { background: var(--blue-hover); border-color: var(--blue-hover); }

/* Glass rather than a plain outline — a hairline border alone
   disappears against moving footage. */
.hero__btn--ghost {
  background: rgba(255, 255, 255, .12);
  border: 1px solid rgba(255, 255, 255, .70);
  color: #FFFFFF;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.hero__btn--ghost:hover { background: #FFFFFF; color: var(--head-ink); border-color: #FFFFFF; }

/* --- trust badges --- */

/* Two columns, not a wrapping row. The pill version wrapped 3 + 1 and
   the break point moved with the copy — a grid holds a deliberate 2x2
   at every width instead, so the block always reads as a set. */
.hero__badges {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px 22px;
  margin: clamp(16px, 2.6vh, 24px) 0 0;
  padding: 0;
  list-style: none;
  max-width: 520px;
}

/* The pills are gone. Four identical glass capsules with four
   identical ticks read as filler — the eye takes in the SHAPE of the
   row and stops. Each claim now carries its own mark, so the icon
   does half the work of saying what the claim is before the text is
   read at all. */
.hero__badges li {
  display: flex;
  align-items: center;
  gap: 11px;
  font-size: clamp(12.5px, 1.6vh, 14px);
  font-weight: 700;
  line-height: 1.25;
  color: #FFFFFF;
  /* Was nowrap, which is what forced the pills into a third row on a
     360px screen. These wrap now, and the grid keeps them aligned. */
  text-shadow: 0 1px 2px rgba(0, 0, 0, .55), 0 2px 10px rgba(0, 0, 0, .45);
}

/* A tinted disc, not a bare glyph. Over moving footage a loose
   1.5px-stroke icon disappears; the disc gives it a constant ground
   and ties the set to the brand green already used on the headline. */
.hero__badge-ico {
  width: 34px;
  height: 34px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(113, 183, 69, .18);
  border: 1px solid rgba(113, 183, 69, .45);
  color: var(--accent-green);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.hero__badge-ico svg { width: 18px; height: 18px; display: block; }

/* ============================================================
   BADGES ON WIDE SCREENS — give them a ground

   Above 960 the copy sits ON the film, and this is the one block in
   the hero with no solid behind it: the headline has its shadow
   stack, the buttons and the Google card are solid objects, but four
   small labels and four thin icons were left floating on moving
   footage. Below 960 the layout stacks and the badges land on the
   gradient instead, where they already read fine — so this is scoped
   to the case that actually has the problem.

   ONE panel rather than four capsules. Re-pilling each badge would
   bring back exactly the look that was rejected — a row of identical
   containers the eye skims as a shape. A single card keeps the four
   marks distinct inside it, and it pairs with the Google card above:
   white card for the third-party proof, dark card for our own.
   ============================================================ */
@media (min-width: 961px) {
  .hero__badges {
    width: max-content;
    padding: 18px 24px;
    border-radius: 16px;
    background: rgba(9, 15, 32, .72);
    border: 1px solid rgba(255, 255, 255, .16);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    column-gap: 30px;
  }

  /* The shadow was doing the separating; the panel does it now, and
     leaving both made the type look smudged against a flat ground. */
  .hero__badges li { text-shadow: none; }

  /* Slightly stronger on the panel than on the film — against a
     near-solid ground the .18 tint read as a smudge rather than a
     disc. */
  .hero__badge-ico {
    background: rgba(113, 183, 69, .22);
    border-color: rgba(113, 183, 69, .55);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* --- Google rating card --- */
/* Every value here was measured off the reference at
   72630.preview.arsenicdigital.com rather than eyeballed. It is a
   white card on purpose: this is the one element in the hero that is
   meant to read as borrowed credibility, so it deliberately does not
   share the glass treatment the badges use. */
.gbadge {
  display: inline-flex;
  align-items: center;
  gap: 16px;                          /* reference: 16px */
  margin-top: clamp(16px, 2.6vh, 26px);
  padding: 14px 20px;                 /* reference: 14px 20px */
  border-radius: 16px;                /* reference: 16px */
  background: #FFFFFF;
  /* Reference's exact three layers: a hairline ring that keeps the
     card's edge crisp on a light frame, then two drops at different
     spreads so the shadow has a core and a falloff. */
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, .05),
    0 20px 25px -5px rgba(0, 0, 0, .1),
    0 8px 10px -6px rgba(0, 0, 0, .1);
}

.gbadge__mark { width: 40px; height: 40px; flex: none; }

.gbadge__body { display: flex; flex-direction: column; gap: 4px; }

.gbadge__row { display: flex; align-items: center; gap: 8px; }

.gbadge__stars { display: inline-flex; align-items: center; gap: 2px; }
.gbadge__stars svg {
  width: 20px;
  height: 20px;
  /* The reference's own star gold, sampled: #F5C13C. */
  fill: #F5C13C;
}

.gbadge__score {
  font-size: 18px;
  font-weight: 800;
  line-height: 1.6;
  color: var(--head-ink);
}

.gbadge__sub {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.5;
  color: var(--body-ink);
}

/* --- right: quote form --- */

/* SCALED UP roughly half again, to match the widened column above.

   Widening the column on its own would only have stretched the fields
   into letterboxes — same 46px controls, same 22px title, just longer.
   So every internal dimension moves with it: padding, radius, title,
   label spacing, control height, submit height. That is what makes it
   read as a bigger card rather than a stretched one.

   The one number that does NOT scale is the input font size. 16px is
   the threshold below which iOS zooms the page on focus, and it is
   already at the floor; it goes UP with the rest, never down. */
.hero__form {
  background: #FFFFFF;
  border-radius: 22px;
  padding: 36px 32px;
  box-shadow: 0 28px 70px rgba(6, 12, 28, .36);
}

.hero__form-title {
  margin: 0;
  font-family: var(--font);
  font-weight: 800;
  font-size: 28px;
  line-height: 1.2;
  letter-spacing: -.4px;
  color: var(--head-ink);
}

.hero__form-sub {
  margin: 8px 0 24px;
  font-size: 16px;
  line-height: 1.5;
  color: var(--body-ink);
}

.hero__field { margin-bottom: 16px; }

.hero__field label {
  display: block;
  margin-bottom: 7px;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #6A7185;
}

.hero__field input,
.hero__field select,
.hero__field textarea {
  width: 100%;
  height: 52px;
  padding: 0 16px;
  border: 1px solid #D5DCEA;
  border-radius: 11px;
  background: #FFFFFF;
  font-family: var(--font);
  font-size: 17px;          /* never below 16 — iOS zooms the page on focus */
  color: var(--head-ink);
}
.hero__field select { appearance: none; -webkit-appearance: none; padding-right: 38px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1l5 5 5-5' fill='none' stroke='%236A7185' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 14px center;
}
.hero__field input:focus-visible,
.hero__field select:focus-visible,
.hero__field textarea:focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 1px;
  border-color: var(--blue);
}

.hero__submit {
  width: 100%;
  min-height: 60px;
  margin-top: 10px;
  border: 1px solid var(--blue-line);
  border-radius: 100px;
  background: var(--blue);
  color: #FFFFFF;
  font-family: var(--font);
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  transition: background .2s linear;
}
.hero__submit:hover { background: var(--blue-hover); }

.hero__form-note {
  margin: 13px 0 0;
  font-size: 13.5px;
  line-height: 1.4;
  text-align: center;
  color: #7A8296;
}

/* Honeypot. Off-screen rather than display:none — some bots skip
   hidden fields but happily fill positioned ones. */
.hero__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

/* ---- Full quote form: the five fields restored from the live site.
   The live form asks for eight things. Eight stacked 56px fields ran
   the card past the fold on a 768px-tall laptop, so three numbers came
   down together — field height 56>52, gap 16>13, label gap 7>5 — and
   the two name fields share a row. Nothing was dropped to make it fit.
   See v2/_audit/home.md, A13. -------------------------------------- */
.hero__field { margin-bottom: 13px; }
.hero__field label { margin-bottom: 5px; }

/* First name / Last name side by side. Collapses to stacked on the
   narrow breakpoint below, where there is no width to split. */
.hero__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 12px;
}

.hero__field textarea {
  height: auto;
  min-height: 76px;
  padding: 13px 16px;
  line-height: 1.45;
  resize: vertical;
}

.hero__field input::placeholder,
.hero__field textarea::placeholder { color: #9AA2B4; }

/* Terms / Privacy. Live sets these as two bare links under the
   message box; same position, sized to sit quiet under the field. */
.hero__legal {
  display: flex;
  gap: 18px;
  margin: 2px 0 0;
  font-size: 13px;
  line-height: 1.4;
}
.hero__legal a {
  color: #6A7185;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.hero__legal a:hover { color: var(--blue); }

/* Submit/success/error. Hidden until the form reports back, which is
   what Webflow does on the live page — the copy is in the DOM either
   way, which is the point. */
.hero__status {
  margin: 12px 0 0;
  padding: 12px 14px;
  border-radius: 10px;
  font-size: 14px;
  line-height: 1.4;
  text-align: center;
}
.hero__status[hidden] { display: none; }
.hero__status--ok  { background: #E7F6EC; color: #1B5E33; }
.hero__status--err { background: #FDECEC; color: #8A1F1F; }

/* Under ~420px a split row leaves each name field too narrow to show
   its own placeholder, so the pair stacks. */
@media (max-width: 420px) {
  .hero__row { grid-template-columns: 1fr; }
}


/* ============================================================
   THE FOLD, HELD — desktop only (min-width: 961px)

   .hero__inner ASKS for exactly one fold:

     min-height: calc(100svh - var(--bar-h));

   and then blew straight through it, because min-height is a
   floor and not a ceiling. Everything above this line sized the
   LEFT column off the viewport height — the headline is
   min(4.2vw, 7vh), the eyebrow 1.75vh, the lede 1.9vh — and then
   sized the RIGHT column, the form, in flat pixels: 36px padding,
   a 28px title, 52px controls, 13px between fields, a 76px
   textarea, a 60px submit. On a 1300px-tall screen that fits and
   nobody noticed. On every laptop it does not.

   Measured before this block — the hero's bottom edge minus the
   fold, in a real browser at each size:

     2560x1300      0        1440x720    +410
     1920x945    +174        1366x633    +562
     1680x860    +250        1280x585    +585
     1600x780    +322        1000x640    +555

   Four things fix it, in the order they matter:

   1. THE EIGHT PIXELS NOBODY COULD SEE. <input> is inline-block by
      default, so every control sat on a text baseline and carried
      the line box's descender space under it — 8px a row, six
      rows, 48px of nothing. display:block deletes it with no
      visual change whatsoever. This is free height and it is the
      first thing to spend.

   2. TWO FIELDS TO A ROW. First and Last were already paired.
      Email pairs with Phone and Service pairs with Address the
      same way (see .hero__pair in index.html), which turns four
      stacked rows into two. At full size that is ~170px — more
      than the 1920x945 overflow on its own. It is also the right
      trade on the screens that hurt most: a 1366x633 laptop is
      short and WIDE, so it has horizontal room to spend and none
      vertical.

   3. EVERY REMAINING NUMBER SCALES OFF THE HEIGHT. Same device the
      left column already used, applied to the form: clamp(floor,
      Nsvh, today's value). The ceiling is what the form measures
      right now, so a screen tall enough to have never had the bug
      renders identically to before — nothing shrinks that did not
      have to. svh rather than vh to match the container's own
      min-height, so the two can never disagree.

   4. THE COPY COLUMN TOO. It was already fluid in its TYPE but not
      in its FURNITURE — 52px buttons, a 34px badge disc, 14/20px
      and 18/24px card padding. At 1000x640 the copy alone overran
      the fold by ~60px with the form out of the picture entirely.

   Scoped to min-width: 961px throughout. Below that the layout
   stacks, .hero__inner sets min-height: 0 and drops the fold
   constraint on purpose — a stacked hero legitimately runs past
   one screen — so none of this applies and the phone rendering is
   what it always was.
   ============================================================ */
@media (min-width: 961px) {

  /* 1 — see note above. It also settles the two fields in a row
     onto exactly the same baseline instead of a hair apart. */
  .hero__field input,
  .hero__field select,
  .hero__field textarea { display: block; }

  /* 2 — the pairs. Same 12px column gap as .hero__row above them,
     so the three two-up rows read as one rhythm rather than as a
     paired row followed by two that happen to also be paired. */
  .hero__pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 12px;
  }

  /* 3a — the fold's own breathing room. Was clamp(16px, 5vh, 60px),
     which spent 94px of a 945px screen on the two gaps alone. At
     3.4svh it is 32px there and 21px on a 633px laptop, and it
     still opens to 48 on a tall monitor that has height to give
     away. */
  :root { --hero-gap: clamp(14px, 3.4svh, 48px); }

  /* 3c — the grid, on narrow desktops. 48px of gutter and a 420px
     floor under the form column left the copy column 453px at 961,
     which is where the eyebrow breaks to two lines and the headline
     to three. Both numbers stay exactly what they were from about
     1250px up — where the copy column is wide enough that neither
     happens — and give ground only below it. */
  .hero__inner {
    gap: clamp(28px, 3.2vw, 48px);
    grid-template-columns: minmax(0, 1fr) minmax(0, clamp(396px, 38vw, 600px));
  }

  /* 3b — the form. Every ceiling below is the value this file
     already shipped; only the floors and the svh slope are new. */
  .hero__form {
    padding: clamp(16px, 3.1svh, 36px) clamp(22px, 2.2vw, 32px);
    border-radius: clamp(16px, 2.3svh, 22px);
  }

  .hero__form-title { font-size: clamp(19px, 2.75svh, 28px); }

  .hero__form-sub {
    margin: clamp(4px, .85svh, 8px) 0 clamp(9px, 1.9svh, 24px);
    font-size: clamp(12.5px, 1.6svh, 16px);
  }

  .hero__field { margin-bottom: clamp(5px, 1.1svh, 13px); }

  .hero__field label {
    margin-bottom: clamp(2px, .4svh, 5px);
    font-size: clamp(11px, 1.32svh, 13px);
  }

  /* The one number that does NOT scale is the font size INSIDE the
     control. 16px is the floor below which iOS zooms the page on
     focus; it is a phone behaviour, but the rule is easier to keep
     everywhere than to remember where it applies. Only the box
     shrinks — the type in it stays at 17px. */
  .hero__field input,
  .hero__field select { height: clamp(36px, 5.4svh, 52px); }

  /* height, not min-height, and rows="3" stops deciding: at
     17px/1.45 three rows plus padding is ~102px, which is taller
     than the 76px min-height ever let anyone see anyway. Setting
     height outright is what lets this shrink at all. */
  .hero__field textarea {
    height: clamp(44px, 7.2svh, 76px);
    min-height: 0;
    padding-top: clamp(6px, 1.3svh, 13px);
    padding-bottom: clamp(6px, 1.3svh, 13px);
  }

  .hero__legal { font-size: clamp(11.5px, 1.35svh, 13px); }

  .hero__submit {
    min-height: clamp(44px, 6.2svh, 60px);
    margin-top: clamp(4px, .8svh, 10px);
    font-size: clamp(16px, 1.9svh, 18px);
  }

  .hero__form-note {
    margin-top: clamp(6px, 1.1svh, 13px);
    font-size: clamp(11.5px, 1.4svh, 13.5px);
  }

  /* 4 — the copy column's furniture.

     The nudge first. .hero__copy's `top: 44px` is a PAINT offset
     rather than a layout one, which is exactly why it needed
     catching: it moves the whole left column 44px down the screen
     without the fold arithmetic ever seeing it, so on a short
     laptop it was quietly pushing the badge panel below the fold
     that everything else had just been sized to fit. It scales
     now, and reaches zero on the screens that cannot afford it. */
  .hero__copy { top: clamp(0px, 4.7svh, 44px); }

  /* The copy column's last flat numbers. Its TYPE was already fluid
     — the headline, eyebrow and lede all size off vh — but the
     spaces BETWEEN them were literals, so on a short screen the type
     shrank and the gaps did not, and the block ended up mostly air.
     Same ceilings as before, floors low enough to be worth having. */
  .hero__eyebrow { margin-bottom: clamp(7px, 1.5svh, 14px); }
  .hero__text { margin-top: clamp(10px, 1.9svh, 18px); }
  .hero__text--services { margin-top: clamp(4px, .85svh, 8px); }
  .hero__actions { margin-top: clamp(12px, 3svh, 32px); }

  .hero__btn { min-height: clamp(42px, 5.6svh, 52px); }

  .gbadge {
    margin-top: clamp(10px, 2.6svh, 26px);
    gap: clamp(11px, 1.7svh, 16px);
    padding: clamp(10px, 1.5svh, 14px) clamp(14px, 2.1svh, 20px);
    border-radius: clamp(12px, 1.7svh, 16px);
  }
  .gbadge__mark { width: clamp(30px, 4.2svh, 40px); height: clamp(30px, 4.2svh, 40px); }
  .gbadge__stars svg { width: clamp(16px, 2.1svh, 20px); height: clamp(16px, 2.1svh, 20px); }
  .gbadge__score { font-size: clamp(15px, 1.9svh, 18px); }
  .gbadge__sub { font-size: clamp(12px, 1.5svh, 14px); }

  .hero__badges {
    margin-top: clamp(10px, 2.6svh, 24px);
    padding: clamp(9px, 1.9svh, 18px) clamp(14px, 2.5svh, 24px);
    border-radius: clamp(12px, 1.7svh, 16px);
    row-gap: clamp(9px, 1.5svh, 14px);
    column-gap: clamp(18px, 3.2svh, 30px);
  }
  .hero__badges li { gap: clamp(8px, 1.2svh, 11px); font-size: clamp(11.5px, 1.5svh, 14px); }
  .hero__badge-ico { width: clamp(24px, 3.6svh, 34px); height: clamp(24px, 3.6svh, 34px); }
  .hero__badge-ico svg { width: clamp(14px, 1.9svh, 18px); height: clamp(14px, 1.9svh, 18px); }
}

/* ------------------------------------------------------------
   THE LAST 40px — desktop windows under 700px tall

   A second step, and it exists because clamp() draws a straight
   line and this needed a curve. Every ceiling in the block above
   is a value the 1920x945 layout still has to reach, which pins
   the svh slope: .hero__text's 18px top margin needs 1.9svh to
   land on 18 at 945, and a slope that reaches 18 at 945 is still
   handing out 11.4 at 600. The floors cannot help — they are not
   what is binding at that height, the slope is.

   So the screens that need one more pass get it as a pass rather
   than as a steeper line that would have cost the big monitors.
   Everything here is the copy column's FURNITURE: the gaps between
   its blocks, the rating card's internal leading, the badge discs.
   No type shrinks, nothing is hidden, and the headline gives up
   0.04 of its line-height, which at three lines is 5px.

   Measured at 961x600, the tightest desktop window worth holding:
   copy column 510 -> 455 against 467 of room. It clears.
   ------------------------------------------------------------ */
@media (min-width: 961px) and (max-height: 700px) {
  /* The form joins in for the last few pixels, same reasoning: its
     ceilings pin its slopes, so the bottom of the range needs a
     second pass rather than a steeper first one. */
  .hero__form { padding: 16px clamp(20px, 2.2vw, 32px); }
  .hero__form-sub { margin-bottom: 10px; }
  .hero__field { margin-bottom: 6px; }

  .hero__title { line-height: 1; }
  .hero__eyebrow { margin-bottom: 7px; }
  .hero__text { margin-top: 9px; }
  .hero__text--services { margin-top: 4px; }
  .hero__actions { margin-top: 13px; }
  .hero__btn { min-height: 40px; }

  /* The rating card was 66px tall for 30px of logo, because its two
     text rows carried 1.6 and 1.5 line-heights meant for a card with
     room around it. */
  .gbadge { margin-top: 11px; gap: 10px; padding: 8px 12px; }
  .gbadge__body { gap: 2px; }
  .gbadge__score { line-height: 1.25; }
  .gbadge__sub { line-height: 1.3; }

  .hero__badges { margin-top: 11px; padding: 8px 14px; row-gap: 7px; }
  .hero__badge-ico { width: 22px; height: 22px; }
  .hero__badge-ico svg { width: 13px; height: 13px; }
}

/* ------------------------------------------------------------
   AND THE THIRD STEP — desktop windows under 600px tall

   Nothing SHIPS this short. A 1280x720 panel gives the browser
   about 585 of viewport and that is the smallest real desktop
   there is; everything below is a window someone has dragged
   down, a half-screen split, or a laptop with devtools docked at
   the bottom. It still holds, because "fits the fold" that stops
   being true the moment you resize the window is not a fix, it is
   a screenshot.

   The measured gap this closes: at 540px of viewport the hero
   came in 30px long at every width, and 48 long at 961 where the
   copy column is narrowest and its headline runs to three lines.
   Below 460 the announcement bar removes itself (see the rule
   further down) which buys back another 30, so this is the last
   step that needs to exist.
   ------------------------------------------------------------ */
@media (min-width: 961px) and (max-height: 600px) {
  /* Clamps rather than the flat numbers this block first carried,
     for the same reason the block above it exists: a flat value
     stops shrinking, and below 600 there is still 100px of range to
     cover. Every ceiling here is what the rule wants AT 600, so the
     step into this block is seamless; the floors are where a control
     stops being clickable and the range ends. */
  .hero__form { padding: clamp(5px, 1.83svh, 11px) clamp(18px, 2.2vw, 32px); }
  .hero__form-title { font-size: clamp(15px, 2.92svh, 17.5px); }
  .hero__form-sub { margin-bottom: clamp(4px, 1svh, 6px); }
  .hero__field { margin-bottom: clamp(2px, .67svh, 4px); }
  .hero__field label { margin-bottom: 1px; }
  .hero__field input,
  .hero__field select { height: clamp(26px, 5.5svh, 33px); }
  .hero__field textarea { height: clamp(27px, 6.33svh, 38px); }
  .hero__submit { min-height: clamp(29px, 6.67svh, 40px); margin-top: clamp(2px, .5svh, 3px); }
  .hero__form-note { margin-top: clamp(3px, .67svh, 4px); }

  .hero__eyebrow { margin-bottom: clamp(3px, .83svh, 5px); }
  .hero__text { margin-top: clamp(5px, 1.17svh, 7px); }
  .hero__text--services { margin-top: clamp(2px, .5svh, 3px); }
  .hero__actions { margin-top: clamp(6px, 1.67svh, 10px); }
  .hero__btn { min-height: clamp(28px, 6svh, 36px); }

  .gbadge {
    margin-top: clamp(5px, 1.33svh, 8px);
    gap: clamp(6px, 1.5svh, 9px);
    padding: clamp(3px, .83svh, 5px) clamp(7px, 1.67svh, 10px);
  }
  .gbadge__mark { width: clamp(18px, 4.33svh, 26px); height: clamp(18px, 4.33svh, 26px); }
  .gbadge__stars svg { width: clamp(11px, 2.33svh, 14px); height: clamp(11px, 2.33svh, 14px); }

  .hero__badges {
    margin-top: clamp(5px, 1.33svh, 8px);
    padding: clamp(3px, .83svh, 5px) clamp(9px, 2svh, 12px);
    row-gap: clamp(3px, .83svh, 5px);
  }
  .hero__badge-ico { width: clamp(14px, 3.33svh, 20px); height: clamp(14px, 3.33svh, 20px); }
  .hero__badge-ico svg { width: clamp(10px, 2svh, 12px); height: clamp(10px, 2svh, 12px); }
}


/* ============================================================
   ABOUT / VSL

   The one section with no reference site behind it.

   Rebuilt after the first pass read as generic and left a 240px
   hole under the player. Two causes, both removed: the copy column
   carried an eyebrow, a seven-line quote, an attribution line, a
   three-item check list and two buttons — the stock "feature
   section" stack — and it ran so much taller than a 16:9 frame that
   no alignment could make the pair sit together.

   Now the video leads and the copy is a headline, one line of
   support, and the two buttons. That is the point of a VSL: the
   film does the selling, the text only has to earn the click. With
   the copy that short the two columns come out within ~50px of each
   other, so they genuinely centre against one another.

   Sized off the page's own system: 1200px rail and 100px padding
   like the services and reviews rows, 20px radius like the service
   tiles, the same blue and the same pill CTAs.
   ============================================================ */

/* 72px, the reference's own vertical padding, down from 110. Together
   with the shorter portrait this is most of why their section is 546px
   tall where ours was 795 — the section was not bigger because it held
   more, it was bigger because it was padded more. */
.about {
  background: #FFFFFF;
  padding: 72px 20px;
}

/* Measured off the reference at a 1920 viewport: 1200px rail, 40px
   gap, image column 450, text column 710. The proportion is the point
   — the text gets 59% of the rail, so the paragraph can run wide
   instead of being squeezed into a narrow measure beside a big photo.
   A fixed image track rather than a fraction, because the photo has a
   natural size and should not grow with the viewport. */
.about__inner {
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 450px) minmax(0, 1fr);
  gap: 40px;
  align-items: center;
}

.about__media { margin: 0; }

/* aspect-ratio holds the frame before the image decodes, so the copy
   beside it never jumps. Capped, or on a wide screen the portrait
   grows taller than the copy it is meant to sit beside. */
/* 450x499 on the reference, a ratio of 0.90 — nearly square. Ours was
   4:5 (0.80), which at the same width stood 75px taller and was
   dragging the whole section down with it. 9/10 matches. */
.about__portrait {
  display: block;
  width: 100%;
  max-width: 450px;
  margin-left: auto;
  aspect-ratio: 9 / 10;
  border-radius: 20px;
  background: #0B1226;
  object-fit: cover;
  box-shadow: 0 20px 44px rgba(11, 18, 38, .16);
}

/* Places him before he is introduced — the section is about a person,
   and where he works is the first thing that makes him local. */
/* Reference: 18px/700, uppercase, in the accent colour. Ours was
   13px and blue, which read as a caption rather than as a label
   introducing a person. */
.about__eyebrow {
  margin: 0 0 14px;
  font-size: clamp(15px, 1.2vw, 18px);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--accent-green);
}

/* Reference: 48px / weight 600 / line-height 1.2. Bigger AND lighter
   than what was here (36/800) — the extra weight was what made it
   feel shouty at a size that could not carry it. */
.about__title {
  margin: 0;
  font-family: var(--font);
  font-weight: 600;
  font-size: clamp(30px, 3.4vw, 48px);
  line-height: 1.2;
  letter-spacing: -.6px;
  color: var(--head-ink);
  text-wrap: balance;
}

/* Plain coloured TEXT, not a highlight block. The reference splits
   its heading exactly this way and it is quieter than the marker
   treatment — which stays on the other section headings, so this
   section reads as the personal one. Green rather than blue keeps it
   with the hero accent and the eyebrow above. */
.about__accent { color: var(--accent-green); }

/* Reference: 18px / 500 / 1.5, and the paragraph runs the FULL 710px
   column. The 46ch cap that was here is what made this section read as
   narrow — a normal reading measure, but wrong next to a 450px photo,
   because it left a column of dead space to its right and shrank the
   only block of text in the section. At three lines the long measure
   costs nothing. */
.about__lede {
  margin: 22px 0 0;
  font-size: clamp(16px, 1.15vw, 18px);
  font-weight: 500;
  line-height: 1.5;
  color: #4C5265;
}
.about__lede + .about__lede { margin-top: 16px; }

/* A credential line rather than a bullet list — same facts, none of
   the stacked-checkmark look. Separators are drawn in CSS so the
   markup stays plain text. */
.about__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 18px;
  margin: 26px 0 0;
  padding-top: 22px;
  border-top: 1px solid #E4E7EE;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: .02em;
  color: #6A7185;
}
.about__meta span { display: inline-flex; align-items: center; }
.about__meta span + span::before {
  content: '';
  width: 4px;
  height: 4px;
  margin-right: 18px;
  border-radius: 50%;
  background: var(--blue);
}

.about__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 32px;
}

.about__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0 34px;
  border-radius: 100px;
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
  transition: background .2s linear, border-color .2s linear, color .2s linear;
}
.about__btn--solid {
  background: var(--blue);
  border: 1px solid var(--blue);
  color: #FFFFFF;
}
.about__btn--solid:hover { background: var(--blue-hover); border-color: var(--blue-hover); }
.about__btn--ghost {
  background: transparent;
  border: 1px solid #0B1226;
  color: #0B1226;
}
.about__btn--ghost:hover { background: #0B1226; color: #FFFFFF; }


/* ============================================================
   SHOWCASE — four videos

   Reference measurements, all at a 1905px viewport:
     section    #F1F5F7, padding 100px 20px
     rail       1200px centred
     heading    900 / 60px / line-height 54px / uppercase / #737373
     paragraph  400 / 17px / line-height 30.6px / #737373
     grid       4 x 300px columns, 20px inner padding -> 260 x 150
                players separated by 40px
     gap        40px from paragraph baseline box to player top
   ============================================================ */

.showcase {
  background: var(--surface);
  padding: 100px 20px;
}

/* The reference's players are 9:16 PORTRAIT — its source clips are
   2160 x 3840 vertical phone footage, rendered at 260 x 462. That is
   the "long" shape.

   The whole section — heading, paragraph AND video row — sits on ONE
   1200px rail (1160px of content inside 20px padding). That single
   shared rail is what makes the composition read as the reference's:
   the row of videos is exactly as wide as the paragraph above it,
   never wider.

   --showcase-rail is the one number that scales the whole block. At
   the reference's 1200px the players land at 260 x 462, pixel-exact.
   Raising it grows all four together and keeps the alignment.

   WIDENED, at Maaz's direction. A fixed 1200px rail is 83% of a
   1440px laptop, which is where it was measured and where it looks
   right — but only 63% of a 1920px monitor, and on one of those the
   section reads as a narrow strip with a third of the screen empty
   beside it. The rail now tracks the viewport at the 80% he asked
   for, between two stops:

     floor 1200px   below ~1500px the fixed rail is ALREADY wider
                    than 80vw, so the floor is what stops this change
                    making the section narrower on a laptop than it
                    was before.
     cap   1600px   400px columns, 360px players. Past that the
                    players start dominating the fold: 360 wide at
                    9:16 is already 640 tall, and a 2560px monitor
                    taking a literal 80% would put four 472 x 840
                    videos on the page.

   Resolves to 1536px at 1920 (exactly 80%), 1280px at 1600, and the
   old 1200px at 1440 and below. */
:root { --showcase-rail: clamp(1200px, 80vw, 1600px); }

.showcase__inner {
  max-width: var(--showcase-rail);
  margin: 0 auto;
}

.showcase__head { padding: 0 20px; }

/* DEVIATION — the reference jumps this straight from 60px with no
   fluid step, same as its hero. Clamped here; it resolves to the
   reference's exact 60px from ~1430px up. */
.showcase__title {
  margin: 0 0 20px;
  font-family: var(--font);
  font-weight: 900;
  font-size: clamp(28px, 4.2vw, 60px);
  /* DEVIATION — the reference sets 54px on 60px (0.9). At that
     leading the two lines nearly touch, and the highlight block on
     "CLEAR-OUT" clipped the line above it. 1.02 is the only value on
     the page changed for the highlight, and it buys 7px. */
  line-height: 1.02;
  letter-spacing: normal;
  text-align: center;
  color: var(--head-ink);
}

/* The one part of the block that does NOT take the wider rail, and
   the only place the "paragraph is exactly as wide as the video row"
   alignment above is deliberately broken.

   17px centred copy set to the full rail was already running about
   135 characters a line at 1200px; on the widened rail it would be
   175, against a comfortable 65-90. Long measures are hard to read
   for the ordinary mechanical reason - the longer the line, the
   further the eye travels back and the easier it is to land on the
   wrong one - and centred text makes it worse, because the ragged
   left edge gives the return sweep nothing to aim at.

   900px is ~105 characters here. Above the 65-90 a typography book
   would give you, and chosen by eye against the widened row rather
   than off the rule: at 760 the measure is textbook, but the block
   goes wide heading, narrow column, wide video row, and the pinch in
   the middle is the thing you notice. 900 holds the three widths in
   the same family while still cutting the line nearly in half. */
.showcase__text {
  max-width: 900px;
  margin: 0 auto;
  font-size: 17px;
  font-weight: 400;
  line-height: 1.8;                /* reference: 30.6px on 17px */
  text-align: center;
  color: var(--body-ink);
}
.showcase__text strong { font-weight: 700; }

.showcase__grid {
  list-style: none;
  margin: 40px 0 0;               /* reference gap under the paragraph */
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}

/* 1200 / 4 = 300px columns; 20px each side leaves the reference's
   260px player and its 40px gutters. The padding IS the gutter,
   which is why there is no grid-gap here — and it is what keeps the
   outer edges flush with the paragraph above. */
.showcase__item { margin: 0; padding: 0 20px; }

/* 9:16, the reference's own ratio: 260 wide -> 462 tall, exact.
   The max-width is a safety rail for the two-column fold, where a
   cell can otherwise get wide enough to produce a 900px-tall video.

   NOTE — the stand-in clips in assets/video/ are all 16:9 landscape
   (checked: 1280x720 / 1920x1080, all eight of them), so cover is
   cropping them hard to fill a vertical frame. That is a footage
   problem, not a layout one: this shape wants vertical phone footage,
   which is what the reference shoots. */
.showcase__item video {
  display: block;
  width: 100%;
  max-width: 400px;
  margin: 0 auto;
  aspect-ratio: 9 / 16;
  background: #000;
  object-fit: cover;
}


/* ============================================================
   SERVICES — eight photo cards

   Reference measurements at a 1905px viewport:
     section  padding 100px 20px, white
     rail     1500px centred
     heading  900 / 60px / line-height 78px / uppercase / #737373,
              margin 0 20px 20px
     grid     4 x 375px columns, 20px inner padding -> 335 x 200
              cards, 40px gutters horizontally AND vertically
     card     border-radius 20px, overflow hidden, #F3F3F3 under
     photo    absolute inset 0, cover, transition .5s,
              scale(1.1) on card hover
     label    Montserrat 900 / 30px / white / centred both ways,
              text-shadow 1px 1px 28px rgb(25,26,31)
     button   18px/700, padding 12px 24px, radius 30px
   ============================================================ */

.services {
  background: #FFFFFF;
  padding: 100px 20px;
}

.services__inner {
  max-width: 1500px;
  margin: 0 auto;
}

/* 20px matches the reference heading's own side margin, which is
   what puts the heading flush with the card edges below it. */
.services__head { padding: 0 20px; }

.services__title {
  margin: 0 0 20px;
  font-family: var(--font);
  font-weight: 900;
  font-size: clamp(28px, 4.2vw, 60px);
  line-height: 1.3;                 /* reference: 78px on 60px */
  text-align: center;
  color: var(--head-ink);
}

.services__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* Three, not the reference's four. There are six project tiles now
     that Junk Removal and Dumpster Rental have moved up to their own
     row, and six across a 4-wide grid leaves a ragged 4+2. Three gives
     two even rows. */
  grid-template-columns: repeat(3, 1fr);
}

/* 1500 / 3 = 500px cells; 20px all round keeps the reference's 40px
   gutters in both directions. The padding IS the gutter, so there is
   no grid-gap. */
.services__item { margin: 0; padding: 20px; }

.services__card {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 335 / 200;          /* reference card proportions */
  border-radius: 20px;
  overflow: hidden;                 /* this is what clips the zoom */
  background: #F3F3F3;
  isolation: isolate;
}

/* The photo is its own layer so the zoom scales the image without
   touching the label. DEVIATION — the reference anchors these at
   50% 100% (bottom); NJ's photos carry their subject mid-frame, so
   these are centred. */
.services__bg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  transition: transform .5s ease;
  transform: translateZ(0);         /* keeps the scale off the text */
}
.services__card:hover .services__bg,
.services__card:focus-visible .services__bg { transform: scale(1.1); }

@media (prefers-reduced-motion: reduce) {
  .services__bg { transition: none; }
  .services__card:hover .services__bg,
  .services__card:focus-visible .services__bg { transform: none; }
}

/* DEVIATION — the reference's line-height is 55.7px, inherited from
   its body copy. Every one of its labels is a single word or two, so
   it never shows. NJ's are longer ("Landscaping & Yard Work"), so
   this is tightened to 1.15 — identical on one line, survivable on
   three. */
.services__label {
  position: relative;
  z-index: 1;
  padding: 0 16px;
  font-family: var(--font);
  font-weight: 900;
  font-size: clamp(20px, 1.9vw, 30px);
  line-height: 1.15;
  text-align: center;
  color: #FFFFFF;
  text-shadow: 1px 1px 28px rgb(25, 26, 31);
  text-wrap: balance;
}

.services__cta {
  margin-top: 60px;                 /* reference gap below the grid */
  text-align: center;
}

/* Same pill as the hero, at the reference's own values. */
.services__btn {
  display: inline-block;
  padding: 12px 24px;
  border: 1px solid var(--blue-line);
  border-radius: 30px;
  background: var(--blue);
  color: #FFFFFF;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  transition: background .2s linear, border-color .2s linear;
}
.services__btn:hover {
  background: var(--blue-hover);
  border-color: var(--blue-hover);
}

/* ------------------------------------------------------------
   THE TWO ACTUAL SERVICES

   This section used to be eight equal tiles, which read as eight
   services. The company offers two. v1's nav, its home "What We Do"
   and its LocalBusiness schema all agree: Junk Removal and Dumpster
   Rental, and nothing else.

   The other six are PROJECT TYPES, lifted from v1's dumpster page
   ("What It's For — One Dumpster, Every Kind of Project"). They are
   real, they are v1's own words, but they are jobs the two services
   cover rather than services in their own right. Presented at the
   same size as the two that convert, they buried them.

   So the two get their own row above, at roughly four times the area,
   with a sentence each. The six keep the existing tile component
   below, unchanged, under a subhead that names them for what they
   are.
   ------------------------------------------------------------ */

/* Eyebrow over the section H2. Carries the old "What We Do" heading,
   which the live site's own H2 displaced. Geometry mirrors
   .about__eyebrow so the two sections agree. */
/* ============================================================
   PHONE CTA BAND — restored from the live site (audit A11)

   Live renders this as three stacked lines on a dark field. Same
   three lines here, laid out on one row at desktop width and stacking
   below 760 where the shout line would otherwise break badly.

   The gradient is doing real work: a perfectly flat dark fill is what
   the display pipeline spatial-dithers into a visible diagonal across
   the viewport. Two stops a few luminance steps apart, plus the shared
   .noise-overlay at .035, gives the dither nothing to latch onto.
   ============================================================ */
.callband {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: 44px 20px;
  background: linear-gradient(180deg, #131B2E 0%, #0F172A 100%);
}

.callband__grain {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: .035;
  mix-blend-mode: overlay;
  pointer-events: none;
}

.callband__inner {
  position: relative;      /* above the grain layer */
  z-index: 1;
  max-width: 1200px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 14px 34px;
  text-align: center;
}

.callband__shout {
  font-family: var(--font);
  font-size: clamp(19px, 2.1vw, 30px);
  font-weight: 900;
  line-height: 1.15;
  letter-spacing: -.01em;
  color: #FFFFFF;
}

.callband__link {
  font-size: clamp(13px, 1.05vw, 15px);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .74);
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color .2s linear;
}
.callband__link:hover { color: #FFFFFF; }

/* The number is the action, so it gets the button treatment rather
   than sitting as a third line of text. */
.callband__tel {
  display: inline-flex;
  align-items: center;
  min-height: 52px;
  padding: 0 26px;
  border: 1px solid var(--blue-line);
  border-radius: 100px;
  background: var(--blue);
  font-size: clamp(16px, 1.3vw, 19px);
  font-weight: 800;
  letter-spacing: -.01em;
  color: #FFFFFF;
  transition: background .2s linear;
}
.callband__tel:hover { background: var(--blue-hover); }

@media (max-width: 760px) {
  .callband { padding: 34px 20px; }
  .callband__inner { flex-direction: column; gap: 16px; }
}

.services__eyebrow {
  margin: 0 0 12px;
  font-family: var(--font);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  text-align: center;
  color: var(--blue);
}

/* The restored live paragraph bolds four phrases. At 900 against a
   300-weight heading the contrast was shouty, so <strong> here is 600
   and takes the heading ink rather than a colour of its own. */
.services__lead strong { font-weight: 600; color: var(--head-ink); }

.services__lead {
  max-width: 62ch;
  margin: 0 auto 46px;
  text-align: center;
  font-size: clamp(16px, 1.15vw, 18px);
  line-height: 1.6;
  color: var(--body-ink);
}

/* v2's own lead, now the second paragraph under the restored live one.
   Must come AFTER .services__lead — that rule sets `margin` as a
   shorthand, so any margin-top declared before it is overwritten. */
.services__lead--second {
  margin: 14px auto 46px;
  font-size: clamp(15px, 1.05vw, 16.5px);
  color: #6A7185;
}

.services__primary {
  list-style: none;
  margin: 0 0 20px;
  /* 20px to line the cards up with the tile grid below, whose gutters
     are padding on .services__item rather than a grid gap. */
  padding: 0 20px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 40px;
}

.services__pcard {
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: clamp(300px, 26vw, 400px);
  padding: 36px;
  border-radius: 20px;
  overflow: hidden;                 /* clips the zoom, as the tiles */
  isolation: isolate;
  background: #0B1226;              /* shows while the photo loads */
  color: #FFFFFF;
}

.services__pbg {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  transition: transform .5s ease;
  transform: translateZ(0);
}
.services__pcard:hover .services__pbg,
.services__pcard:focus-visible .services__pbg { transform: scale(1.06); }

/* THESE CARDS GET A SCRIM AND THE SMALL TILES DO NOT, AND THAT IS
   DELIBERATE. The tiles carry one short label and can lean on a
   text-shadow. These carry a sentence, and service-junk.jpg measures
   1.3:1 for white across the brightest tenth of its lower half. A
   shadow does not save body copy at 1.3:1. Bottom-anchored so the top
   of each photo is untouched. */
.services__pcard::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top,
              rgba(8, 14, 28, .92)   0%,
              rgba(8, 14, 28, .74)  34%,
              rgba(8, 14, 28, .30)  64%,
              rgba(8, 14, 28, .04) 100%);
}

.services__pbody {
  display: block;
  position: relative;
  z-index: 2;
}

/* display:block on both. These are spans, not headings, because they
   live inside the card's <a> and an <a> may not contain block-level
   flow content in valid HTML. Without this they run together as one
   inline paragraph. */
.services__pname {
  display: block;
  margin: 0 0 10px;
  font-family: var(--font);
  font-weight: 900;
  font-size: clamp(24px, 2.4vw, 36px);
  line-height: 1.15;
  letter-spacing: -.6px;
  color: #FFFFFF;
}

.services__pcopy {
  display: block;
  margin: 0 0 18px;
  max-width: 42ch;
  font-size: clamp(15px, 1.05vw, 17px);
  line-height: 1.55;
  color: rgba(255, 255, 255, .88);
}

/* The live copy bolds the service name and the item list. On a photo
   card the body already runs at .88 white, so <strong> just goes to
   full white — enough separation without adding a second weight over
   a busy background. */
.services__pcopy strong { font-weight: 700; color: #FFFFFF; }

.services__plink {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: .02em;
  color: #FFFFFF;
}
/* Arrow drawn in CSS, same trick as the FAQ chevron. */
.services__plink::after {
  content: "";
  width: 8px;
  height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform .2s ease;
}
.services__pcard:hover .services__plink::after { transform: rotate(-45deg) translate(3px, 3px); }

@media (prefers-reduced-motion: reduce) {
  .services__pbg { transition: none; }
  .services__pcard:hover .services__pbg,
  .services__pcard:focus-visible .services__pbg { transform: none; }
  .services__plink::after { transition: none; }
  .services__pcard:hover .services__plink::after { transform: rotate(-45deg); }
}

/* Subhead over the six project tiles. Smaller than .services__title,
   because it labels a subordinate group rather than opening a
   section. */
.services__sub {
  margin: 56px 0 6px;
  padding: 0 20px;
  text-align: center;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(19px, 1.7vw, 26px);
  line-height: 1.25;
  letter-spacing: -.5px;
  color: var(--head-ink);
}

.services__subcopy {
  margin: 0 auto 26px;
  padding: 0 20px;
  max-width: 56ch;
  text-align: center;
  font-size: 16px;
  line-height: 1.6;
  color: var(--body-ink);
}

@media (max-width: 860px) {
  .services__primary { grid-template-columns: minmax(0, 1fr); gap: 20px; }
  .services__pcard { min-height: 260px; padding: 28px; }
}

/* ON A PHONE THESE TWO CARDS DROP THEIR SENTENCE AND BECOME NAMEPLATES.

   At 375px the sentence wraps to five or six lines. Five lines of body
   copy over a photo, at the bottom of a 230px card, needs a scrim dark
   enough that the photograph underneath stops being a photograph — you
   are looking at a dark rectangle with a paragraph on it, twice in a
   row. The copy is not doing enough work to be worth that.

   So on mobile: hide the sentence, centre the name, and lift the scrim
   to a light even veil. The name goes up to 30px because it is now the
   only thing in the card and has to carry it, but no further — bigger
   than this and it spans the full width and we are back to text
   covering the photo. Text-shadow does the rest of the legibility work,
   the same way the six tiles below already do it. */
@media (max-width: 640px) {
  .services__primary { padding: 0 6px; }
  .services__pcard {
    min-height: 230px;
    padding: 22px;
    border-radius: 16px;
    align-items: center;              /* was flex-end — text sits centred */
    justify-content: center;
  }

  /* The bottom-weighted scrim was built to carry body copy at the foot
     of the card. The text is in the middle now, so the darkness moves
     with it: a wide soft ellipse behind the words, over a light base.

     It is a radial rather than an even veil because of what is in these
     two photographs. service-junk.jpg has the truck's own white sign in
     it, high-contrast red type on white, and at this crop it sits
     almost exactly where "Junk Removal" lands — there is no panning
     away from it, the image is 1.50:1 into a 1.46:1 box. An even veil
     dark enough to beat that sign is dark enough to flatten the whole
     photo. The ellipse pays for the legibility only where the words
     are and leaves the edges of the frame alone. */
  .services__pcard::after {
    background:
      radial-gradient(ellipse 88% 52% at 50% 47%,
                      rgba(8, 14, 28, .66)  0%,
                      rgba(8, 14, 28, .40) 62%,
                      rgba(8, 14, 28, .10) 100%),
      linear-gradient(to top,
                      rgba(8, 14, 28, .40)   0%,
                      rgba(8, 14, 28, .22)  55%,
                      rgba(8, 14, 28, .16) 100%);
  }

  .services__pbody { text-align: center; }

  .services__pname {
    font-size: 30px;
    margin-bottom: 12px;
    letter-spacing: -.4px;
    text-shadow: 0 1px 24px rgba(8, 14, 28, .85);
  }

  .services__pcopy { display: none; }

  .services__plink {
    font-size: 14px;
    text-shadow: 0 1px 18px rgba(8, 14, 28, .85);
  }

  .services__sub { margin-top: 42px; }
}

/* 30px holds "Dumpster Rental" on one line down to a 360px viewport.
   Below that it breaks, and one card reading over two lines while its
   neighbour reads over one throws the pair out of balance. 25px is the
   size that still fits at 320. */
@media (max-width: 359px) {
  .services__pname { font-size: 25px; }
}


/* ============================================================
   REVIEWS

   Reference measurements at a 1905px viewport:
     section  #F1F5F7, padding 75px 20px 20px
     rail     1200px centred
     heading  900 / 60px / line-height 78px / uppercase / #737373
     grade    "EXCELLENT" 24px / 600 / black
     card     white, radius 12px, padding 20px, 365px wide,
              shadow 1px 4px 10px rgba(0,0,0,.1)
                   + 0 0 2px rgba(0,0,0,.05)
     avatar   40 x 40
     name     15px / 600 / black
     arrows   30 x 30 white circles, one each side

   The reference runs Trustindex, which injects a live Google feed.
   This is a static hand-built equivalent: same geometry, no
   third-party script, no external requests.
   ============================================================ */

.reviews {
  background: var(--surface);
  padding: 75px 20px 20px;
}

.reviews__inner {
  max-width: 1200px;
  margin: 0 auto;
}

.reviews__head { padding: 0 20px; }

.reviews__title {
  margin: 0 0 20px;
  font-family: var(--font);
  font-weight: 900;
  font-size: clamp(28px, 4.2vw, 60px);
  line-height: 1.3;                /* reference: 78px on 60px */
  text-align: center;
  color: var(--head-ink);
}

.reviews__text {
  margin: 0;
  font-size: 17px;
  line-height: 1.8;
  text-align: center;
  color: var(--body-ink);
}
.reviews__text strong { font-weight: 700; }


/* --- aggregate block --- */

.reviews__summary {
  margin: 34px 0 24px;
  text-align: center;
}

.reviews__grade {
  margin: 0 0 4px;
  font-size: 24px;
  font-weight: 600;
  line-height: 1.2;
  text-transform: uppercase;       /* reference prints EXCELLENT */
  color: #000000;
}

.reviews__stars,
.reviews__cardstars {
  display: flex;
  justify-content: center;
  gap: 2px;
}
.reviews__stars svg { width: 26px; height: 25px; }
.reviews__cardstars svg { width: 17px; height: 16px; }
.reviews__stars svg,
.reviews__cardstars svg { fill: #FBBC04; flex: none; }

.reviews__count {
  margin: 6px 0 0;
  font-size: 14px;
  line-height: 1.4;
  color: #4A4A4A;
}
.reviews__count strong { font-weight: 700; color: #1F1F1F; }

/* The source badge. These reviews come off the client's Google
   Business Profile, which is what the wordmark states — same
   attribution the reference's widget prints. */
.reviews__source {
  margin: 8px 0 0;
  font-size: 30px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: -1px;
}
.reviews__source .g-b { color: #4285F4; }
.reviews__source .g-r { color: #EA4335; }
.reviews__source .g-y { color: #FBBC04; }
.reviews__source .g-g { color: #34A853; }


/* --- carousel --- */

.reviews__carousel {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
}

.reviews__track {
  list-style: none;
  margin: 0;
  padding: 8px;                    /* room for the card shadow */
  flex: 1;
  min-width: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: calc((100% - 32px) / 3);   /* 3 visible */
  gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Matches the 8px padding above. Without it the snapport starts at
     the padding edge, so the first card parks at scrollLeft 8 and the
     "at the start" test never fires. */
  scroll-padding: 0 8px;
  scrollbar-width: none;
  overscroll-behavior-x: contain;

  /* See the note on .showcase__grid. overflow clips PAINT but does
     not make this a containing block, so an absolutely positioned
     descendant escapes it and widens the document. */
  contain: paint;
}
.reviews__track::-webkit-scrollbar { display: none; }
.reviews__track:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }

.reviews__card {
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  background: #FFFFFF;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 1px 4px 10px rgba(0, 0, 0, .10), 0 0 2px rgba(0, 0, 0, .05);
}

.reviews__card-head {
  display: grid;
  grid-template-columns: 40px 1fr auto;
  align-items: center;
  gap: 12px;
}

.reviews__avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  color: #FFFFFF;
  font-size: 17px;
  font-weight: 700;
  line-height: 1;
  user-select: none;
}

.reviews__name {
  font-size: 15px;
  font-weight: 600;
  color: #000000;
  min-width: 0;
  overflow-wrap: anywhere;
}

.reviews__badge {
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  color: #4285F4;
}

.reviews__cardstars { margin: 14px 0 10px; }

.reviews__body {
  margin: 0;
  font-size: 15px;
  line-height: 1.5;
  text-align: center;
  color: #4A4A4A;
}

.reviews__arrow {
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #FFFFFF;
  color: #6B6B6B;
  display: grid;
  place-items: center;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .14);
  transition: color .2s linear, box-shadow .2s linear, opacity .2s linear;
}
.reviews__arrow svg { width: 16px; height: 16px; }
.reviews__arrow:hover { color: #1F1F1F; box-shadow: 0 2px 8px rgba(0, 0, 0, .2); }
.reviews__arrow[disabled] { opacity: .35; pointer-events: none; }

/* Position dots. Off above 560, where the arrows are on screen and
   two or three cards are visible at once — both of which already say
   the row is a row. See the phone block near the end of this file for
   where they switch on and why. */
.reviews__dots {
  display: none;
  list-style: none;
  margin: 18px 0 0;
  padding: 0;
  justify-content: center;
  align-items: center;
  gap: 7px;
}

.reviews__dots li {
  width: 7px;
  height: 7px;
  border-radius: 100px;
  background: #C7CCD8;

  /* Width is the thing that animates, so the active dot grows into a
     short bar rather than just changing colour. Colour alone is a weak
     signal at 7px, and on a page this blue it competes with the links.
     transition on width and background only — never on transform,
     which would blur the dot mid-animation on a subpixel grid. */
  transition: width .25s ease, background .25s ease;
}

.reviews__dots li.is-at {
  width: 20px;
  background: var(--blue);
}

@media (prefers-reduced-motion: reduce) {
  .reviews__dots li { transition: none; }
}


/* ============================================================
   STEPS — three circular photos

   Reference measurements at a 1905px viewport:
     section  white, padding 100px 20px
     rail     1200px centred
     heading  900 / 60px / line-height 60px / uppercase / #737373,
              margin 0 0 20px
     grid     3 x 400px columns, 20px inner padding -> 360px circles
              with 40px gutters
     photo    360 x 360, border-radius 100%
     title    900 / 32px / line-height 32px / uppercase / #737373,
              50px under the photo, 20px above the copy
     copy     17px / line-height 27.2px (1.6) / #737373 / centred
     button   360px wide (one full column), 18px/700,
              padding 12px 24px, radius 30px
   ============================================================ */

.steps {
  background: #FFFFFF;
  padding: 100px 20px;
}

.steps__inner {
  max-width: 1200px;
  margin: 0 auto;
}

.steps__head { padding: 0 20px; }

/* Same eyebrow treatment as .services__eyebrow — this section is
   white too, so it can share the geometry exactly. */
.steps__eyebrow {
  margin: 0 0 12px;
  font-family: var(--font);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  text-align: center;
  color: var(--blue);
}

.steps__title {
  margin: 0 0 20px;
  font-family: var(--font);
  font-weight: 900;
  font-size: clamp(28px, 4.2vw, 60px);
  line-height: 1;                  /* reference: 60px on 60px */
  text-align: center;
  color: var(--head-ink);
}

.steps__grid {
  list-style: none;
  margin: 40px 0 0;                /* reference gap under the heading */
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  counter-reset: step;
}

/* 1200 / 3 = 400px cells; 20px each side leaves the reference's 360px
   circle and its 40px gutters. */
.steps__item {
  margin: 0;
  padding: 0 20px;
  text-align: center;
}

/* DEVIATION — the reference sets no object-fit, so its images are
   pre-cropped square. NJ's photography is landscape, so cover does
   the cropping here instead of stretching them into the circle. */
.steps__photo {
  width: 100%;
  max-width: 360px;
  aspect-ratio: 1;
  height: auto;
  margin: 0 auto;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface);
}

.steps__name {
  margin: 50px 0 20px;             /* reference gap under the photo */
  font-family: var(--font);
  font-weight: 900;
  font-size: clamp(24px, 2.4vw, 32px);
  line-height: 1;                  /* reference: 32px on 32px */
  color: var(--head-ink);
}

.steps__copy {
  margin: 0;
  font-size: 17px;
  line-height: 1.6;                /* reference: 27.2px on 17px */
  color: var(--body-ink);
}

.steps__cta {
  margin-top: 60px;
  text-align: center;
}

/* The reference's button is exactly one column wide, not auto. */
.steps__btn {
  display: inline-block;
  width: 360px;
  max-width: 100%;
  padding: 12px 24px;
  border: 1px solid var(--blue-line);
  border-radius: 30px;
  background: var(--blue);
  color: #FFFFFF;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  text-align: center;
  transition: background .2s linear, border-color .2s linear;
}
.steps__btn:hover {
  background: var(--blue-hover);
  border-color: var(--blue-hover);
}


/* ============================================================
   IMPACT — six stat cards
   Source: jsdisposalnj.com, "Making a Measurable Impact".
   This is the one section taken from that site rather than from
   coconutcleaningco.com.

   Reference measurements at a 1905px viewport:
     section  position relative; ::before is the photo, cover,
              0% 50%, OPACITY 0.15; inner padding 70px 0
     rail     1140px centred
     heading  Plus Jakarta Sans 700 / 52px / line-height 67.6px /
              #111111, centred
     copy     Plus Jakarta Sans 400 / 16px / line-height 26.4px /
              #111111, 912px wide, margin-bottom 25.6px
     grid     3 x 380px columns, 10px inner padding -> 360 x 213
              cards, 20px gutters both ways
     card     white, border-radius 20px, padding 30px, NO shadow
     icon     50 x 50, accent colour
     number   Plus Jakarta Sans 800 / 38px / line-height 49.4px /
              accent, margin-bottom 4px
     label    Plus Jakarta Sans 400 / 20px / line-height 32px / #111

   DEVIATION — the reference's accent is #C30016, J&S Disposal's
   brand red. Dropping a red section into an otherwise blue page
   would read as a mistake, so this uses NJ's own --blue. One
   variable to flip back.

   DEVIATION — the reference's icons are a licensed icon font. These
   are hand-drawn SVGs in the same weight and outline style.
   ============================================================ */

.impact {
  --impact-accent: var(--blue);
  --impact-ink: var(--head-ink);

  position: relative;
  background: #FFFFFF;
  padding: 0 20px;
  overflow: hidden;
  font-family: 'Plus Jakarta Sans', var(--font);
}

/* The photo sits at 15% — the reference's exact value. It reads as
   a texture, not an image. */
.impact::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: url('/assets/img/about-panel.jpg');
  background-size: cover;
  background-position: 0% 50%;
  background-repeat: no-repeat;
  opacity: .15;
  pointer-events: none;
}

.impact__inner {
  position: relative;
  z-index: 1;
  max-width: 1140px;
  margin: 0 auto;
  padding: 70px 0;
}

.impact__title {
  margin: 0;
  font-family: 'Plus Jakarta Sans', var(--font);
  font-weight: 700;
  font-size: clamp(30px, 3.4vw, 52px);
  line-height: 1.3;                /* reference: 67.6px on 52px */
  text-align: center;
  color: var(--impact-ink);
}
.impact__title span { color: var(--impact-accent); }

.impact__copy {
  max-width: 912px;                /* reference paragraph width */
  margin: 0 auto 25.6px;
  font-family: 'Plus Jakarta Sans', var(--font);
  font-weight: 400;
  font-size: 16px;
  line-height: 1.65;               /* reference: 26.4px on 16px */
  text-align: center;
  color: var(--impact-ink);
}

.impact__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

/* 1140 / 3 = 380px cells; 10px all round leaves the reference's
   360 x 213 card and its 20px gutters in both directions. */
.impact__item { margin: 0; padding: 10px; }

.impact__card {
  height: 100%;
  min-height: 213px;               /* reference card height */
  background: #FFFFFF;
  border-radius: 20px;
  padding: 30px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.impact__icon {
  display: block;
  width: 50px;
  height: 50px;
  margin-bottom: 16px;
  color: var(--impact-accent);
}
.impact__icon svg { width: 100%; height: 100%; display: block; }

.impact__num {
  margin: 0 0 4px;
  font-family: 'Plus Jakarta Sans', var(--font);
  font-weight: 800;
  font-size: 38px;
  line-height: 1.3;                /* reference: 49.4px on 38px */
  color: var(--impact-accent);
}

.impact__label {
  margin: 0;
  font-family: 'Plus Jakarta Sans', var(--font);
  font-weight: 400;
  font-size: 20px;
  line-height: 1.6;                /* reference: 32px on 20px */
  color: var(--impact-ink);
}


/* ============================================================
   AREA — service-area checker
   Source: groundworks.com, the "branch finder" band.

   Reference measurements at a 1905px viewport:
     section  #FEF9E7, full width, padding 0 30px
     inner    1240px, flex, align-items center, padding 100px 0
     image    387 x 258, square corners
     gap      59px from image to the text column
     text     793px column
     h3       700 / 38px / line-height 47.5px / #172F53,
              margin 20px 0
     input    631 x 54, white, 2px border, radius 2px, padding 13px
     button   154 x 54, radius 3px, 700 / 16px, 8px from the input
     link     16px, #555, underlined, 40px under the input row

   DEVIATION — the reference's cream and gold are Groundworks'
   brand. This uses a blue tint and NJ's own blue.

   DEVIATION — the reference sets this in Mulish. Kept in Montserrat
   deliberately: the page is already carrying four families and a
   fifth for one band is not worth the download.
   ============================================================ */

/* A BOLD band, not a tint. The reference does this in a saturated
   green (rgb(60,180,67)); this is the same move in the brand blue.
   Everything inside therefore flips to on-dark, which is what the
   override block below is for.

   Two stops rather than one flat fill, plus the noise layer on
   ::before: a large flat saturated field is exactly what the display
   pipeline spatial-dithers into a visible diagonal. The stops are
   only a few steps apart — it does not need to read as a gradient,
   only to stop being flat. */
.area {
  --area-tint: #2F6FE5;
  --area-ink:  #FFFFFF;
  --area-soft: rgba(255, 255, 255, .82);

  position: relative;
  background: linear-gradient(180deg, #3A79EE 0%, #2A5FCC 100%);
  padding: 0 30px;
}

.area::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: .04;
  mix-blend-mode: overlay;
  pointer-events: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
}
/* No longer a flex row with a photo in it. The figure is lifted OUT
   of the flow and positioned against the band, so the band's height
   is set by the copy alone and the figure is free to overhang it —
   which is the whole effect. padding-left reserves his column. */
.area__inner {
  position: relative;
  z-index: 1;                       /* above the .area::before noise */
  max-width: 1240px;
  margin: 0 auto;
  padding: 100px 0 100px 360px;
  /* 360 clears the 336px figure with a little air and no more. It
     was 400, which pushed the copy further right than it needed to
     go; the copy is centred inside whatever is left. */
  min-height: 340px;
}

/* DEVIATION — the reference's image is 387 x 258. Enlarged to 500
   because at 387 the photograph had to carry a whole street scene
   in a box too small to read it. Bigger box, tighter crop; the text
   column still clears 680px, which is plenty for the heading and
   the search field. */
/* Stands ON the band's bottom edge and runs off the top of it.

   height is calc(100% + overhang) rather than a fixed size, so the
   overhang stays constant as the copy reflows; a fixed height would
   swing between floating and buried as the band grows. Percentages
   on an absolutely positioned box resolve against the containing
   block's PADDING box, which here is the whole band, so bottom:0
   lands him flush with the section's lower edge.

   z-index because he crosses into the section above — without it
   that section's background paints over his head. */
.area__cutout {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 2;
  /* 200px, not 78. The overhang has to clear his FACE, and 78 put the
     band's top edge across it at 15% down the figure. Measured the
     alpha width profile of the asset: the silhouette narrows to its
     minimum at 35% (the neck) and flares past 42% (the shoulders).
     The asset was recropped to reach the knees so the head is a
     smaller fraction of it; its neck now measures at 27.5% down.
     187px against a 448px band puts the edge at ~29% — just below
     the neck, on the top of the shoulders. Re-measure if the crop
     changes: the two numbers are bound together. */
  height: calc(100% + 187px);
  width: auto;
  max-width: none;
  display: block;
  pointer-events: none;
}

/* Centred type, and pulled toward the middle of the page rather than
   starting hard against the figure. Capped at 760 so centred lines
   stay readable — centred text has no consistent left edge for the
   eye to return to, so a long measure punishes it far more than a
   left-aligned one. margin auto then centres that cap inside the
   space the figure leaves. */
.area__body {
  min-width: 0;
  max-width: 760px;
  margin: 0 auto;
  text-align: center;
}

.area__title {
  margin: 0 0 20px;
  font-family: var(--font);
  font-weight: 700;
  font-size: clamp(24px, 2.4vw, 38px);
  line-height: 1.25;                /* reference: 47.5px on 38px */
  color: var(--area-ink);
}

.area__form {
  display: flex;
  gap: 8px;                         /* reference input-to-button gap */
  align-items: stretch;
  justify-content: center;
}

/* The input and its popup share a positioning context so the
   listbox hangs directly off the field. */
.area__combo { position: relative; flex: 1; min-width: 0; }

.area__options {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 20;
  margin: 0;
  padding: 6px;
  list-style: none;
  max-height: 288px;
  overflow-y: auto;
  background: #FFFFFF;
  border: 1px solid #D8E2F5;
  border-radius: 8px;
  box-shadow: 0 14px 30px rgba(20, 41, 79, .16);
}
.area__options[hidden] { display: none; }

.area__option {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 6px;
  font-size: 16px;
  color: var(--area-ink);
  cursor: pointer;
}
.area__option:hover,
.area__option.is-active { background: #EAF1FE; }

.area__tick { width: 20px; height: 20px; flex: none; fill: var(--blue); }
.area__option-name { font-weight: 600; }
.area__option-note {
  margin-left: auto;
  font-size: 13px;
  font-weight: 400;
  color: #5C6B86;
  white-space: nowrap;
}

/* "Did you mean" divider above the typo guesses, so a fuzzy hit can
   never read as a literal one. */
.area__group {
  padding: 10px 12px 4px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  /* Was a hardcoded #7A8296, which measures 3.85:1 on the popup's
     white card and fails AA for 12px text. Nobody had noticed because
     until the token re-scope this text was white on white and could
     not be read at all. Taking the popup's own --area-soft puts it at
     5.38:1 and means it follows the surface if that ever changes. */
  color: var(--area-soft);
}

.area__hint {
  margin: 10px 0 0;
  font-size: 14px;
  color: #5C6B86;
}

/* Announced, never shown. */
.area__sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Visually hidden but still read out — the placeholder alone is not
   a label. */
.area__label {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.area__input {
  /* Fills .area__combo, which is the flex child now that the input
     has a popup anchored to it. */
  width: 100%;
  display: block;
  height: 54px;                     /* reference input height */
  padding: 13px;
  border: 2px solid var(--blue);
  border-radius: 2px;
  background: #FFFFFF;
  font-family: var(--font);
  font-size: 16px;
  color: #111111;
}
.area__input::placeholder { color: #7A8296; }
.area__input:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }

.area__btn {
  flex: none;
  height: 54px;
  padding: 16px 48px;               /* reference button padding */
  border: 2px solid var(--blue);
  border-radius: 3px;
  background: var(--blue);
  color: #FFFFFF;
  font-family: var(--font);
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
  transition: background .2s linear, border-color .2s linear;
}
.area__btn:hover { background: var(--blue-hover); border-color: var(--blue-hover); }

/* Empty until the checker answers, so it takes no space up front. */
.area__result {
  margin: 0;
  font-size: 17px;
  line-height: 1.5;
  color: var(--area-ink);
}
.area__result:not(:empty) { margin: 20px 0 0; }
.area__result strong { font-weight: 700; }

.area__result .area__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 14px;
}
.area__result .area__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 28px;
  border-radius: 100px;
  border: 1px solid var(--blue);
  background: var(--blue);
  color: #FFFFFF;
  font-size: 15px;
  font-weight: 700;
  transition: background .2s linear;
}
.area__result .area__cta:hover { background: var(--blue-hover); }
.area__result .area__cta--ghost {
  background: transparent;
  color: var(--area-ink);
  border-color: var(--area-ink);
}
.area__result .area__cta--ghost:hover { background: var(--area-ink); color: #FFFFFF; }

/* Candidate towns offered when a query is ambiguous. */
.area__result .area__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 14px;
}
.area__result .area__chip {
  padding: 9px 18px;
  border: 1px solid var(--blue);
  border-radius: 100px;
  background: #FFFFFF;
  font-family: var(--font);
  font-size: 15px;
  font-weight: 600;
  color: var(--blue);
  transition: background .15s linear, color .15s linear;
}
.area__result .area__chip:hover { background: var(--blue); color: #FFFFFF; }

/* Reference styles this as a plain underlined link; it is a button
   here because it toggles rather than navigates. */
.area__link {
  margin-top: 40px;                 /* reference gap under the row */
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font);
  font-size: 16px;
  color: #555555;
  text-decoration: underline;
}
.area__link:hover { color: var(--area-ink); }

.area__towns {
  list-style: none;
  margin: 16px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 6px 20px;
  font-size: 15px;
  line-height: 1.6;
  color: var(--area-ink);
}
.area__towns[hidden] { display: none; }


/* ============================================================
   TALK — contact block
   Source: trugreen.com, "Let's talk lawn."
   Third reference site; the type here is theirs (DM Sans for
   headings, Inter for everything else).

   Reference measurements at a 1905px viewport:
     section  white, padding 80px 0
     rail     1250px centred
     h2       DM Sans 500 / 40px / line-height 44px /
              letter-spacing -2px / #222222, left aligned
     row      822px left block + 32px gap + 396px right column
     panel    411 x 657, #F2FBF5, radius 24px 0 0 24px, padding 40
     photo    411 x 657, radius 0 24px 24px 0, cover
     h3       DM Sans 500 / 32px / line-height 38.4px, mb 24px
     list     Inter 400 / 16px / 24px / #414141, 7px apart,
              30px check icon, text inset 46px from the panel edge
     note     Inter 16px / 24px / #414141, margin 24px 0
     button   48px tall, radius 100px, padding 0 56px,
              Inter 500 16px, 16px apart
     card     396 wide, #F2FBF5, radius 24px, padding 24px,
              32px apart
     h4       DM Sans 500 / 24px / line-height 28.8px
     sub      Inter 16px / 24px, margin 8px 0
     hours    Inter 14px / 19.6px

   DEVIATION — the reference's green (#1EE576 pills on a #F2FBF5
   tint) is TruGreen's brand. This uses NJ's blue and a blue tint
   mixed to the same lightness. Pill text goes white because the
   blue is dark; theirs is #222 on bright green.

   DEVIATION — CTAs are not copied. The reference offers Chat Now,
   Log In, a text short code and Email Us. This build has two
   actions only: Call Now and Book Now.
   ============================================================ */

.talk {
  --talk-tint: #F1F5FD;      /* the blue equivalent of their #F2FBF5 */
  --talk-ink:  var(--head-ink);
  --talk-body: #414141;

  background: #FFFFFF;
  padding: 80px 20px;
  font-family: 'Inter', var(--font);
}

.talk__inner {
  max-width: 1250px;
  margin: 0 auto;
}

.talk__title {
  margin: 0 0 64px;
  font-family: 'DM Sans', var(--font);
  font-weight: 500;
  font-size: clamp(30px, 2.6vw, 40px);
  line-height: 1.1;                 /* reference: 44px on 40px */
  letter-spacing: -2px;
  color: var(--talk-ink);
}

/* 822 + 32 + 396 = 1250. The fr values are the reference's own
   column widths, so the split holds at any rail width. */
.talk__row {
  display: grid;
  grid-template-columns: 822fr 396fr;
  gap: 32px;
  align-items: stretch;
}

/* Panel and photo are one block: each takes half, and only the
   outer corners are rounded so they read as a single card. */
.talk__main {
  display: grid;
  /* minmax(0,1fr) rather than 1fr: a plain 1fr floors at the track's
     min-content, so the panel's nowrap buttons were pushing it past
     411 and stealing width from the photo. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  min-height: 657px;                /* reference row height */
}

.talk__panel {
  background: var(--talk-tint);
  border-radius: 24px 0 0 24px;
  padding: 40px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.talk__photo {
  width: 100%;
  height: 100%;
  min-height: 0;
  object-fit: cover;
  object-position: 50% 50%;
  border-radius: 0 24px 24px 0;
  display: block;
}

.talk__heading {
  margin: 0 0 24px;
  font-family: 'DM Sans', var(--font);
  font-weight: 500;
  font-size: clamp(24px, 1.9vw, 32px);
  line-height: 1.2;                 /* reference: 38.4px on 32px */
  color: var(--talk-ink);
}

.talk__list {
  list-style: none;
  margin: 0 0 24px;
  padding: 0;
}
.talk__list li {
  display: flex;
  align-items: center;
  gap: 16px;                        /* 30px icon + 16 = 46px inset */
  margin: 0 0 7px;
  font-size: 16px;
  line-height: 1.5;                 /* reference: 24px on 16px */
  color: var(--talk-body);
}
.talk__list li:last-child { margin-bottom: 0; }

/* The reference's is a 30px SVG on li::before. Redrawn inline so it
   takes the accent colour rather than shipping a second asset. */
.talk__tick {
  flex: none;
  width: 30px;
  height: 30px;
  display: block;
  color: var(--blue);
}
.talk__tick svg { width: 100%; height: 100%; display: block; fill: currentColor; }

.talk__note {
  margin: 24px 0;
  font-size: 16px;
  line-height: 1.5;
  color: var(--talk-body);
}

/* The reference's two buttons come out 155px each on a 331px panel
   with a 16px gap — i.e. they split the row evenly rather than
   sizing to their labels. Same here. */
.talk__actions {
  display: flex;
  gap: 16px;
}
.talk__actions > * {
  flex: 1 1 0;
  min-width: 0;                     /* so they land equal, not label-sized */
  padding-left: 16px;
  padding-right: 16px;
}

/* Reference pill: 48px tall, radius 100px, padding 0 56px. */
.talk__btn,
.talk__pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0 56px;
  border-radius: 100px;
  font-family: 'Inter', var(--font);
  font-size: 16px;
  font-weight: 500;
  line-height: 1;
  text-align: center;
  white-space: nowrap;
  transition: background .2s linear, border-color .2s linear, color .2s linear;
}

.talk__btn--solid,
.talk__pill {
  background: var(--blue);
  border: 1px solid var(--blue);
  color: #FFFFFF;
}
.talk__btn--solid:hover,
.talk__pill:hover { background: var(--blue-hover); border-color: var(--blue-hover); }

.talk__btn--ghost {
  background: transparent;
  border: 1px solid var(--talk-ink);
  color: var(--talk-ink);
}
.talk__btn--ghost:hover { background: var(--talk-ink); color: #FFFFFF; }

.talk__side {
  display: flex;
  flex-direction: column;
  gap: 32px;                        /* reference gap between cards */
}

.talk__card {
  background: var(--talk-tint);
  border-radius: 24px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* The reference's two cards are 384 and 241 tall and together fill
   the row exactly. Dropping its text and email CTAs left this
   column short, so the cards grow in the reference's own 384:241
   proportion to fill the height instead of leaving a void. */
.talk__side > :first-child { flex: 384 1 0; }
.talk__side > :last-child  { flex: 241 1 0; }

.talk__cardhead {
  margin: 0;
  font-family: 'DM Sans', var(--font);
  font-weight: 500;
  font-size: 24px;
  line-height: 1.2;                 /* reference: 28.8px on 24px */
  color: var(--talk-ink);
}

.talk__sub {
  margin: 8px 0;
  font-size: 16px;
  line-height: 1.5;
  color: var(--talk-body);
}

.talk__hours {
  margin: 0 0 20px;
  font-size: 14px;
  line-height: 1.4;                 /* reference: 19.6px on 14px */
  color: var(--talk-body);
}
.talk__hours strong { font-weight: 700; }


/* ============================================================
   FOOTER

   The background is deliberately NOT a flat dark fill. A large area
   of perfectly even charcoal is what display pipelines spatial-
   dither to hide 8-bit banding, and that dither reads as a faint
   diagonal hairline running the width of the section — it survives
   screenshots, browsers and monitors, because it happens before the
   framebuffer is read. A two-stop gradient plus a low-opacity noise
   layer leaves the pattern nothing to lock onto. The two stops are
   only a few luminance steps apart; it does not need to look like a
   gradient, only to stop being flat.
   ============================================================ */

.noise-overlay {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>");
}

/* THE FOOTER IS THE INK BAND, with white type on it.

   IT WENT LIGHT ONCE AND CAME BACK. The complaint was that the
   colours "invert" at the bottom of the page, so it was rebuilt on
   --surface with dark type — and then the actual objection turned out
   to be the LOGO rather than the band: the white-out logo-white.png
   threw away the NJ mark's blue and green. The band is dark again and
   the logo is logo-white-wordmark.png, which keeps the mark's brand
   colours and whites only the "INDOOR OUTDOOR MAINTENANCE LLC" line
   under it. If the light version is ever wanted back, git has it.

   THE NOISE LAYER IS NOT DECORATION AND MUST NOT BE DROPPED. A flat
   dark fill is what the display pipeline spatial-dithers into a
   visible diagonal hairline across the band. Two stops five luminance
   steps apart plus 3.5% noise give the dither nothing to latch onto.
   The <div class="noise-overlay"> in every v2 footer is that layer —
   if this ever goes light again, the div comes out WITH the gradient,
   never one without the other.

   CONTRAST, measured on #131B2E:
     #FFFFFF phone, hovers ...... 17.16
     rgba(255,255,255,.76) body .. 10.28
     rgba(255,255,255,.62) muted .. 7.24
     rgba(255,255,255,.55) bar .... 5.97
     rgba(255,255,255,.52) labels . 5.47

   THE LABEL ALPHA WAS .45 AND FAILED. That is 4.43:1 against a 4.5
   floor — .foot__head and .foot__hours span are 12px uppercase, which
   is body copy, not large text. Raised to .52. Do not take it back
   down; the band is fixed and the alpha is the only variable. */
.foot {
  position: relative;
  overflow: hidden;
  background: linear-gradient(180deg, #131B2E 0%, #0F172A 100%);
  color: rgba(255, 255, 255, .76);
  padding: 80px 20px 32px;
  font-size: 15px;
  line-height: 1.7;
}

.foot > .noise-overlay {
  position: absolute;
  inset: 0;
  opacity: .035;
  mix-blend-mode: overlay;
  pointer-events: none;
}

/* Everything sits above the noise layer. */
.foot__inner,
.foot__areas,
.foot__bar { position: relative; z-index: 1; }

.foot__inner {
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  /* The contact column is the widest of the three narrow ones so the
     email address fits on one line — it was breaking mid-word. */
  grid-template-columns: 1.5fr 1fr 1fr 1.6fr;
  gap: 48px;
}

.foot__logo { display: inline-block; }
.foot__logo img { width: auto; height: 58px; }

.foot__blurb {
  margin: 20px 0 0;
  max-width: 42ch;
  color: rgba(255, 255, 255, .70);
}

.foot__hours {
  margin: 18px 0 0;
  font-size: 14px;
  color: rgba(255, 255, 255, .62);
}
.foot__hours span {
  display: block;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  font-size: 12px;
  color: rgba(255, 255, 255, .52);
}

.foot__head {
  margin: 0 0 16px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .52);
}

.foot__col ul { list-style: none; margin: 0; padding: 0; }
.foot__col li { margin-bottom: 9px; }
.foot__col li:last-child { margin-bottom: 0; }

.foot a { color: rgba(255, 255, 255, .76); transition: color .18s linear; }
.foot a:hover { color: #FFFFFF; }

.foot__phone {
  display: block;
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -.4px;
  color: #FFFFFF;
  line-height: 1.3;
}
.foot__phone:hover { color: var(--blue-hi, #6E9CFF); }

.foot__link {
  display: block;
  margin-top: 8px;
  font-size: 13.5px;
  /* break-word, not anywhere: if it ever does have to wrap, let it
     do so at a sensible point rather than mid-word. */
  overflow-wrap: break-word;
}

.foot__address {
  margin: 14px 0 0;
  font-style: normal;
  font-size: 14px;
  color: rgba(255, 255, 255, .62);
}

.foot__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  margin-top: 20px;
  padding: 0 30px;
  border-radius: 100px;
  background: var(--blue);
  color: #FFFFFF;
  font-size: 15px;
  font-weight: 700;
  transition: background .2s linear;
}
.foot__cta:hover { background: #4B87F2; color: #FFFFFF; }

.foot__areas {
  max-width: 1200px;
  margin: 56px auto 0;
  padding-top: 32px;
  border-top: 1px solid rgba(255, 255, 255, .10);
}
.foot__areas p {
  margin: 0;
  font-size: 13.5px;
  line-height: 2;
  color: rgba(255, 255, 255, .52);
}

.foot__bar {
  max-width: 1200px;
  margin: 32px auto 0;
  padding-top: 24px;
  border-top: 1px solid rgba(255, 255, 255, .10);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
  font-size: 13.5px;
  color: rgba(255, 255, 255, .55);
}
.foot__bar p { margin: 0; }
.foot__bar ul {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  margin: 0;
  padding: 0;
}


/* ============================================================
   RESPONSIVE
   ============================================================ */

@media (max-width: 1200px) {
  .primary-nav__list > li > a { padding: 0 18px; font-size: 16px; }
  .primary-nav { gap: 18px; }
}

@media (max-width: 992px) {
  .main-header { padding: 0 20px; }
  .nav-toggle { display: flex; }

  .primary-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 8px 20px 22px;
    background: #fff;
    border-bottom: 1px solid var(--bar-line);
    box-shadow: 0 14px 26px rgba(0, 0, 0, .10);
    display: none;
  }
  .primary-nav.is-open { display: flex; }

  .primary-nav__list { flex-direction: column; align-items: stretch; }
  .primary-nav__list > li > a {
    padding: 13px 0;
    line-height: 1.2;
    font-size: 17px;
    color: var(--nav-text);
    text-shadow: none;
    border-bottom: 1px solid #F1F1F1;
  }
  .primary-nav__list > li > a:hover { opacity: 1; color: var(--nav-hover); }

  .sub-menu {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    border: 0;
    box-shadow: none;
    padding: 0 0 6px 16px;
    min-width: 0;
  }
  .sub-menu a { padding: 10px 0; }

  .header-cta { margin-top: 16px; text-align: center; }

}

/* Portrait players stay four-across much longer than landscape ones
   would — they are narrow by nature, so the row only folds when each
   drops under ~250px, which is where Chrome starts pulling buttons
   out of its own control bar. */
@media (max-width: 1099px) {
  .showcase__grid { grid-template-columns: repeat(2, 1fr); row-gap: 40px; }
}

@media (max-width: 1099px) {
  .services__grid { grid-template-columns: repeat(2, 1fr); }
}

/* Video over copy. It stays first — it is the thing the section is
   built around, and burying it under a paragraph on a phone would
   defeat the point. */
/* The copy column stops holding a readable line before the layout
   itself breaks, so it folds early. */
/* 860, not 1180. The old value was set when this column held a 16:9
   player, which runs out of width early; a 4:5 portrait sits happily
   beside the copy far longer. minmax(0, 1fr) rather than 1fr for the
   usual reason — a bare 1fr floors at min-content. */
@media (max-width: 860px) {
  .about__inner { grid-template-columns: minmax(0, 1fr); gap: 36px; max-width: 620px; }
  /* No padding override here: the max-width:900 block below sets it,
     and it wins at every width this block applies to. Declaring 80px
     here was dead code. */
  /* Centred once stacked; the right-align only makes sense when it is
     hugging the copy column. */
  .about__portrait { margin-left: auto; margin-right: auto; max-width: 380px; }
}

@media (max-width: 900px) {
  .about { padding: 72px 20px; }
  .about__inner { gap: 32px; }

  /* Brand block spans the top, the three link columns sit under it. */
  .foot { padding: 64px 20px 28px; }
  .foot__inner { grid-template-columns: repeat(2, 1fr); gap: 36px 32px; }
  .foot__brand { grid-column: 1 / -1; }
  .foot__areas { margin-top: 40px; }
}

@media (max-width: 560px) {
  .foot__inner { grid-template-columns: 1fr; gap: 32px; }
  .foot__logo img { height: 48px; }
  .foot__bar { flex-direction: column; align-items: flex-start; }
  .foot__areas p { line-height: 1.9; }
}

@media (max-width: 560px) {
  .about { padding: 56px 16px; }
  .about__lede { font-size: 16px; }
  .about__actions > * { flex: 1 1 auto; }
}

@media (max-width: 992px) {
  .showcase { padding: 72px 20px; }
  .services  { padding: 72px 20px; }
  .reviews   { padding: 60px 20px 20px; }
  .steps     { padding: 72px 20px; }
  /* two cards visible */
  .reviews__track { grid-auto-columns: calc((100% - 16px) / 2); }
}

@media (max-width: 900px) {
  .impact__grid { grid-template-columns: repeat(2, 1fr); }
  .impact__inner { padding: 56px 0; }
}

@media (max-width: 980px) {
  .area { padding: 0 20px; }

  /* He moves from beside the copy to underneath it. The band stops
     reserving a left column and he stands on its bottom edge in
     normal flow instead — still overhanging nothing above, because
     at this width there is no room for him to overlap the copy.

     Flex only so that `order` is available. The figure is FIRST in
     the source, because on desktop it is positioned out of the flow
     and the source order there is arbitrary; in flow that same order
     would put him above the copy, which is where he was and what was
     wrong with it.

     Why the bottom is the only place he works: the cut-out ends at
     the belt. Above the copy that crop is a horizontal line across
     his waist with a search form under it, and it reads as a broken
     image. Flush against the band's lower edge the crop stops being
     a crop — the band cuts him off, so he reads as standing behind
     it, which is the same trick the desktop layout plays at the top
     edge with his head. The heading and the town search go above him
     for the same reason: he is the thing you look at last.

     Nothing here may reintroduce padding-bottom. The effect is the
     figure meeting the band's edge exactly; a gap under him turns
     him back into a sticker floating in a blue box. */
  .area__inner {
    display: flex;
    flex-direction: column;
    padding: 64px 0 0;
    min-height: 0;
  }
  .area__cutout {
    order: 1;
    position: relative;
    height: auto;
    width: 210px;
    /* -1px, not 0: at fractional device pixel ratios a flush edge
       can leave a hairline of the section below showing under him. */
    margin: 24px auto -1px;
    left: auto;
    bottom: auto;
  }
  .area__towns { grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 620px) {
  /* Top only — see the note above on padding-bottom. */
  .area__inner { padding: 48px 0 0; }
  .area__form { flex-direction: column; }
  .area__btn { width: 100%; padding: 16px 24px; }
  .area__towns { grid-template-columns: repeat(2, 1fr); }
  .area__link { margin-top: 28px; }
}

/* The right column drops under the main block and lays its two
   cards out side by side instead. */
@media (max-width: 1100px) {
  .talk__row { grid-template-columns: 1fr; }
  .talk__side { flex-direction: row; }
  .talk__side > * { flex: 1; }
  .talk__main { min-height: 560px; }
}

/* Panel and photo stack. The photo goes on top and the rounded
   corners move with it so the pair still reads as one card. */
@media (max-width: 760px) {
  .talk { padding: 56px 16px; }
  .talk__title { margin-bottom: 32px; }
  /* minmax(0, 1fr), not 1fr — the same floor-at-min-content trap the
     two-column rule above documents. A plain 1fr let the panel's
     nowrap CTA buttons set the track width, which pushed the whole
     page 38px wider than a 320px screen and gave the site a real
     horizontal scroll. Reproduce by measuring documentElement
     scrollWidth against clientWidth at 320px. */
  .talk__main { grid-template-columns: minmax(0, 1fr); min-height: 0; }
  /* The source is a 822x1225 portrait, so a full-width band this short
     crops it hard: at 375px the image renders 511px tall inside a ~290px
     window, and the default 50% centre put the top edge at 24% of the
     image — right through the man's forehead (his head runs 21%-32%).
     22% pins the window near the top instead, which leaves about a fifth
     of the band as headroom at every width from 320 to 760. The height
     tracks the viewport so that headroom stays constant rather than
     collapsing into a head-and-shoulders sliver on wide phones. */
  .talk__photo {
    order: -1;
    height: clamp(280px, 78vw, 380px);
    object-position: 50% 22%;
    border-radius: 24px 24px 0 0;
  }
  .talk__panel { border-radius: 0 0 24px 24px; padding: 28px 24px; }
  .talk__side { flex-direction: column; }
  .talk__btn, .talk__pill { padding: 0 32px; flex: 1 1 auto; }
}

/* The hero's two columns stop fitting the fold side by side once
   the form column is squeezed under ~340px. Copy over form, both
   left-aligned, and the fold constraint is dropped: a stacked hero
   with a four-field form legitimately runs past one screen, and
   forcing it would shrink the type to nothing. */
@media (max-width: 960px) {
  .hero__inner {
    grid-template-columns: 1fr;
    gap: 28px;
    min-height: 0;
    padding-top: calc(var(--header-h) + 32px);
    padding-bottom: 44px;
  }
  .hero__copy { max-width: none; }
  .hero__form { max-width: 460px; }
  .hero__actions > * { flex: 1 1 auto; }
}

/* ============================================================
   HERO ONCE THE LAYOUT STACKS
   The wide hero is a full-bleed film with the copy laid over it.
   That inverts on a narrow screen, and the arithmetic is the whole
   reason why:

   object-fit: cover scales to the LARGER ratio. At 390x740 against a
   1920x1080 clip that is max(390/1920, 740/1080) = 0.685, so the film
   renders 1315px wide and only 390 of it survives — under a third of
   the frame. The truck sits outside that slice, which is why the film
   read as "sky and a telegraph pole". Worse, the vertical fit is then
   exact, so object-position's Y value does nothing at all and the
   framing cannot be nudged back.

   So on phones the film stops being a background and becomes a band
   at the top of the fold. At 390x340 the same maths gives a 0.315
   scale, 605px wide, keeping ~64% of the frame — the truck reads at a
   glance. The copy then sits below it on the gradient, and the form
   moves down past the fold, which is the natural place for it: the
   film and the headline earn the scroll, the form collects it.
   ============================================================ */
/* ------------------------------------------------------------
   MOVE THE HERO COPY DOWN THE FILM

   Roughly 75px on a normal phone, asked for as an amount that should
   scale rather than a fixed number.

   9svh is 76px on a 390x844 phone, easing to 52 on a short one and
   capped at 84. svh rather than vh: vh on iOS is the viewport with
   browser chrome RETRACTED, so a vh-based offset would shrink as you
   scroll and the copy would drift up the film mid-scroll.

   It moves the copy DOWN the frame, which is also the safer direction
   for contrast. The notes below record that the middle of this clip
   is open sky and the bottom fifth is the truck's body and shadowed
   asphalt, measured at Y=58.5/255 — white type is about 2.5:1 on the
   first and about 11:1 on the second. Every pixel this shifts is
   toward the dark end.
   ------------------------------------------------------------ */
@media (max-width: 960px) {
  :root { --hero-drop: clamp(52px, 9svh, 84px); }
}

/* Tied to 960 — the same breakpoint where the grid stacks — rather
   than to a phone width. The rule is: the moment the copy stops
   sitting BESIDE the film and starts sitting ON it, the film becomes
   a band. Below 960 the form would otherwise cover the truck at every
   width, not just on phones. */
@media (max-width: 960px) {
  :root {
    /* Header-inclusive, because the band starts behind the floating
       nav — this is what guarantees the film's VISIBLE height, not
       its layout height.

       Grown from 33svh to 40svh so the film keeps more of the fold
       and the headline has somewhere dark to sit (see the overlap
       note below). The cost is real and worth stating: a taller band
       means cover scales the clip up, so LESS of its width survives.
       At 390px this moves the visible slice from ~54% of the frame
       to ~46%. Past roughly 45svh the truck starts losing its nose. */
    --hero-band: calc(var(--header-h) + clamp(450px, 69svh, 675px));
  }

  .hero__video {
    height: var(--hero-band);
    bottom: auto;
    /* 46% rather than centred: the cab and body sit left of frame
       centre, so the surviving slice is aimed at the truck instead of
       at the roadside behind it. */
    object-position: 46% 50%;
  }

  /* NO FADE at the seam. There was a gradient here that dissolved the
     film's bottom edge into the section below it. It softened the
     seam, but it did so by washing out the last ~170px of footage —
     the wheels and the road, the part that makes it read as a working
     truck. The edge is crisp: the film is fully visible right down to
     its last row of pixels, and it simply stops. */

  .hero__inner {
    /* The headline straddles the seam: its first line sits ON the
       film, the rest runs onto the solid below. No gradient is needed
       to make that legible, which was the thing worth checking rather
       than assuming — the bottom of this frame is the truck's lower
       body and shadowed asphalt, measured at Y=58.5/255 (range
       53-63 across the loop). White type on that is about 11:1, the
       cleanest contrast anywhere in the hero. It was the MIDDLE of
       the frame — open sky — that was never going to hold type.

       56px of overlap, against a dark bottom fifth that is ~75px
       tall at this band height. If the band shrinks or the clip is
       replaced, re-measure before trusting this:
         ffmpeg -i clip.mp4 -vf "crop=iw*0.5:ih*0.2:iw*0.21:ih*0.8,\
           signalstats,metadata=print:key=lavfi.signalstats.YAVG" -f null - */
    /* 56px of overlap became 20px when the eyebrow arrived. The 56 was
       measured for a headline first line landing on the film's dark
       bottom fifth. The eyebrow now sits above the headline and is 35px
       tall with its margin, so 56 put IT on the film instead — 35px
       higher up, on pale asphalt and the white wall, where it measured
       2.5:1. At 20 the eyebrow clears onto the solid below with only its
       top edge over the film, and the headline follows it down.

       Then 20 became +8, i.e. no overlap at all, because 20 still left
       the eyebrow at 2.2:1 on this band height — at 820 the film's
       bottom edge lands on pale asphalt and a white retaining wall,
       which is the brightest strip in the whole clip. There is no
       offset that puts small uppercase type over that and keeps it
       readable.

       So the straddle is gone on the band layout. It was a real detail
       and it is worth saying what it cost: the seam is now a clean line
       with the copy starting below it, rather than the headline
       crossing it. What it bought is every line of hero copy sitting on
       the solid charcoal at about 17:1. Readability was the explicit
       requirement; the straddle was in service of it and stopped
       serving it once the eyebrow went in above the headline. */
    padding-top: calc(var(--hero-band) + 8px + var(--hero-drop));
    padding-bottom: 40px;
    /* The gap IS the "move the form down" — it separates the CTAs
       from the form rather than padding the form itself, so the two
       never collide as the headline rewraps. */
    gap: 34px;
  }

  .hero__title { font-size: clamp(29px, 8.2vw, 40px); }
  .hero__text  { margin-top: 14px; max-width: none; }
}

@media (max-width: 560px) {
  /* 22px, not 20: at 20 the headline's cap-height sat visually flush
     with the screen edge on a 360px phone. */
  .hero__inner { padding-left: 22px; padding-right: 22px; }

  /* The 2x2 grid holds; only the spacing tightens. The old rule here
     existed to stop the pills wrapping to a third row, which the
     grid makes impossible by construction. */
  .hero__badges { gap: 12px 14px; }
  .hero__badges li { gap: 9px; font-size: 12.5px; }
  .hero__badge-ico { width: 30px; height: 30px; }
  .hero__badge-ico svg { width: 16px; height: 16px; }
}

/* Three circles stop working well once each is under ~200px, and a
   2 + 1 row leaves an orphan, so they stack. */
@media (max-width: 820px) {
  .steps__grid { grid-template-columns: 1fr; row-gap: 56px; }
  .steps__name { margin-top: 32px; }
  .steps__photo { max-width: 300px; }
  .steps__copy { max-width: 46ch; margin: 0 auto; }
}

@media (max-width: 620px) {
  /* ============================================================
     SHOWCASE ON PHONES — a swipeable rail, not a stack

     Stacked, four portrait players ran roughly 1,800px of vertical
     scroll for what is meant to be a glance at the work. As a rail
     they occupy one screen and the second card peeking at the edge
     is what tells you to swipe — so no arrows are needed at this
     size, where they would be too small to hit anyway.

     grid-auto-flow: column keeps the same grid the wide layout uses
     rather than switching to flex, so .showcase__item needs no
     changes. 78% columns deliberately: at 100% there is nothing
     peeking and the rail reads as a stack that has stopped working.

     overscroll-behavior-x: contain is not optional here. Without it
     a swipe that reaches the end of the rail CHAINS to the page and
     drags the whole document sideways, which is indistinguishable
     from the page being broken.
     ============================================================ */
  .showcase__grid {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 78%;
    row-gap: 0;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding: 0 6px;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 4px;

    /* THIS is the sideways-scroll bug, and it is not overscroll.

       `overflow-x: auto` clips PAINT but does not make the element a
       containing block. An absolutely positioned descendant whose
       nearest positioned ancestor is further up the tree therefore
       resolves against the initial containing block: it is clipped
       by nothing, and it contributes its full offset to the
       DOCUMENT's scrollable width. Four video players sit in this
       rail, each of which acquires such a descendant — the native
       control layer on a phone, and a browser extension's injected
       node on my desktop. Same mechanism, two triggers, and it
       measures identically: 593px of overflow at a 430px viewport,
       on the reporting device and here.

       That is also why every earlier attempt missed it. body-level
       overflow reads 0 because body genuinely does not overflow —
       only the root does, via the initial containing block. And
       `overflow: hidden` on this element changes nothing, because
       the clipping was never the problem.

       `contain: paint` fixes it properly: it makes this element a
       containing block for absolutely positioned descendants, so
       they can no longer escape to the root. */
    contain: paint;
  }
  .showcase__grid::-webkit-scrollbar { display: none; }
  .showcase__item { scroll-snap-align: start; }
}

@media (max-width: 768px) {
  :root { --logo-h: 52px; }
  .above-header__btn { font-size: 12px; padding: 6px 12px; }
  .hero__title { letter-spacing: -.5px; }
  .hero__form { padding: 22px 18px; }
  .showcase__text { font-size: 16px; line-height: 1.7; }
}

@media (max-width: 560px) {
  .showcase { padding: 56px 14px; }
  .showcase__head { padding: 0 6px; }
  .showcase__item { padding: 0 6px; }

  .services { padding: 56px 14px; }
  .services__grid { grid-template-columns: 1fr; }
  .services__item { padding: 8px 6px; }
  .services__head { padding: 0 6px; }
  .services__cta  { margin-top: 40px; }

  .impact { padding: 0 14px; }
  .impact__inner { padding: 44px 0; }
  .impact__card { min-height: 0; padding: 24px 18px; }
  .impact__num { font-size: 32px; }
  .impact__label { font-size: 17px; }

  .steps { padding: 56px 14px; }
  .steps__head { padding: 0 6px; }
  .steps__item { padding: 0 6px; }
  .steps__copy { font-size: 16px; }

  .reviews { padding: 48px 14px 20px; }
  .reviews__head { padding: 0 6px; }
  .reviews__text { font-size: 16px; line-height: 1.7; }
  /* THE SCROLL AFFORDANCE, and the reason it is two things.

     This used to be grid-auto-columns: 100% with the arrows hidden,
     which meant a phone saw one card filling the row and nothing else:
     no arrows, no scrollbar (it is suppressed above), no second card.
     It read as a single testimonial that happened to have five more
     hidden behind it, and there was no reason to swipe.

     86% is the fix that needs no explaining. The next card's left edge
     sits about 30px into the row on a 390 screen, and a slice of card
     hanging off the edge is the one signal everyone already reads as
     "this moves". The card is still 86% wide, so nothing is cramped.

     The dots are the other half, and they answer the question the peek
     does not: HOW MANY. A peek says there is more; six dots say there
     are six and you are on the first. Together they also cover the
     scrolled-to-the-end case, where the peek has nothing left to show
     and only the dots still report position.

     Deliberately NOT done: a fade or mask over the right edge. It
     greys the card it is meant to advertise, and with the peek already
     there it is a second signal saying the same thing less clearly. */
  .reviews__track { grid-auto-columns: 86%; }
  .reviews__arrow { display: none; }
  .reviews__carousel { gap: 0; }
  .reviews__dots { display: flex; }
}

@media (max-width: 480px) {
  :root { --logo-h: 44px; }
  .above-header__btn { font-size: 11px; letter-spacing: .2px; }
  .main-header { padding: 0 16px; }
}

/* Short viewports — phones in landscape, small laptops, split
   screens. The bar has to give ground here or the header eats a
   third of the fold; --hero-gap is already tracking vh. The logo
   comes down only as far as the bar forces it to. */
@media (max-height: 620px) {
  :root {
    --bar-h:    30px;
    --header-h: 62px;
    --logo-h:   48px;
  }
  .above-header__btn { font-size: 11px; padding: 5px 12px; }
  .primary-nav__list > li > a { font-size: 15px; }
  .header-cta { font-size: 13px; padding: 8px 26px; }
  .nav-toggle { width: 40px; height: 32px; }
}

/* Very short — the announcement bar is the first thing to go, it
   is the least load-bearing element on the page. */
@media (max-height: 460px) {
  /* --hero-drop goes with it. This is a phone on its side: the whole
     fold is 390px tall, the announcement bar has already been dropped
     to buy some of it back, and spending another 52 pushing the copy
     down would undo that. The band layout is doing something
     different here anyway — there is no room for the copy to sit low
     on the film, so it starts near the top and scrolls. */
  :root { --bar-h: 0px; --hero-drop: 0px; }
  .above-header { display: none; }
}

/* ============================================================
   HEADING FACE — DM Sans everywhere EXCEPT the home hero

   Taken from .talk__title, which was the one heading already set in
   DM Sans (weight 500, tracking -2px) because the Let's-talk block
   was copied from trugreen.com. Maaz wanted to see it carry the
   whole page.

   Applied as ONE block at the end of the file rather than by editing
   a dozen rules in place. Two reasons: it wins on source order
   without touching specificity, and reverting is deleting this block
   plus the DM Sans weights from the font link — the per-section
   rules below still say var(--font) and are untouched.

   Notably this also overrides the impact section's Plus Jakarta Sans,
   which was matched deliberately to jsdisposalnj.com. That match is
   gone while this is in force; that is the trade for one face.

   Tracking is set per size band, not globally. DM Sans is a wide
   geometric face and Montserrat's tracking does not transfer: at
   display sizes it needs pulling in hard, at small sizes barely at
   all, and applying one value everywhere is what makes a swapped
   face look wrong more often than the face itself does.

   THREE HEADINGS ARE NOT IN THIS LIST — .hero__title, the quote
   box's .hero__form-title, and .about__title. Those are the build's
   Montserrat headings, chosen off design-lab/hero-font.html; see the
   block at the end of this file. Leaving them out is the whole
   mechanism: their own rules already say var(--font), so dropping
   them from the selector list below is what hands them back to
   Montserrat. Do not re-add any of them here without deleting that
   block too.
   ============================================================ */

:root { --font-head: 'DM Sans', var(--font); }

.services__label,
.steps__name,
.impact__num,
.impact__label,
.talk__heading,
.talk__cardhead,
/* .foot__head is a 12px uppercase label, not a display heading. It
   takes the face but is deliberately left out of every tracking rule
   below — it already carries +.14em, and negative tracking on caps
   closes the letters into each other. */
.foot__head {
  font-family: var(--font-head);
}

/* Mid sizes sit between, and the small labels keep their own
   spacing — .services__label and .impact__label are set in caps or
   near it, where negative tracking closes the letters up. */
.steps__name,
.talk__heading,
.impact__num { letter-spacing: -.8px; }

/* ============================================================
   TITLE CASE ON DISPLAY HEADINGS

   The six display headings were text-transform: uppercase, carried
   over from coconutcleaningco.com. Removed rather than swapped for
   `capitalize`, and the distinction is worth stating: `capitalize`
   capitalises EVERY word, so the hero would read "Rental In Central
   NJ" and the showcase "See The Difference A Clear-Out Makes."
   Removing the transform renders the source text as authored, and
   that text was already written in proper title case with small
   words left down — "in", "the", "a".

   Tracking is inherited from the DM Sans block above and holds up in
   mixed case; uppercase-specific tracking would need loosening, but
   these were already set for a wide geometric face.

   STILL UPPERCASE, deliberately: the announcement bar, the Book Now
   button, form field labels, the About eyebrow, the reviews grade,
   and the footer column heads. Those are small caps LABELS, where
   all-caps is the convention and title case reads as a mistake. The
   About eyebrow in particular was matched to
   naturescompanionlandscaping.com, which sets it that way.
   ============================================================ */

/* ============================================================
   SERVICE-AREA BAND — ON-DARK OVERRIDES

   These live at the END of the file on purpose. The band's own rule
   sits up with the other section styles, but the elements inside it
   (.area__hint, .area__btn, .area__link and the rest) are defined
   BELOW that point, so overrides written next to .area lost on source
   order every time and the copy stayed dark-on-blue.
   ============================================================ */

/* ---- on-dark overrides for the bold band ---- */

/* The marker block inverts: a blue block on a blue band is invisible,
   so it goes white with blue type inside it. */
.area .hl::before { background: #FFFFFF; }
.area .hl.in,
.no-js .area .hl { color: var(--blue); }

/* Opaque white, not the 82% --area-soft the rest of the band uses.
   At 14px over the blue the softened white measures 3.95:1, under the
   4.5:1 AA needs for normal text; opaque takes it to 5.05:1. Fixed on
   the TEXT rather than by darkening the band, which was the explicit
   call — the brand blue stays exactly as it is.

   .area__towns goes with it, for the same reason and against the
   assumption written here first: it looked like it would sit far
   enough down the band to have contrast to spare, and it does not.
   Opening the list expands the band, so the list starts back up at
   roughly the same blue the hint is on — 4.06:1 softened, 5.21:1
   opaque. The measurement is in tools/area-ui.test.js, which is where
   that guess got caught. */
.area__hint,
.area__towns { color: #FFFFFF; }

/* Solid white against the band reads as the primary action far better
   than blue-on-blue, which all but disappears here. */
.area__btn {
  background: #FFFFFF;
  border-color: #FFFFFF;
  color: var(--blue);
}
.area__btn:hover { background: #EAF1FE; border-color: #EAF1FE; color: var(--blue-hover); }

.area__input { border-color: #FFFFFF; }
.area__input:focus-visible { outline-color: #FFFFFF; }

/* Its own rule sets #555555 with :hover going to var(--area-ink) —
   fine when the band was light, but --area-ink is #FFF now and the
   link is #FFF, so hovering did nothing at all. Dimming to the soft
   token is the affordance the inversion took away. */
.area__link { color: #FFFFFF; }
.area__link:hover { color: var(--area-soft); }

/* ------------------------------------------------------------
   THE AUTOCOMPLETE POPUP IS A LIGHT SURFACE ON A DARK BAND

   This is the one thing in the section that did NOT invert with the
   band, and it is why the town search looked broken. .area__options
   is a white card with a border and a drop shadow — it always was,
   and it should stay white; a blue popup on a blue band has no edge.
   But everything painting inside it was being coloured for the band:

     .area__option        color: var(--area-ink)   -> #FFF on #FFF
     .area__option-note   pushed to --area-soft    -> white on white
     .area__group         pushed to rgba(255...)   -> white on white

   So the field worked, the list opened, and the results were
   invisible — the popup rendered as an empty white rectangle that
   grew as you typed. The two blanket overrides above were written
   for "everything in .area sits on blue", which was true of every
   element except this one.

   FIXED BY RE-SCOPING THE TOKENS, not by restating colours on each
   child. Custom properties inherit, so redefining --area-ink and
   --area-soft on the popup itself flips the whole subtree back to
   light-surface values in two lines, and any option row added later
   is correct by default instead of needing another override. The two
   blanket rules above lost their popup members at the same time —
   .area__option-note and .area__group now keep the light-surface
   colours in their own rules, which were right all along.

   The tick keeps fill: var(--blue) from its own rule. Blue on white
   is correct here; it is only the band that cannot take blue.
   ------------------------------------------------------------ */
.area__options {
  --area-ink:  var(--head-ink);
  --area-soft: #5C6B86;
}

/* ============================================================
   SECTION HEADING TREATMENT — variant A, now retired

   Was an A/B behind html[data-type]. A won and was folded in here:
   the Let's-talk block's own treatment (weight 500, tracked in,
   leading 1.1) carried across every section heading, replacing the
   800-900 they had inherited from coconutcleaningco.com.

   ALL SEVEN OF ITS SELECTORS HAVE SINCE MOVED TO MONTSERRAT and are
   set at the end of this file, so the rule that lived here is gone
   rather than left to fight with them. The leading it introduced was
   the one part worth keeping and travelled with them — see the note
   there about why 1.1 and not the 1 to 1.3 the base sizes carry.

   Kept as a comment because the A/B is the reason those headings
   stopped being 900, and that is not otherwise recorded anywhere.
   ============================================================ */

/* 48px of air under it; at the desktop size 64 read as a hole. */
.talk__title { margin-bottom: 48px; }
@media (max-width: 760px) {
  .talk__title { margin-bottom: 32px; }
}

/* ============================================================
   QUICK ACTIONS — the two sticky pills

   Phones and small tablets only. Above 960 the header keeps Book Now
   in the bar and the hero form is on screen next to the copy, so a
   floating pair would be a third copy of the same two actions.

   TWO PILLS, NOT A BAR. The container is a transparent flex row with
   no background, no border and no blur, and it does not span the
   width — the pills sit centred with a real gap between them and the
   page scrolls visibly through it. pointer-events is off on the row
   and back on for each pill, so the invisible strip either side of
   them never eats a tap meant for the content underneath.

   Sits at z-index 50: under .site-header at 60, so the open mobile
   nav panel covers it rather than the pills punching through, and
   above every section, all of which sit at 20 or below.
   ============================================================ */

.quickbar {
  position: fixed;
  left: 0;
  right: 0;

  /* env() so the pills clear the iOS home indicator; the fallback is
     for anything without safe-area support, where 0 is correct. */
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  z-index: 50;

  display: none;              /* flex is switched on at <=960 below */
  justify-content: center;
  gap: 12px;
  padding: 0 16px;
  pointer-events: none;

  /* Off state. visibility, not just opacity — the links have to leave
     the tab order while they are invisible, or a keyboard user lands
     on a Call Now button floating over the middle of the hero. */
  visibility: hidden;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .22s ease, transform .22s ease, visibility .22s;
}

.quickbar.is-on {
  visibility: visible;
  opacity: 1;
  transform: none;
}

@media (max-width: 960px) {
  .quickbar { display: flex; }
}

/* prefers-reduced-motion: still fade, never slide. The fade is what
   stops the pills appearing as an abrupt pop mid-scroll; the travel
   is the part that reads as motion. */
@media (prefers-reduced-motion: reduce) {
  .quickbar { transform: none; transition: opacity .01s linear, visibility .01s; }
  .quickbar.is-on { transform: none; }
}

.quickbar__pill {
  pointer-events: auto;

  /* 1 1 0 with a max, so the two share the row evenly and stay the
     same width as each other whatever the labels say, but never grow
     into a full-width bar on a wide phone. */
  flex: 1 1 0;
  max-width: 190px;
  min-height: 52px;           /* comfortably past the 44px touch floor */

  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 100px;

  font-family: var(--font);
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.1px;
  text-align: center;
  white-space: nowrap;

  /* Two shadows: a tight one that reads as a hairline edge so the pill
     separates from a same-coloured section behind it, and a wide soft
     one that lifts it off the page. */
  box-shadow:
    0 1px 2px rgba(6, 12, 28, .18),
    0 10px 26px rgba(6, 12, 28, .30);
  transition: background .2s linear, transform .12s ease;
}

.quickbar__pill:active { transform: translateY(1px); }

/* Call is the charcoal one and Book is the blue one, in that order.
   Book carries the page's primary blue because it is the primary
   action everywhere else on the site — the header bar, the hero, the
   About block — and the pair would misread if the sticky version
   promoted the other one. Charcoal rather than a white outline for
   Call: an outline pill needs a light backdrop to read against, and
   these float over sections that are variously white, grey, blue and
   near-black. */
.quickbar__pill--call {
  background: #131B2E;
  color: #FFFFFF;
  border: 1px solid rgba(255, 255, 255, .16);
}
.quickbar__pill--call:hover { background: #1B2540; }

.quickbar__pill--book {
  background: var(--blue);
  color: #FFFFFF;
  border: 1px solid var(--blue-line);
}
.quickbar__pill--book:hover { background: var(--blue-hover); }

.quickbar__pill:focus-visible {
  outline: 3px solid #FFFFFF;
  outline-offset: 2px;
}

/* Small phones: the labels are short enough that the pills can shrink
   without wrapping, and 12px of gap on a 360 screen is proportionally
   wider than it looks at 390. */
@media (max-width: 380px) {
  .quickbar { gap: 10px; padding: 0 12px; }
  .quickbar__pill { min-height: 48px; font-size: 15px; }
}

/* ============================================================
   HERO FOLD ON PHONES — the CTAs and the Google card must land
   above it

   Hard requirement, and it was failing at every phone size: at
   360x740 the buttons ended at 841 and the rating card at 963,
   against a 740 fold. Both were off screen.

   The stack is eyebrow, headline, two ledes, two buttons, rating
   card, four badges. That does not fit a small fold at the sizes it
   was using, so three things give, in this order:

   1. THE FILM OVERLAPS AGAIN. The copy is pulled back up onto its
      bottom edge rather than starting below it. This is the only
      change that buys height without shrinking anything, because it
      layers the copy over the film instead of stacking it under.
      Safe here for the reason it was safe the first time: the band
      is height-driven at these widths, so its bottom edge is the
      bottom of the FRAME — the truck's lower body and shadowed
      asphalt at Y=58/255, about 11:1 for white. The earlier note
      about 2.2:1 was measured on a much taller band where the edge
      lands on the pale wall instead.

   2. THE SECOND LEDE GOES, below 560 only. "Cleanouts, renovations,
      yard waste and demolition" is the least load-bearing line in
      the fold — the services grid says the same thing at length —
      and it costs ~54px, which is most of what was missing.

   3. THE RATING CARD GETS COMPACT. Same content, tighter padding and
      a single-line sub, which is ~20px.

   Nothing here touches the wide layout.
   ============================================================ */
@media (max-width: 960px) {
  .hero__inner {
    /* 236px of overlap, derived rather than picked, and it puts ALL
       the copy on the film. This replaced a 110 that deliberately
       straddled — headline crossing the seam, half on each side —
       after both were built and compared on device.

       The arithmetic: eyebrow block 64 + headline 92 + lede block 62
       = 218, plus 18px of clearance so the lede does not sit on the
       seam itself. Move the eyebrow or the headline size and this
       number moves with them; they are the same number.

       The film grew about 1.35x to pay for it — clamp(450px, 66svh,
       640px) against the 300/46/450 it was. It could not simply go to
       1.5x: the Google rating card has to finish above the fold on
       every device, and a 360x740 Android runs out of room first.
       Every band in this file below was set by that constraint, not
       by preference, and the worst clearance across a twelve-device
       sweep is 20px. */
    padding-top: calc(var(--hero-band) - 236px + var(--hero-drop));
    padding-bottom: 28px;
    gap: 24px;
  }

  .hero__eyebrow { margin-bottom: 8px; }
  .hero__text { margin-top: 8px; }
  .hero__actions { margin-top: 12px; }
  .gbadge { margin-top: 10px; }
  .hero__badges { margin-top: 16px; }
}

@media (max-width: 560px) {
  :root {
    /* Shorter film on a small fold. It is still the first thing on
       screen and still the whole width; it just stops taking two
       thirds of a 740px screen. */
    --hero-band: calc(var(--header-h) + clamp(375px, 55.5svh, 525px));
  }

  /* The services line is the one that goes. Flagged rather than
     silent: it is content, and the services grid below covers it. */
  .hero__text--services { display: none; }

  /* One line shorter than the 960 block assumes, so the copy sits
     12px lower to keep the lede off the seam. */
  /* clamp, not a fixed number, and this is the one number in the
     mobile hero that HAS to scale. The offset's whole job is to keep
     the lede's last line above the seam, and the distance from the
     copy's top to that line is the text block's own height — which is
     not fixed, because every size in it is clamped against svh. On a
     430x932 the block measures 242px; on a 375x667 it is 213. A single
     px value is therefore correct at exactly one screen height and
     wrong either side of it, which is what put the lede 22px past the
     seam on a 15 Pro Max while leaving 7px of slack on an iPhone 8.

     29svh tracks the block's growth, with the floor set so the
     shortest phones still clear it and the cap so the tallest do not
     push the copy needlessly far up the frame. Every phone lands
     between 4 and 18px of clearance. */
  .hero__inner { padding-top: calc(var(--hero-band) - clamp(232px, 29svh, 272px) + var(--hero-drop)); }

  .gbadge { padding: 10px 14px; gap: 12px; }
  .gbadge__mark { width: 34px; height: 34px; }
  .gbadge__sub { font-size: 12.5px; }
  .gbadge__score { font-size: 16px; }
  .gbadge__stars svg { width: 17px; height: 17px; }
}

/* TALL phones, by height. The 560 rule above has to serve both a
   360x740 Galaxy and a 430x932 Pro Max, and those two are not the same
   problem: the copy stack below the seam is nearly identical on both,
   but the Galaxy has 26px of fold to spare where the Pro Max has 118.
   One svh coefficient for both means the Galaxy sets the ceiling and
   every large phone is held to 1.39x for its sake.

   So they split at 810px. Above it, 60svh takes the common iPhone and
   Pixel sizes to a true 1.5x with 30-80px still in hand; below it, the
   55.5svh above stands and the short-and-wide Androids stay at 1.39x,
   which is as far as their fold allows. The jump at the boundary is
   real and it is the honest answer: the film grows by exactly what
   each screen can give it, and 740-tall phones have less to give. */
@media (max-width: 560px) and (min-height: 810px) {
  :root {
    --hero-band: calc(var(--header-h) + clamp(375px, 60svh, 560px));
  }
}

/* The drop on these two branches is UNCHANGED, and that is a result
   rather than an oversight. Lifting it was tried and measured: the
   eyebrow is the topmost line of copy on a tall phone, it already
   sits within a couple of percent of the film's dark-zone boundary,
   and every pixel up is a pixel into the pale cloud-and-wall strip.
   At 390x844, stepping the eyebrow up in 4px increments through the
   clip's loop: 3.38 at rest, 2.77 at 4px, 2.52 at 8, 2.02 at 12,
   1.48 at 16. There is no lift to take here. The height these phones
   gained came from the leading and the furniture instead. */

/* 320px screens. Everything above fits from 360 up; at 320 the rating
   card still finished 24px past the fold, so the film gives that back.
   Scoped this tight because it is the only width that needs it and the
   film should not shrink on phones that do not. */
@media (max-width: 340px) {
  :root {
    --hero-band: calc(var(--header-h) + clamp(255px, 39svh, 360px));
  }
  .hero__inner { gap: 20px; padding-top: calc(var(--hero-band) - 210px + var(--hero-drop)); }
  .hero__actions { margin-top: 12px; }
  .gbadge { margin-top: 10px; }
}

/* ============================================================
   PHONES — EVERYTHING UP

   Asked for plainly: the copy, the buttons, the rating card and the
   badges all a little higher in the fold, and nothing pinned to a
   number that only works on one phone.

   The first thing this ran into is that the copy CANNOT simply be
   pulled up. --hero-drop already exists for that and it is already
   negative, and the reason it stops where it does is the film. The
   clip's own luminance, measured on the rendered layout rather than
   on the file (the method in tools/luma-v2.js), at 390x844 with the
   eyebrow's box stepped up in 4px increments through the loop:

     lift    0    4    8   12   16   20
     eyebrow 3.38 2.77 2.52 2.02 1.48 1.39

   That is a cliff, not a slope, and it is the pale cloud-and-wall
   strip the notes above have been circling all along: the eyebrow is
   the topmost line of copy, so it is the one that reaches it first.
   Eight pixels is what the film gives, and this block takes exactly
   that and no more. On the short-phone branches the eyebrow is
   already hidden and the headline is topmost, which is a far stronger
   element — 4.6:1 where the eyebrow is 3.4 — so those branches take
   ten, and the tablet branch takes twenty-eight, where it also drags
   the ghost button off a measured 1.90:1 and up to 4.29.

   So the rest of the movement is bought, not borrowed, and almost all
   of it comes from one place:

   THE EYEBROW WAS INHERITING BODY'S LINE-HEIGHT. 1.857, which is a
   measure for paragraphs, applied to a two-line label — 56px of box
   around 30px of type. It never set its own. At 1.3 the label takes
   39px, and every pixel of that 17 comes off the TOP of everything
   below it while the eyebrow's own first line does not move at all.
   It is free height and it is the largest single piece here.

   The rest is the furniture, made fluid the way the fold-fitting pass
   did it on desktop: the two CTAs, the rating card's internal leading
   (16px type on a 1.6 line-height, inside a card that is 68px tall to
   hold 34px of logo) and the badge discs. Ceilings are what this file
   already shipped, so the biggest phones lose almost nothing; the
   floors are what a short phone gets.

   Net on a 390x844, measured: the headline lands 25px higher, the
   rating card and the badge grid 51px higher, and the badge grid
   finishes inside the fold instead of 44px under it.
   ============================================================ */
@media (max-width: 960px) {
  /* See above. The margin goes fluid at the same time — it was 8px
     flat, which is right at 844 and too much at 568. */
  .hero__eyebrow {
    line-height: 1.45;
    margin-bottom: clamp(5px, .95svh, 8px);
  }

  .hero__text { margin-top: clamp(5px, .95svh, 8px); }

  .hero__actions {
    margin-top: clamp(8px, 1.2svh, 12px);
    gap: clamp(10px, 1.5svh, 14px);
  }

  /* 46px floor, not lower: this is a phone and it is a primary CTA,
     so it stays a comfortable thumb target however short the screen
     gets. 5.6svh reaches the full 52 again on a 430x932. */
  .hero__btn { min-height: clamp(46px, 5.6svh, 52px); }

  /* The rating card was 68px tall to present 34px of logo, because
     its two text rows carried 1.6 and 1.5 line-heights meant for a
     card with room around it, and a 4px gap between them on top. */
  .gbadge {
    margin-top: clamp(7px, 1.2svh, 10px);
    padding: clamp(7px, 1.15svh, 10px) clamp(11px, 1.7svh, 14px);
  }
  .gbadge__body { gap: 2px; }
  .gbadge__score { line-height: 1.25; }
  .gbadge__sub { line-height: 1.3; }

  /* The badge grid is the last thing in the fold and it was the last
     thing still hanging out of it — 44px under on a 390x844. The
     margin, the row gap and the discs all give a little, which is
     what takes it inside. 26px is the floor on the disc: below that
     the tick inside stops reading at a glance. */
  .hero__badges {
    margin-top: clamp(10px, 1.7svh, 14px);
    row-gap: clamp(8px, 1.2svh, 11px);
  }
  .hero__badge-ico {
    width: clamp(26px, 3.4svh, 30px);
    height: clamp(26px, 3.4svh, 30px);
  }
  .hero__badge-ico svg { width: clamp(14px, 1.9svh, 16px); height: clamp(14px, 1.9svh, 16px); }
}

/* SHORT viewports, by HEIGHT rather than width. A 360x640 Android and
   a 320x568 SE are not narrow problems, they are short ones — the
   width rules above do nothing for them because the stack is already
   as narrow as it gets.

   The film gives up most of it, and the eyebrow goes: it is the one
   element in the fold that carries no information the headline does
   not already carry ("Aberdeen, NJ · Central New Jersey" against
   "...in Central NJ" two lines below). On a screen this short that
   redundancy is the cheapest thing to spend.

   WIDTH-SCOPED, and it was not always. Every number in here is a
   BAND number — it moves --hero-band, it subtracts 184 from it to
   set the straddle, it hides the eyebrow because the eyebrow is what
   lands on the pale strip at the film's bottom edge. All of that is
   the stacked phone layout. Keyed on height alone it also caught
   every desktop window under 700px tall, where --hero-band means
   nothing: a 1366x633 laptop was getting a 170px padding-top derived
   from a film seam that does not exist at that width, an 18px column
   gap where the two-column grid wants 48, and no eyebrow. That was
   ~100px of the home hero's overflow on exactly the laptops that
   could least afford it. See "THE FOLD, HELD" above. */
@media (max-width: 960px) and (max-height: 700px) {
  :root {
    /* Short screens had been the ones LEFT BEHIND by the 1.5x push:
       their band was floored at 196 and they were still sitting at
       1.32x while tall phones reached 1.40. They are also the ones
       with room to spare — a 320x568 finishes the rating card 77px
       above the fold — because the copy stack shrinks faster than the
       screen does. So the floor goes to 239 and the svh term to 36,
       which puts every screen under 700px tall at a true 1.5x and
       still leaves the tightest of them 34px of clearance. */
    --hero-band: calc(var(--header-h) + clamp(239px, 36svh, 320px));
  }
  .hero__eyebrow { display: none; }
  /* With the eyebrow gone the eyebrow block is 0, so the headline's
     offset from the seam is just -overlap. 46 keeps the same
     half-and-half straddle that 110 gives when the eyebrow is
     present. */
  /* 184, up from 160. With the eyebrow hidden the block is shorter,
     but a 320x568 still measures 170 and 160 put its lede 10px under
     the seam. */
  /* --hero-drop was a flat 36 here. It is now clamped and lands on
     26, a 10px lift, and that is measured too: with the eyebrow
     hidden the HEADLINE is topmost, and a headline is a far stronger
     element than a 15px label — 4.6:1 against the eyebrow's 3.4. At
     a 10px lift it still reads 4.32 on a 375x667 and 3.90 on a
     320x568, both clear of the 3:1 that large text needs. */
  .hero__inner { gap: 18px; padding-bottom: 22px; padding-top: calc(var(--hero-band) - 184px + var(--hero-drop)); }
  .hero__text { margin-top: 10px; }
  .hero__actions { margin-top: 14px; }
  .hero__btn { min-height: 46px; }
  /* 12/9,13 became 9/7,11. This block runs AFTER the fluid rules
     above and so re-fixes the rating card on exactly the screens
     where it was worst, and the last few pixels had to come from
     here: a 320x568 finished its rating card 32px BELOW the fold
     before any of this, against a rule three sections up that says
     it has to land above it on every device. It now finishes 17px
     inside. */
  .gbadge { margin-top: 9px; padding: 7px 11px; }
}

/* ============================================================
   WEIGHT AND CASE PASS

   Two complaints, one cause. The headings read as heavy, and the
   labels are set in caps — and both are what makes a page look
   machine-assembled rather than art-directed. Weight and caps are the
   two loudest things type can do, and this page was doing both at
   once, in every band.

   The section titles were already handled: the variant-A block above
   took them to weight 500. What it did not touch was everything
   AROUND them — the hero, the card titles, the step names, the
   eyebrows — so the page ended up with quiet section headings sitting
   between a 900-weight hero and a row of 800-weight caps labels. That
   inconsistency is more visible than the weight itself.

   Written as one trailing block for the same reason the DM Sans swap
   was: it wins on source order without touching specificity, and
   reverting it is deleting the block.

   THE SCALE, now stated once instead of implied in thirty rules:

     500   display headings — section titles (set above)
     600   card and sub-headings, all labels
     700   numerals only — stats, scores, the footer phone
     400   body

   The hero is not on this scale. It is its own thing in every
   respect now — see the block below.

   Numerals sit ABOVE the headings deliberately. A figure has no
   ascenders or descenders and no word shape, so at equal weight it
   reads lighter than the words around it; 700 is what makes a stat
   read as a data point rather than as a heading that lost its nerve.
   ============================================================ */

/* ---- tier 2: card and sub-headings ---- */

.services__label,
.services__pname,
.services__sub,
.steps__name {
  font-weight: 600;
}

/* ---- tier 3: numerals ---- */

.impact__num,
.gbadge__score,
.foot__phone {
  font-weight: 700;
}

/* ============================================================
   THE HOME HERO — MONTSERRAT, LIGHT

   The one place on either page that departs from the DM Sans heading
   face, and it is scoped to the hero section on purpose: the headline
   and the quote box's heading, which are the only two headings inside
   the hero band. Picked off design-lab/hero-font.html, where ten
   faces were shown at the real hero size across their full weight
   axes with the actual film behind them; Montserrat at 300 won.

   THREE THINGS MOVE TOGETHER AND ALL THREE ARE LOAD-BEARING:

     · The FACE comes from .hero__title's own rule further up, which
       already said var(--font). Nothing here sets font-family — the
       hero simply gets left out of the --font-head selector list in
       the heading-face block. That is why re-adding it there breaks
       this silently, and why the note up there says not to.

     · The WEIGHT is 300 against a heading scale that runs 500-700.
       That gap is the point rather than an inconsistency: the hero is
       the only type on the page with no neighbour at its size.

     · The TRACKING is -1px, not the -1.6px the hero carried under DM
       Sans. Two reasons pushing the same way. Montserrat is the
       narrower face and does not need pulling in as hard, and light
       strokes need MORE air than heavy ones, not less, because there
       is less ink separating one letter from the next. Carrying the
       old value over is the specific thing that makes a font swap
       look broken rather than just different.

   Weight 500 was tried on this headline once before, under DM Sans,
   and rejected as a legibility problem over the moving footage — see
   the variant-A note above. 300 is lighter still, so that finding is
   being overruled knowingly: it was chosen against the real frame,
   and Montserrat holds a light weight at display size better than DM
   Sans does. It remains the value on the page most worth a second
   look on a bright frame. If it fails there, the fix is the crop or
   the shadow, not the weight.

   THE QUOTE BOX HEADING IS 400, NOT 300, and that is the two of them
   matching rather than diverging. Weight is a proportion of stroke to
   counter, so a number is not a constant amount of thinness across
   sizes — 300 at the headline's 62px and 300 at this heading's 28px
   are visibly different colours on the page, the smaller one reading
   markedly lighter. 400 here is what looks like the SAME choice as
   300 there. Matching the numbers would be what makes them look
   mismatched.

   Nothing else in the quote box needed touching. The labels, inputs,
   button and note were always on var(--font) — the box was already
   Montserrat everywhere except this one heading, which the
   --font-head list had been pulling out to DM Sans.
   ============================================================ */

/* Tracking moved from -1px to em, and tightened, so the hero holds
   the same fit as the section titles — same face, same weight, so
   they have to agree or the hero reads as the odd one out. The em
   also retires the mobile px override this used to need: -1px was
   twice the proportion at the headline's 30px floor as at its 62px
   ceiling, which is exactly what an em value stops happening. */
.hero__title {
  font-weight: 300;
  letter-spacing: -.04em;
}

/* Less negative than the display sizes, and that IS the match rather
   than a departure. Tracking is optical, not arithmetic: the smaller
   the type, the more space it needs between letters to stay readable,
   so holding one em value across a 62px headline and a 28px form
   heading would leave this one visibly over-tightened. Same reason it
   sits at weight 400 while they sit at 300. */
.hero__form-title {
  font-weight: 400;
  letter-spacing: -.028em;
}

/* ------------------------------------------------------------
   EVERY SECTION TITLE — the same treatment

   .about__title is "The owner is the one who shows up." on the home
   page, and the SAME selector carries "Keeping Your Spaces Clean,
   One Job at a Time" on /about/, because that page reuses the
   .about block wholesale from this file rather than restyling it.
   One rule, two pages.

   The eyebrows above these headings are NOT touched — .about__eyebrow
   ("Meet Mustafa"), .services__label and the rest. They were signed
   off as they are, and a small caps-height label is a different job
   from a display heading.

   FONT-FAMILY IS SET EXPLICITLY HERE, unlike .hero__title and
   .hero__form-title, which get Montserrat just by being dropped from
   the --font-head list. That trick does not work for all of these:
   .talk__title's own rule says 'DM Sans' and .impact__title's says
   'Plus Jakarta Sans', so dropping them from the list would hand them
   to the wrong face rather than to Montserrat. Naming the family once
   for the whole group is both correct and less to reason about.

   TRACKING IS IN em, NOT px, AND THAT IS DELIBERATE. Every size here
   is a clamp — .services__title runs 28px to 60px — and a fixed px
   value is a completely different proportion at each end. An em value
   holds the same optical fit at every size in every clamp, and no
   per-breakpoint override is needed. The px values it replaced came
   from the DM Sans system: -1.2px, and -.05em from variant A.

   -.04em, arrived at in two steps. It started at -.016em, the hero's
   own tracking expressed as a ratio (-1px at 62px) — the right way to
   derive it, but it landed loose for a reason worth recording:
   tracking has to answer the WEIGHT as well as the size. Montserrat
   sets wide at 300, because a light face has thin strokes and
   unchanged sidebearings, so the same gap is a larger share of the
   word than it is at 600. Deriving from the hero carried the size
   across but not the lightness.

   WORD-SPACING IS NOT SET, and that is a decision rather than an
   omission. A pass in between took it to -.06em on the theory that
   Montserrat's .26em word space was the thing reading as loose. That
   was wrong, and wrong in an instructive way: closing the word gaps
   and closing the letter gaps look nothing alike to a reader. Word
   gaps are what the eye uses to find where one word ends, and pulling
   them in makes a heading read as CRAMPED — the words start to run
   together — while the letters inside each word still look airy. The
   complaint was only ever about the letters.

   So the space between words is left at the face's own value and only
   the letters are pulled in. Note that these interact anyway: a
   negative letter-spacing applies to the space character too, so
   -.04em quietly nets a .22em word space. That is the whole
   adjustment the gaps needed.

   LEADING 1.1 is carried over from variant A and is load-bearing.
   The base rules set leadings from 1 to 1.3 depending on which
   reference each section was measured off, and at 1 the .hl highlight
   blocks on a two-line heading touch each other. .about__title has no
   highlight block, but it takes the same value to stay with the group.
   ------------------------------------------------------------ */
.about__title,
.showcase__title,
.services__title,
.reviews__title,
.steps__title,
.impact__title,
.area__title,
.talk__title {
  font-family: var(--font);
  font-weight: 300;
  letter-spacing: -.04em;
  line-height: 1.1;
}

/* The mobile tracking override that used to sit here is GONE, and its
   absence is the point rather than an omission. It existed because a
   px value against a clamped font-size is a different proportion at
   each end of the clamp; -.032em is the same proportion by
   definition, so there is nothing left to correct.

   There is still a dead `.hero__title { letter-spacing: -.5px }` in
   the max-width:768px block at line ~3458. It has never applied since
   the heading-face block landed — that block sets tracking
   unconditionally and later in the file, and a media query cannot
   beat a later rule of equal specificity. Left in place rather than
   removed, because deleting it changes nothing and the next person to
   grep for hero tracking should find this note explaining why it does
   nothing, not a clean file that hides the trap.
   ------------------------------------------------------------ */

/* ============================================================
   TITLE CASE ON THE LABELS

   The display headings lost their uppercase in an earlier pass; the
   labels kept it, on the argument that all-caps is the convention for
   a small label and title case reads as a mistake there. That was the
   call at the time. It is reversed here: the page is to have no
   all-caps text at all.

   TRACKING IS NOT OPTIONAL HERE. Caps are letterspaced because
   uppercase glyphs are all one height and sit too close at their
   natural fit — which is why every one of these carried .1em or more.
   Mixed case does not have that problem, and carrying caps tracking
   over to it is the single most common way this change goes wrong:
   the words come apart and the label reads as spaced-out rather than
   as quiet. Each is pulled back to roughly .01em, kept fractionally
   positive rather than zero because these are still small type on
   coloured or photographic grounds.

   Source text needed no rewriting — every one of these was already
   authored in title case, because the transform was doing the
   shouting. The one exception is the announcement bar, handled in
   index.html.

   Weights come down with the case. A caps label needs weight to hold
   at 12-13px; the same label in mixed case has word shapes doing that
   work, and 700 on top of it reads heavy.
   ============================================================ */

.hero__eyebrow,
.about__eyebrow {
  font-weight: 600;
  letter-spacing: .01em;
  text-transform: none;
}

.hero__field label,
.area__group {
  font-weight: 600;
  letter-spacing: .01em;
  text-transform: none;
}

.foot__head {
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: none;
}

.foot__hours span {
  font-weight: 600;
  letter-spacing: .01em;
  text-transform: none;
}

/* Trustpilot prints this word in caps and the section was measured
   off that row, so this is a deliberate departure from the reference
   rather than an oversight in matching it. */
.reviews__grade { text-transform: none; }

/* The two buttons. .header-cta stays at 700 — it is the page's
   primary action and the only thing in the bar competing with a
   photograph behind it; the weight is load-bearing there, the caps
   were not. */
.above-header__btn {
  font-weight: 600;
  text-transform: none;
}
.header-cta { text-transform: none; }


/* ============================================================
   HERO HEADLINE — ONE WEIGHT STEP UP ON THE STACKED LAYOUT

   300 was chosen against the WIDE hero, where the headline runs
   clamp(30px, min(4.2vw, 7vh), 62px) and lands near its 62px cap.
   Below 960 it lands near the floor instead — clamp(29px, 8.2vw,
   40px) from the 768 block — and the same number is not the same
   colour on the page at half the size. This is the exact effect the
   quote-box note further up already records: 300 at 62px and 300 at
   28px read as different weights, the smaller one markedly lighter,
   so matching the NUMBER across sizes is what makes them look
   mismatched. 400 on phones is what looks like the SAME choice as
   300 on desktop, not a heavier one.

   The film underneath is the second reason. On the stacked layout
   the copy sits ON the footage rather than beside it, so every
   stroke is competing with moving video — and thin strokes are what
   a busy frame eats first. The desktop hero has no such problem at
   62px.

   ONE STEP ONLY, and that is the brief. 500 was not considered: it
   was already tried on this headline under DM Sans and rejected, and
   the light-display look is the whole point of the face choice.

   Tied to 960, the breakpoint where the grid stacks — the same one
   --hero-band, --hero-drop and the overlap all use. Above it nothing
   changes; the desktop headline is untouched at 300.

   Placed at the end of the file for the same reason every other pass
   here is: it beats the 300 rule on source order without touching
   specificity, and reverting it is deleting the block.
   ============================================================ */
@media (max-width: 960px) {
  .hero__title { font-weight: 400; }
}


/* ============================================================
   ANNOUNCEMENT BAR — GIVE IT A COLOUR

   The complaint was that the top of the page reads black-and-white
   and dull, and the palette notes at the top of this file explain
   exactly why: --bar-bg #EEEEEE, --bar-line #EAEAEA and --bar-text
   #7D7D7D were all measured off the reference, and the reference's
   bar is deliberately near-invisible furniture. Three greys stacked
   on each other is what "completely grey" means literally.

   Worse, the pill was never finished. .above-header__btn already
   carries padding 6px 16px and border-radius 30px — the whole shape
   of a button — with `background: transparent`, so it draws nothing
   at rest. The bar was a button-shaped hole in a grey strip.

   So the band takes the brand blue and the type goes white. This is
   the page's first paint and the thing it says is "call us", which
   makes it the one strip on the page with a reason to shout.

   THE GRADIENT IS NOT DECORATION, IT IS THE CONTRAST BUDGET.
   White on --blue is 4.64:1 — over AA's 4.5 for normal text, but
   only just, and this is 13px type that drops to 11px in the 480
   block. Running the band DOWN to --blue-hover puts the lower half
   at 5.79:1, so the tightest measurement on the strip is the top
   edge rather than the whole of it. Both stops are existing palette
   values; no new hex was invented for this. Do not flip the stops —
   the lighter end has to be the smaller share of the band, and it
   has to be the end furthest from the type's baseline.

   It also means the band is not a flat fill, which is the standing
   rule for any solid colour block in these builds.

   BORDER-BOTTOM goes to a dark hairline. It was #EAEAEA, chosen to
   separate one near-white from another; under a blue band and above
   the white header that line would read as a light scratch.

   HOVER FILLS THE PILL rather than changing the text colour. The old
   rule moved the type to --nav-hover #424242, which on blue is
   roughly 1.5:1 — the hover state was going to be less readable than
   the rest state. A 16% white wash lights the existing radius
   instead, which is what that radius was always for.

   FOCUS RING GOES WHITE. The global :where() ring is 3px of --blue,
   invisible against a --blue band. :where() has zero specificity so
   a plain class selector overrides it.

   THE EMOJI IS GONE, replaced by an inline SVG in currentColor —
   index.html, about/ and junk-removal-page/ all carry the markup.
   📞 renders as a dark red handset on every platform that has it,
   which was tolerable on grey and clashes badly on blue, and it is
   a different glyph on every OS. .above-header__emoji is left in the
   base rules above; it matches nothing now and deleting it is safe
   whenever someone is in there anyway.

   NOT SCOPED TO A BREAKPOINT. Raised as a phone problem, but the bar
   is one element and it was the same three greys at every width —
   colour on phones and grey on desktop would read as a bug.
   ============================================================ */
.above-header {
  background: linear-gradient(180deg, var(--blue) 0%, var(--blue-hover) 100%);
  border-bottom-color: rgba(11, 18, 38, .16);
}

.above-header__btn {
  /* inline-flex so the glyph centres against the cap-height instead
     of sitting on the baseline the way an inline span does. */
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: #FFFFFF;
  transition: background .2s linear;
}
.above-header__btn:hover { color: #FFFFFF; background: rgba(255, 255, 255, .16); }
.above-header__btn:focus-visible { outline-color: #FFFFFF; }

/* THE TWO FLANKS GO WHITE TOO. When the band took the blue the pill
   above was recoloured and these were not, so the location link and
   the email were left on --bar-text #7D7D7D at opacity .82 — a grey
   that was chosen to be near-invisible against #EEEEEE and is close
   to unreadable against blue. They are the first line of the page
   and one of them is the email address; they have to be legible.

   OPACITY GOES TO 1, it does not stay at .82. This is 12.5px type,
   so it needs the full 4.5:1 and white-at-.82 over --blue is roughly
   3.3:1 — the dimming was affordable on a light grey bar and is not
   here. Hierarchy against the Call Now pill is carried by size,
   position and the pill's radius instead, which is enough.

   HOVER UNDERLINES rather than moving the colour. The old rule sent
   these to --nav-hover #424242, dark grey on blue at about 1.5:1,
   which made hovering a link harder to read than leaving it alone —
   the same trap the pill's hover was pulled out of above. The icons
   are fill: currentColor, so they follow the type without a rule. */
.above-header__side {
  color: #FFFFFF;
  opacity: 1;
  transition: text-decoration-color .2s linear;
}
.above-header__side:hover {
  color: #FFFFFF;
  opacity: 1;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.above-header__side:focus-visible { outline-color: #FFFFFF; }

/* Sized in em so it tracks the type through the 768, 480 and short-
   viewport blocks, all three of which change the bar's font-size and
   none of which should need touching for the icon. */
.above-header__ico {
  width: 1.08em;
  height: 1.08em;
  flex: none;
  fill: currentColor;
}


/* ============================================================
   PHONE HERO — WEIGHT UP, AND PULL THE WHOLE STACK UP THE FILM

   Two complaints, and they are the same complaint: on a phone the
   hero copy is hard to read, and everything under it — CTAs, the
   Google card, the four trust badges, the form — sits too far down
   the fold.

   Placed at the end of the file by the same convention as every
   other pass here: it beats the earlier rules on source order
   without touching specificity, and reverting it is deleting the
   block.

   ------------------------------------------------------------
   1. WHY THERE IS ROOM TO MOVE UP AT ALL

   Measured rather than guessed. Sampling the clip at 4fps through
   the loop, cropped to exactly what the phone band shows (390 wide
   against a 1920x1080 source is a 720px-wide slice at x=46%, and the
   FULL frame height survives — cover's vertical fit is exact on a
   phone, which is also why object-position's Y value does nothing
   here), the band's luma profile runs:

     0-100    Y 113-122   sky              ~4.4:1 white
     105-195  Y 130-170   brightening      3.3:1 -> 2.2:1
     200-265  Y 175-193   pale cloud/wall  1.8:1   <- unusable
     270-285              transition, falls away fast
     285-585  Y 20-60     truck body       7:1 -> 18:1

   The dark zone starts at 48.7% of the band height and runs to the
   seam. That boundary is a fraction, not a pixel count: the whole
   source height always survives, so it tracks the band at every
   phone size.

   The copy was starting at about 71% of the band. Everything from
   48.7% down is dark enough to take white type, so roughly 130px of
   perfectly good dark film was sitting unused ABOVE the headline —
   which is the space this block spends.

   ------------------------------------------------------------
   2. WHAT MOVES, AND WHY IT IS NOT ONE NUMBER

   --hero-drop already exists for exactly this, and it is added into
   every phone breakpoint's padding-top, so moving it moves the whole
   left column AND the form below it as one block. Nothing else is
   touched: the band keeps its height, the film keeps its crop, and
   there is no scrim or solid panel behind the type — the footage
   stays fully visible, which was the explicit requirement.

   It cannot be a single value, because how much dark film is spare
   depends on which band branch a phone lands in, and those branches
   were each calibrated separately further up:

     390x844  band 585  copy was at 71%  -> 124px of pull available
     430x932  band 638  copy was at 71%  -> 133px
     360x740  band 490  copy was at 66%  ->  75px   <- the tight one
     320x568  band 318  short-screen rules, eyebrow hidden ->  ~21px

   The 740-tall Androids are the constraint, as they are for every
   other number in this hero: their band is proportionally shorter
   while the copy stack is nearly the same height. They get the
   smallest move. Each branch below takes what it can actually
   afford, and every one lands with the topmost line of copy at or
   below 50% of the band — i.e. inside the dark zone, with margin.
   ------------------------------------------------------------ */

@media (max-width: 960px) {
  /* Was clamp(52px, 9svh, 84px), which pushed the copy DOWN the film.
     The reasoning for that still holds — down is the safe direction
     for contrast — it was just overshooting: it landed the copy well
     past the top of the dark zone rather than on it. */
  :root { --hero-drop: clamp(-80px, -8.5svh, -62px); }
}

/* Phones under 560. Holds back hard, because this rule has to serve
   the 740-tall Androids that only have ~75px to give.

   -8 became -2 when the eyebrow's leading was tightened (see PHONES —
   EVERYTHING UP above). That change lifts everything below the
   eyebrow by 12px for free, and on this branch 12px was enough to
   carry the LEDE off the truck body and onto the brightening rows
   above it: measured 3.60:1 before, 2.69 after. Six pixels back puts
   it at 2.73 and, more to the point, takes the eyebrow from 2.63 to
   4.34 — the best it has ever measured here. The branch still gains
   44px net, so the six are cheap. */
@media (max-width: 560px) {
  :root { --hero-drop: -2px; }
}

/* Tall phones — the same 810px height split the band already uses,
   for the same reason: above it there is real room, below it there
   is not. iPhone 14/15/16 and Pixel land here and take the full
   ~120px move. */
@media (max-width: 560px) and (min-height: 810px) {
  :root { --hero-drop: clamp(-48px, -5svh, -42px); }
}

/* Short screens keep a positive drop. Their band is floored, the
   eyebrow is already hidden, and the headline still has to clear the
   pale strip — 36 is what is left after that, not a preference.

   Set by the 320x568 case specifically. At 30 its headline landed at
   48.8% of the band, which is the transition row itself: measured
   worst-case 6.9:1, passable for large text and with no margin at
   all for a frame of the loop being brighter than the ones sampled.
   36 puts it at 51.5% and back on the truck body proper. The 6px
   costs nothing anywhere else — the 360x640 that shares this rule
   still finishes its rating card 54px above the fold. */
@media (max-height: 700px) and (min-height: 461px) {
  :root { --hero-drop: clamp(24px, 4.6svh, 26px); }
}

/* Landscape phones stay exactly as they were. The max-height:460
   rule further up zeroes the drop on purpose — the fold is 390px
   tall and the copy starts near the top and scrolls — and the
   ≤960 rule above would otherwise pull it up off the film. */
@media (max-height: 460px) {
  :root { --hero-drop: 0px; }
}

/* ------------------------------------------------------------
   3. WEIGHT

   The headline is DM Sans, set at 300 on desktop and already stepped
   to 400 for phones by the pass above — "one step only, and that is
   the brief". The brief has moved: it is still reading thin against
   moving footage, so it takes the second step to 500.

   That is the weight the earlier pass explicitly declined, so it is
   worth saying what changed. The objection was that the light
   display look is the point of the face, and it is — at 62px, where
   the desktop headline still sits at 300 and is untouched here. At
   the 29-40px the phone headline actually renders, over video, the
   light cut is not reading as elegant, it is reading as faint.

   The lede goes to 500 from the body's 400, and the eyebrow to 700
   from 600. Both are small type over the busiest part of the frame.

   Shadows go with the weight, and they go WIDE-plus-SOFT rather than
   tight: a 1px hard pass is what made the desktop headline look
   outlined, and that failure mode is in this file's history twice.
   3px at .5 is below the threshold where the eye finds an edge; it
   reads as the glyph having density. The wide pass underneath is the
   one doing the contrast work, unchanged in kind from the rule above,
   just stronger.
   ------------------------------------------------------------ */
@media (max-width: 960px) {
  .hero__title { font-weight: 500; }

  .hero__title,
  .hero__accent {
    text-shadow:
      0 1px 3px  rgba(8, 14, 28, .5),
      0 2px 22px rgba(8, 14, 28, .66);
  }

  .hero__text {
    font-weight: 500;
    text-shadow:
      0 1px 3px  rgba(8, 14, 28, .5),
      0 1px 16px rgba(8, 14, 28, .72);
  }

  .hero__eyebrow {
    font-weight: 700;
    text-shadow:
      0 0 12px   rgba(8, 14, 28, .95),
      0 1px 22px rgba(8, 14, 28, .75);
  }
}
