/* ============================================================
   about.css — the v2 About page

   Loaded AFTER v2.css, and only by v2/about/index.html. Everything
   shared — colour tokens, the header, the footer, the type scale —
   comes from v2.css; this file is only what the About page adds.

   Prefixed .ahero rather than .hero on purpose. The home hero carries
   a lot of behaviour (the film band, the straddle arithmetic, the
   fold rules) that has nothing to do with this page, and reusing the
   class would have inherited all of it.
   ============================================================ */

/* ------------------------------------------------------------
   ABOUT HERO — FULL-BLEED PHOTOGRAPH, TITLE OVER IT

   The photograph is the background of the section and the type sits
   on top of it, bottom-left. Same structure as the home hero.

   NO SCRIM, NO TINT — SAME RULE AS THE HOME HERO. Which means the
   placement has to earn it, because this photograph is bright almost
   everywhere: 205-214 across the top third (1.5:1 for white), 150-170
   down the sides. The one genuinely dark area is the crew's own navy
   jackets through the middle.

   So the type goes on the jackets. Measured across the band the type
   actually occupies:

     source y 36-46%   mean  3.4:1    <- too bright, this is faces
     source y 46-56%   mean  8.2:1
     source y 50-60%   mean 10.3:1
     source y 54-64%   mean 10.5:1

   The crop and the padding below are both set to land the type in
   that 48-58% window. Move either one and the contrast goes with it.

   SUPERSEDED BY THE CARD — see .ahero__card below. Everything above
   still describes the crop, and the crop is unchanged, but the type
   no longer depends on it for contrast. Leaving the numbers because
   they are the record of WHY the card was needed, and the flaw in
   them is the useful part: every figure here was measured for WHITE
   type. The headline is not all white.
   ------------------------------------------------------------ */

.ahero {
  position: relative;
  /* Required by the zoom on .ahero__photo below — the scaled image is
     larger than this box and would otherwise paint outside it. */
  overflow: hidden;
  /* Two-stop gradient rather than a flat fill, per the dark-section
     rule: a perfectly flat charcoal is what the display pipeline
     dithers into a visible diagonal hairline. It only shows through
     while the photograph is still loading, but it costs nothing. */
  background: linear-gradient(180deg, #131B2E 0%, #0F172A 100%);
}

.ahero__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The band is roughly 3.4:1 and the source is 3:2, so cover can
     only ever show about 45-50% of the frame's height. The crew fill
     it top to bottom (heads at 11%, boots at 97%), which means the
     result is always a head-to-waist crop — the only question is
     where it starts.

     15% with the shorter band below shows source y 8-53% BEFORE the
     zoom; the zoom then takes it to 8-48%. The helmets clear the top
     edge by a couple of percent. 30% was tried first and cut straight
     through the hats. */
  object-position: 50% 15%;

  /* ---- THE ZOOM ----

     Cropping alone could not do this. object-fit: cover picks its
     window from the BAND'S aspect ratio, so a shorter band shows less
     of the frame but at exactly the same magnification — the crew stay
     the size they were and just lose their legs. The ask was to see
     them closer, so the picture has to actually scale.

     origin 50% 12% rather than the centre, and that is what makes one
     number work at both ends. Scaling about the centre pulls the top
     of the frame down as hard as it pushes the bottom out, which eats
     the helmets first. Anchoring near the top means the zoom mostly
     spends itself on the bottom of the frame — which is the legs, and
     the legs are what we are trying to lose.

     1.15 IS DELIBERATELY MODEST, and the constraint is resolution
     rather than taste. The source is 2000px wide and the band is 1920
     at full width, so there is almost no headroom: at 1.15 the browser
     draws 2208px from a 2000px file, a 1.10x upsample that does not
     show. 1.4 would have framed better and would have been visibly
     soft — the wrong trade on a page where softness is the complaint.
     The short band does the rest of the work, for free.

     Net on desktop: source y 8.7-48%, helmets to mid-torso. */
  transform: scale(1.15);
  transform-origin: 50% 12%;
  z-index: 0;
}

.ahero__inner {
  position: relative;
  z-index: 2;
  max-width: 1240px;
  margin: 0 auto;
  /* The band height lives here rather than on the section so the
     padding and the minimum are measured against the same box.

     SHORTER THAN IT WAS — 680 down to 580. The old value was tall on
     purpose, and the reason is now dead: a taller band showed more of
     the frame's height, which was needed to reach the dark part of the
     jackets for the type to sit on. The type sits on .ahero__card now
     and carries its own ground, so height no longer buys contrast. It
     only buys legs.

     This is half of the tighter crop and it is the free half — cropping
     by band height costs no resolution at all, where the zoom above
     costs a little. Doing as much as possible here is what let the zoom
     stay at 1.15. */
  min-height: clamp(430px, 66svh, 580px);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* Bottom padding kept tight so the type sits low, on the jackets
     rather than up on the faces. */
  padding: 40px 30px clamp(28px, 4vh, 44px);
}

/* ------------------------------------------------------------
   THE CARD — A KNOWN GROUND UNDER THE TYPE

   WHY THIS EXISTS, and it is not the white type. Measured against the
   pixels genuinely under each element, at the crop above:

                      white          green #71B745
     eyebrow      8.99 / 6.82      3.66 / 2.78
     headline     8.69 / 4.16      3.54 / 1.69
     accent word  9.25 / 4.81      3.77 / 1.96

   (mean / worst tenth, i.e. the odds a stroke crosses a bright pixel.)

   The white column is fine and always was — 4.16:1 at its worst is
   past the 3:1 large-text floor. The green column is the failure, and
   1.69:1 is not a near miss; at the very worst pixels it measures
   1.19:1, which is green type on green. The crew wear hi-vis green
   and green trousers, and the accent word is the brand green, so the
   headline was landing on its own colour.

   PLACEMENT CANNOT FIX THAT ONE, which is what separates this from
   every earlier pass on this hero. Moving the crop trades one green
   region for another: the jackets are green at the shoulders, the
   trousers are green below, and the block sits between them. There is
   no window in this frame where a green word has a dark ground. The
   measurements at the top of this file never caught it because they
   were all taken for white.

   So the ground stops being the photograph. A card gives every glyph
   the same known backdrop no matter what is behind it:

     white  on the card   18.6:1
     green  on the card    7.6:1     <- from 1.69:1

   and, more to the point, those numbers no longer move if the photo,
   the crop or the viewport height changes.

   THE FILL IS NOT OPAQUE — .92 to .95. Opaque reads as a box pasted
   onto the picture; at this alpha the crew still show through enough
   that it reads as sitting ON the photograph, and the contrast cost
   is under half a stop. The blur is what buys that: 3px of backdrop
   blur kills the busy jacket detail behind the type, so the letters
   have nothing structured to compete with. Alpha alone, without the
   blur, still reads noisy at this size.

   TWO-STOP GRADIENT + NOISE, not a flat fill, per the dark-section
   rule this file already follows for .ahero and .stats. A card is
   smaller than the full-width sections that rule was written for, but
   it is the same flat charcoal the pipeline dithers, and the fix
   costs nothing.

   THE TEXT-SHADOWS ARE GONE from .ahero__title and .ahero__accent.
   They existed to separate glyphs from a busy photograph and there is
   no longer a busy photograph under them — kept, they only soften
   type that now has a clean ground.
   ------------------------------------------------------------ */
.ahero__card {
  position: relative;
  overflow: hidden;
  align-self: flex-start;
  max-width: min(660px, 100%);
  padding: 26px 34px 30px;
  border-radius: 20px;
  background: linear-gradient(158deg, rgba(19, 27, 46, .92) 0%, rgba(11, 18, 38, .95) 100%);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  /* Wide and soft, same reasoning the old text-shadow was written
     with: it separates the card from the photograph by depth rather
     than by drawing an edge. */
  box-shadow: 0 18px 48px rgba(8, 14, 28, .38);
}

.ahero__card > .noise-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: .035;
  mix-blend-mode: overlay;
}

/* Above the noise layer. */
.ahero__card > .ahero__eyebrow,
.ahero__card > .ahero__title {
  position: relative;
  z-index: 1;
}

.ahero__eyebrow {
  /* inline-block, matched to .hero__eyebrow on the home page: as a
     full-width block the empty rest of the line sits over whatever
     the photograph has there, and a contrast measurement then
     describes pixels no glyph touches. Inside the card that reason is
     gone — the ground is uniform now — but it stays for the home
     page match. */
  display: inline-block;
  align-self: flex-start;
  margin: 0 0 14px;
  font-family: var(--font);         /* Montserrat, as the home hero */
  font-size: clamp(14px, 1.6vh, 17px);
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #FFFFFF;
}

.ahero__title {
  margin: 0;
  max-width: 18ch;
  /* Identical to .hero__title on the home page: DM Sans 900, same
     leading, same tracking, and the same SINGLE soft shadow rather
     than a multi-pass stack. The stack reads as an outline with a
     cloud behind it; one wide low-opacity shadow separates the glyph
     the way depth does, with no edge for the eye to find. */
  font-family: var(--font-head);
  font-weight: 900;
  font-size: clamp(34px, min(5.2vw, 8vh), 68px);
  line-height: 1.04;
  letter-spacing: -1.6px;
  color: #FFFFFF;
}

/* Green, matching the home hero's "Central NJ". Unchanged as a
   colour: the accent stays the brand green and the card is what makes
   it readable. Darkening it to clear the photograph was the other way
   to fix this and it was not taken — it would have put a second,
   duller green on the site for the sake of one word. */
.ahero__accent {
  color: var(--accent-green);
}

/* ------------------------------------------------------------
   PHONES — THE TYPE HAS TO COME UP OFF THE BOTTOM

   On a phone the whole frame is visible vertically and there is no
   way around it. cover scales to the larger ratio, so the band would
   have to be SHORTER than width x 0.667 (240px at 360 wide) before it
   began cropping top and bottom, and a 240px hero is not a hero.
   Every phone band therefore shows source y 0-100% and crops the
   sides instead.

   That matters because the bottom of this frame is green trousers and
   pale concrete, and with no scrim the type cannot sit there. Measured
   down the block the type occupies:

     source y 51-69%   mean 9.7:1   p90 4.4:1   <- jackets
     source y 62-74%   mean 7.9:1   p90 4.2:1
     source y 68-80%   mean 6.5:1   p90 1.6:1
     source y 74-86%   mean 5.6:1   p90 1.5:1   <- trousers

   p90 is the brightest tenth of the pixels under the block, i.e. the
   odds a stroke crosses a hi-vis stripe, and it falls off a cliff past
   74%. Bottom-aligned the title landed at 77% and the green accent
   word sat on green trousers. The large bottom padding below lifts the
   block back to roughly 51-69%. The empty legs underneath are the cost
   of not using a scrim.

   Horizontal stays centred, which was measured too and is not the
   obvious choice: biasing left to put the type on the darker jackets
   makes it WORSE (5.0:1 mean, p90 1.3:1), because the left edge of the
   frame is the pale roller door.

   THE LIFT IS RETIRED — the card replaced it. Every number above is a
   measurement of the PHOTOGRAPH under the type, and the type is not
   on the photograph any more. The 128-155px of bottom padding this
   block used to carry existed for one reason, stated plainly in the
   line it replaced: it pushed the block up off the trousers, and the
   empty legs underneath were called the cost of not using a scrim.
   There is a ground now, so the cost does not have to be paid, and
   the padding comes back to an ordinary value.

   THE ZOOM IS WHERE THE PHONE STOPS BEING A COMPROMISE. Everything
   above starts from "the whole frame is visible vertically and there
   is no way around it" — true of CROPPING, which is all this block
   could do, and not true of scaling. .ahero__photo now scales, so the
   phone can be framed instead of merely accepted.

   IT ZOOMS HARDER THAN DESKTOP — 1.9 against 1.15 — and that is the
   opposite of the usual instinct, for a resolution reason that
   inverts here. Desktop is starved: a 2000px file across a 1920px
   band leaves 4% of headroom, so 1.15 is nearly all there is. A phone
   draws the same file into a 780px-wide box, so it is throwing away
   more than half the pixels it has. 1.9 brings that to 1482px and is
   still UNDER the native 2000 — the phone zoom is free, and costs
   less than the desktop one does.

   Net: source y 5.7-58%, helmets to waist, on a frame that used to
   show all six men head to boot at postage-stamp size.

   object-position is dropped rather than retuned. With the full height
   visible there is no vertical slack for it to distribute, so it was
   already doing nothing on phones; the framing is entirely the
   transform-origin's job now.
   ------------------------------------------------------------ */
@media (max-width: 720px) {
  .ahero__inner {
    min-height: clamp(430px, 62svh, 520px);
    padding: 32px 22px clamp(34px, 6svh, 56px);
  }
  .ahero__photo { transform: scale(1.9); }
  .ahero__title { max-width: none; }
  /* Full width at this size. A 660px card in a 360px viewport is
     already full width, but the padding comes in — 34px of side
     padding inside a 316px card leaves too little for the headline. */
  .ahero__card { padding: 22px 22px 24px; border-radius: 16px; }
}

@media (max-width: 400px) {
  /* Side padding only. An earlier min-height override here shortened
     the band below the 720px rule and dragged the type back down onto
     the trousers. That specific failure is no longer possible — the
     ground travels with the type now — but the override is still
     wrong for its own reason: it fought the 720px rule. */
  .ahero__inner { padding-left: 18px; padding-right: 18px; }
  .ahero__card { padding: 20px 18px 22px; }
}

/* ============================================================
   WHY TRUST US

   A ONE-TO-ONE COPY OF THE CLIENT'S LIVE SECTION at
   njindooroutdoor.com/about. Every number below was read off that
   page with getComputedStyle at 1489px wide — not eyeballed, and not
   re-derived from v2's scale. Where a value looks off-system for v2,
   that is deliberate: the client asked for his section.

   NOT copied from this repo's own /about/ rebuild. That is a
   different design — navy gradient, line-icon medallions, live text
   in the cards — and an earlier pass copied it here by mistake.

   The four cards are flat images with their captions baked in, so
   there is barely any type to style: this is a background, a heading
   plate, one line under it, and a 4-up image row.
   ============================================================ */

/* ------------------------------------------------------------
   THE TWO BACKDROP PHOTOGRAPHS — WHY THEY ARE BLURRED NOW

   Both bands read as grainy, and the cause is the same in each: the
   file is far smaller than the box it is painted across, so the
   browser upsamples it and the compression artifacts come up with it.
   Measured on the rendered page at 1905px wide:

     trust-bg.webp     1500x482   drawn at 1.42x   30KB
     reviews-bg.webp    600x747   drawn at 3.17x   23KB

   3.17x is the severe one — a 600px-wide file stretched across 1905px
   of band. There is no encoder setting that fixes that, because the
   detail is not in the file to begin with; re-encoding at higher
   quality would only preserve the blur it already has more faithfully.

   So the fix is to stop pretending they are sharp photographs and
   commit to them as depth-of-field. A deliberate blur reads as a
   photographic backdrop; an accidental 3.17x upsample reads as a
   broken image. Same pixels, and the second one is what was on the
   page. The blur is scaled to the abuse each file is taking — 5px for
   the 1.42x band, 12px for the 3.17x one.

   MECHANICS. filter on the section itself would blur the cards and the
   type with it, so the photograph moves to a ::before layer and the
   filter goes there. The scrim rides INSIDE that same layer rather
   than as a second element: blurring a flat gradient is a no-op, so
   one layer does both jobs.

   inset is NEGATIVE so the blur's own soft edge falls outside the
   visible box. Without it a blurred layer fades to transparent at its
   own boundary and draws a pale border around the whole band. The
   bleed is set to twice the blur radius, which is where the falloff
   has gone to nothing.

   Both scrims are two-stop gradients rather than flat fills, per the
   dark-section rule this file already follows for .ahero and .stats.
   ------------------------------------------------------------ */

.vals {
  position: relative;
  isolation: isolate;
  padding: 100px 28px;
  background: #0B1226;
}

.vals::before {
  content: "";
  position: absolute;
  inset: -10px;
  z-index: -1;
  background:
    linear-gradient(rgba(11, 18, 38, .42), rgba(11, 18, 38, .58)),
    url("/assets/img/trust-bg.webp") 50% 50% / cover no-repeat;
  filter: blur(5px);
}

/* The cards sat straight on the photograph before. They keep doing so
   — this only puts them above the ::before layer. */
.vals__inner {
  /* relative + z-index only to sit above the blurred ::before layer. */
  position: relative;
  z-index: 1;
  max-width: 1340px;          /* the live row measures 1340 across */
  margin: 0 auto;
}

.vals__head {
  margin-bottom: 87px;
  text-align: center;
}

/* The black rounded plate is the live site's heading treatment — the
   same one "ABOUT US" wears at the top of that page. inline-block so
   the plate hugs the words rather than spanning the row. */
.vals__title {
  display: inline-block;
  margin: 0;
  /* Uniform 24px. The live plate measures 421x110 around a 62.4px
     line box, which is 24 top/bottom, and 24 on the sides lands the
     width on 421 exactly. */
  padding: 24px;
  border-radius: 17px;
  background: #000000;
  font-family: Poppins, var(--font);
  font-weight: 500;
  font-size: clamp(28px, 3.6vw, 52px);
  line-height: 1.2;
  letter-spacing: normal;
  color: #FFFFFF;
}

/* Small and secondary, under the plate rather than over it. This is
   the line v1 promotes to a 50px headline; on the live site it is
   16px supporting text and that is what is copied. */
.vals__lede {
  margin: 24px 0 0;
  font-family: Poppins, var(--font);
  font-size: 16px;
  font-weight: 600;
  color: #FFFFFF;
}

/* ------------------------------------------------------------
   WHY TRUST US — CAROUSEL

   Was a four-up grid. The scroll/snap/arrow mechanics are shared with
   the home page's reviews row and with the reviews row below; see the
   initCarousel block in v2.js. Only the sizing is local.

   THREE VISIBLE, NOT FOUR, and it is worth saying why a carousel that
   shows fewer cards than the grid did is not a downgrade. Four across
   a 1184px container put each card at 313px, which DOWNSCALED the
   artwork: these are flat images with the caption baked in, natural
   width 358. Three across puts the card at 402px, and the image is
   capped at its natural 358 below — so it now renders at 1:1 instead
   of at 87%, and the row got sharper by being made shorter.

   That cap is load-bearing for the same reason. Without it a 402px
   card would stretch a 358px file UP, which is the one thing that
   would make the graininess complaint worse rather than better. */
.vals__carousel {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
}

/* A CAROUSEL AT EVERY WIDTH, which was the ask. A pass in between made
   this a four-up grid on desktop and switched the scroller on only
   below 900, arguing that a screen wide enough to show the set should
   show the set. That is a real argument and it is not the one that was
   asked for — the row was asked to be a carousel, so it is one all the
   way up. Recorded because it is worth knowing the trade was seen:
   four-up shows every card at once, three-up-plus-arrows shows fewer
   but renders each at 1:1. If the set-at-a-glance version is wanted
   back, the four-up grid is the change, not a tweak to this. */
/* ALL FOUR AT ONCE. This is a grid again; the carousel that used to
   live here now only exists below 900 — see the block in the
   responsive section. The rule is: on a screen wide enough to show
   the set, show the set. Hiding three quarters of the trust signals
   behind an arrow on a 1440px monitor costs something and buys
   nothing — the job of this row is to be taken in at a glance.

   The sharpness argument that justified 3-up still holds and is
   simply outweighed. Four across this container puts each card near
   313px against a natural 358, so the artwork renders at about 87%.
   That is a DOWNSCALE, which is the harmless direction; what made
   these look grainy was upscaling past 358, and the max-width on the
   img below still prevents that at every width. */
/* ALL FOUR AT ONCE — THIS IS A GRID, NOT A SCROLLER.

   PART 1 OF 3. This rule, the .revs__grid rule below it, and the
   "BELOW 900" media query near the end of the file are one change and
   only work together. If you are about to revert one of them because
   it looks inconsistent with the others, revert all three or none —
   half of this change is worse than either whole.

   The rows carousel ONLY below 900. Above that every card is on
   screen: hiding three quarters of the trust signals behind an arrow
   on a 1440px monitor costs something and buys nothing.

   The sharpness argument that once justified 3-up still holds and is
   simply outweighed. Four across puts each card near 313px against a
   natural 358, so the artwork renders at ~87% — a DOWNSCALE, the
   harmless direction. What made these look grainy was upsampling past
   358, and the max-width on the img below still prevents that. */
.vals__grid {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 30px;
  margin: 0;
  padding: 4px;
  list-style: none;
}
.vals__grid::-webkit-scrollbar { display: none; }
.vals__grid:focus-visible { outline: 3px solid #FFFFFF; outline-offset: 3px; }

.vals__card { margin: 0; scroll-snap-align: start; }

/* The card IS the image — white panel, artwork and caption are all
   in the file, so there is nothing here but the corner radius.

   max-width 358 is the file's NATURAL width and it is the sharpness
   fix, not a layout preference: past it the browser upsamples a
   already-soft 8KB graphic. Centred, so a card wider than 358 puts the
   slack either side rather than all on one edge. */
.vals__card img {
  display: block;
  width: 100%;
  max-width: 358px;
  margin: 0 auto;
  height: auto;
  border-radius: 10px;
}

/* --- arrows and dots, shared shape with the home page's ---

   OFF BY DEFAULT NOW. Both rows are grids at desktop widths, so there
   is nothing to page through and an arrow would be a control that does
   nothing.

   THAT IS NO LONGER THE SHAPE OF IT. The rows scroll at every width
   now, so the arrows are on from the start and switch off once, under
   560, where the dots take over and swiping is the native gesture.
   Two states, not three. The rule that used to sit here defaulted them
   to display:none and turned them on inside a 900px block that no
   longer exists — which left the arrows in the markup, wired to a live
   carousel, and invisible. */
.vals__arrow,
.revs__arrow {
  /* OFF BY DEFAULT, AND THAT IS NOT A BUG.

     These rows are GRIDS above 900 (see .vals__grid / .revs__grid and
     the "BELOW 900" media query). A grid has nothing to page through,
     so a visible arrow here would be a control that does nothing.

     The arrows are switched back on inside the 900 block — the only
     band where these rows scroll AND there is room for a 38px button
     beside the track — and off again under 560, where the dots take
     over. Three states, and the arrows exist only in the middle one.

     Do not "restore" this to display: grid without also reverting the
     two grid rules; on their own, always-on arrows sit dead on a
     static grid at desktop width. */
  display: none;
  flex: none;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #FFFFFF;
  color: #1F1F1F;
  cursor: pointer;
  place-items: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .28);
  transition: color .2s linear, box-shadow .2s linear, opacity .2s linear;
}
.vals__arrow svg,
.revs__arrow svg { width: 18px; height: 18px; }
.vals__arrow:hover,
.revs__arrow:hover { box-shadow: 0 3px 12px rgba(0, 0, 0, .4); }
.vals__arrow[disabled],
.revs__arrow[disabled] { opacity: .3; pointer-events: none; }
.vals__arrow:focus-visible,
.revs__arrow:focus-visible { outline: 3px solid #FFFFFF; outline-offset: 3px; }

/* Both rows sit on dark grounds, so the dots invert from the home
   page's — white for the active one instead of blue, which would
   disappear against the band.

   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.
   They swap in below, where the arrows swap out. */
.vals__dots,
.revs__dots {
  display: none;
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  justify-content: center;
  align-items: center;
  gap: 7px;
}

.vals__dots li,
.revs__dots li {
  width: 7px;
  height: 7px;
  border-radius: 100px;
  background: rgba(255, 255, 255, .38);
  /* Width animates, not transform — a transform would blur the dot
     mid-animation on a subpixel grid. */
  transition: width .25s ease, background .25s ease;
}

.vals__dots li.is-at,
.revs__dots li.is-at {
  width: 20px;
  background: #FFFFFF;
}

@media (prefers-reduced-motion: reduce) {
  .vals__dots li,
  .revs__dots li,
  .vals__arrow,
  .revs__arrow { transition: none; }
}

/* ============================================================
   CUSTOMER SUCCESS — REVIEWS

   A ONE-TO-ONE COPY OF THE CLIENT'S LIVE SECTION at
   njindooroutdoor.com/about, measured with getComputedStyle at
   1489px wide. Same rule as Why Trust Us above: where a value looks
   off-system for v2, it is the client's and stays.

   TWO BANDS. .revs__band is flat #00529F and carries only the
   heading; .revs__body is the same blue with a 74% black scrim over
   a photograph and carries the cards. The hard edge between them is
   the point — do not merge them into one padded section.
   ============================================================ */

.revs { margin: 0; }

/* Flat blue plate, heading only. */
.revs__band {
  padding: 80px 28px;
  background: #00529F;
  text-align: center;
}

.revs__title {
  margin: 0;
  font-family: Poppins, var(--font);
  font-weight: 600;
  font-size: clamp(34px, 4.8vw, 70px);
  line-height: 1.2;
  letter-spacing: normal;
  /* Not pure white — the live value is #ECF0EF. */
  color: #ECF0EF;
}

/* The scrim is a real gradient layer over the photograph, not an
   opacity on the image, so the cards above it stay at full contrast. */
.revs__body {
  position: relative;
  isolation: isolate;
  padding: 88px 28px 133px;
  background: #00529F;
}

/* 12px, against the .vals band's 5px, because this file takes 3.17x
   where that one takes 1.42x. The 74% black scrim is carried over
   unchanged — it was already doing the heavy lifting on contrast, and
   it is what let this section get away with a 600px file for as long
   as it did. */
.revs__body::before {
  content: "";
  position: absolute;
  inset: -24px;
  z-index: -1;
  background:
    linear-gradient(rgba(0, 0, 0, .72), rgba(0, 0, 0, .76)),
    url("/assets/img/reviews-bg.webp") 50% 50% / cover no-repeat;
  filter: blur(12px);
}

/* ------------------------------------------------------------
   CUSTOMER SUCCESS — CAROUSEL

   Was a 3x2 grid of six. Same mechanism as the two rows above.

   THE EVEN-HEIGHT PROBLEM SOLVES ITSELF HERE. As a grid the second row
   ran visibly taller than the first, because the quotes are real
   Google reviews of wildly different lengths and each grid row sized
   to its own tallest card — the live site has the same ragged look.
   One row of six cannot have a second row to disagree with, so the
   cards now stretch to a single common height and the raggedness is
   gone as a side effect rather than as a fix.
   ------------------------------------------------------------ */
.revs__carousel {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 1296px;      /* 1248 track + two 38px arrows + gaps */
  margin: 0 auto;
}

/* A CAROUSEL AT EVERY WIDTH, same call as the trust row above and for
   the same reason — it is what was asked for.

   NO grid-auto-rows HERE. A column-flow track is a SINGLE row, so the
   ragged-row problem it was added to solve cannot occur: there is no
   second row to disagree with the first. Setting 1fr on a one-row
   track would size that row against the container's height rather than
   its content, which is a different bug rather than a spare safeguard.

   The slack still falls BELOW the author, not above — see the note on
   .revs__who. Equal heights, deliberately unaligned avatars. */
/* THREE UP, TWO ROWS — six reviews, all of them on screen. Same
   reasoning as the trust row above: a grid on anything desktop-sized,
   and the carousel only takes over below 900.

   grid-auto-rows: 1fr IS WHAT MAKES GOING BACK TO A GRID FREE. The
   original grid looked bad for one specific reason: each row sized to
   its own tallest card, so a row holding Jonathan's long review towered
   over a row holding Harold's one-liner — the live site has exactly
   that fault, and the single-row carousel was hiding it rather than
   fixing it. 1fr forces every row to the same height, so all six cards
   match and the raggedness is gone at the source.

   The slack still falls BELOW the author rather than above — see the
   note on .revs__who. Equal card heights, deliberately unaligned
   avatars. */
/* THREE UP, TWO ROWS — six reviews, all on screen.

   PART 2 OF 3. See the note on .vals__grid above: this, that, and the
   "BELOW 900" media query are one change.

   grid-auto-rows: 1fr IS WHAT MAKES THE GRID FREE. The original grid
   looked bad for one specific reason: each row sized to its own
   tallest card, so a row holding Jonathan's long review towered over
   one holding Harold's one-liner — the live site has exactly that
   fault, and the single-row carousel was hiding it rather than fixing
   it. 1fr forces every row to the same height, so all six cards match
   and the raggedness is gone at the source. Measured: 357px each.

   The slack still falls BELOW the author, not above — see .revs__who.
   Equal card heights, deliberately unaligned avatars. */
.revs__grid {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  gap: 24px;
  /* 1248 is the live grid width. */
  max-width: 1248px;
  margin: 0 auto;
  padding: 4px;
  list-style: none;
}
.revs__grid::-webkit-scrollbar { display: none; }
.revs__grid:focus-visible { outline: 3px solid #FFFFFF; outline-offset: 3px; }

.revs__card {
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  padding: 24px;
  background: #FFFFFF;
  border-radius: 12px;
}

.revs__stars {
  display: block;
  width: 164px;
  height: 24px;
  margin: 0 0 24px;
}

.revs__quote {
  margin: 0 0 32px;
  font-family: Poppins, var(--font);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.6;
  color: #000000;
}

/* NO margin-top: auto. The cards stretch to equal height per row, but
   the author follows the quote directly and leaves the slack below —
   on the live page Harold's avatar sits well above Rob's in the same
   row. Bottom-pinning them lines the avatars up, which looks tidier
   and is wrong. */
.revs__who {
  display: flex;
  align-items: center;
  gap: 12px;
}

.revs__avatar {
  flex: none;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  object-fit: cover;
}

/* Open Sans here, Poppins in the quote above it — that split is the
   live site's, not a slip. */
.revs__name {
  font-family: "Open Sans", var(--font);
  font-size: 16px;
  font-weight: 700;
  color: #000000;
}

/* ============================================================
   STATS STRIP

   A band, not the home page's six-card grid. The home page is
   selling and can afford six; this is one line of reassurance inside
   a page that is already about the company, so it is four numbers on
   a single rule and nothing else.

   Numbers are the same ones the home page's impact grid uses, which
   in turn come from v1's structured data: 29 in areaServed, a 2000
   foundingDate against 2026, aggregateRating 5.0, and an
   openingHoursSpecification covering Monday to Sunday. v1's own
   About-page bar carries a fourth reading "100% Licensed & Insured";
   that one is dropped here because it duplicates the value card
   directly above it.
   ============================================================ */

.stats {
  position: relative;
  /* Two-stop gradient plus noise, same as the footer and for the same
     reason: a flat dark fill is what the display pipeline dithers,
     and the dither shows up as a diagonal hairline across the band. */
  background: linear-gradient(180deg, #131B2E 0%, #0F172A 100%);
  padding: 46px 20px;
}

.stats > .noise-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: .035;
  mix-blend-mode: overlay;
}

.stats__inner {
  position: relative;
  z-index: 1;
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 28px;
  list-style: none;
  padding: 0;
}

.stats__item {
  text-align: center;
  /* Hairline dividers between the figures rather than boxes around
     them, which is what keeps this reading as a strip. */
  border-left: 1px solid rgba(255, 255, 255, .12);
}
.stats__item:first-child { border-left: 0; }

.stats__num {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(32px, 3.4vw, 46px);
  line-height: 1;
  letter-spacing: -.8px;
  color: #FFFFFF;
}

.stats__label {
  margin: 10px 0 0;
  font-size: clamp(12px, 1vw, 14px);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .62);
}

/* ⚠ THE .areas RULES BELOW ARE UNUSED AS OF THIS EDIT.

   The "Where We Work / 29 towns" section they styled was removed from
   about/index.html because it does not exist on the live site — the
   live town list is the FOOTER block, which every page already has
   and which about/index.html still carries as .sfoot__areas. The full
   reasoning is in the comment left where the section used to be.

   They are kept rather than deleted because v2 is not tracked in git,
   so a deletion here is not recoverable. Nothing else on the site
   uses .areas — grep says about/index.html was the only consumer —
   so this block can be deleted whenever someone is confident the
   section is not coming back.
   ============================================================ */
/* ============================================================
   AREAS WE SERVICE

   The full 29, spelled out. The footer already lists them, but as one
   run-on line of middots that is a legal-ish disclosure, not
   something anyone reads to check their own town. Here they get
   columns and enough leading to scan.
   ============================================================ */

.areas {
  background: #FFFFFF;
  padding: 76px 20px 84px;
}

.areas__inner {
  max-width: 1200px;
  margin: 0 auto;
}

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

.areas__title {
  margin: 0 0 16px;
  max-width: 22ch;
  font-family: var(--font-head);
  font-weight: 500;
  font-size: clamp(28px, 3.2vw, 44px);
  line-height: 1.1;
  letter-spacing: -.05em;
  color: var(--head-ink);
}

.areas__copy {
  margin: 0 0 36px;
  max-width: 60ch;
  font-size: clamp(16px, 1.15vw, 18px);
  line-height: 1.6;
  color: var(--body-ink);
}

/* Columns rather than a grid: 29 items leave a ragged last row in any
   fixed grid, and column flow just fills evenly and rebalances itself
   at every width. */
.areas__list {
  margin: 0 0 34px;
  padding: 0;
  list-style: none;
  columns: 4;
  column-gap: 32px;
}

.areas__list li {
  /* break-inside stops a row being split across the column boundary,
     which column layout will happily do. */
  break-inside: avoid;
  position: relative;
  padding: 9px 0 9px 22px;
  font-size: 16px;
  line-height: 1.35;
  color: var(--head-ink);
  border-bottom: 1px solid #EDF0F5;
}

.areas__list li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 50%;
  width: 6px;
  height: 6px;
  margin-top: -3px;
  border-radius: 50%;
  background: var(--accent-green);
}

.areas__note {
  margin: 0;
  font-size: 16px;
  line-height: 1.6;
  color: var(--body-ink);
}
.areas__note a {
  font-weight: 700;
  color: var(--blue);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.areas__note a:hover { color: var(--blue-hover); }

/* ------------------------------------------------------------
   RESPONSIVE

   Breakpoints match the ones v2.css already uses for .about, so the
   whole page steps down at the same widths instead of each section
   reflowing at a different one.
   ------------------------------------------------------------ */
/* The stats strip breaks earlier than the rest of the page. Four
   across a 768px tablet leaves each figure 157px wide, which wraps
   "Years in Business" onto two lines and makes the strip twice as
   tall as it should be. 2x2 holds its shape instead. */
@media (max-width: 860px) {
  .stats__inner { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 30px 20px; }
  /* With two per row the left rule would fall down the middle of the
     block; the even column carries it instead. */
  .stats__item { border-left: 0; }
  .stats__item:nth-child(even) { border-left: 1px solid rgba(255, 255, 255, .12); }
}

/* STILL A GRID, JUST NARROWER. Four trust cards and three review cards
   both run out of room around here, and the answer is fewer COLUMNS,
   not a scroller — a 2x2 and a 2x3 still show every card at once, which
   is the entire point.

   grid-template-columns, NOT grid-auto-columns. That distinction is
   what silently broke this block once already: while these rows were
   single-row scrollers the visible count was set by how wide each auto
   column was, so the old rules here said grid-auto-columns. They are
   grids again, laid out on explicit tracks, and an auto-columns
   declaration on an explicit grid is simply ignored — the rules were
   still in the file, still looked right, and did nothing at all. */
/* STILL A GRID, JUST NARROWER. Four trust cards and three review
   cards both run out of room around here, and the answer is fewer
   COLUMNS, not a scroller — a 2x2 and a 2x3 still show every card.

   grid-template-columns, NOT grid-auto-columns. That distinction has
   already broken this block once: while these rows were scrollers the
   visible count was set by how wide each AUTO column was. They are
   explicit grids now, and an auto-columns declaration on an explicit
   grid is ignored — the old rules sat here looking correct and doing
   absolutely nothing. */
@media (max-width: 1200px) {
  .vals__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* 87px of air under the heading is measured at full width; at 2-up
     the row starts higher and that much gap reads as a hole. */
  .vals__head { margin-bottom: 56px; }

  .revs__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ------------------------------------------------------------
   BELOW 900 — AND ONLY BELOW 900 — THESE BECOME CAROUSELS

   PART 3 OF 3. See .vals__grid for the other two.

   The single place the scrolling behaviour is switched on. Above it
   both rows are plain grids showing every card; a phone cannot show
   four trust panels or six reviews at a readable size, so down here
   each row becomes a swipeable track with a peek at the next card.

   Everything the scroller needs lives here rather than in the base
   rules, so the desktop grid does not carry dead scroll properties:
   the auto-flow, the snap axis, the hidden scrollbar and the paint
   containment all arrive together and leave together.

   grid-auto-rows returns to auto because a column-flow track is a
   SINGLE row — leaving 1fr would size that one row against the
   container's height rather than its content.

   The JS needs no breakpoint of its own. initCarousel binds to arrows
   that are display:none above this width and to a track with no
   overflow, so it measures a zero scroll range and sits inert. That
   is also why syncArrows special-cases a zero range — see v2.js.
   ------------------------------------------------------------ */
@media (max-width: 900px) {
  .vals__grid,
  .revs__grid {
    grid-template-columns: none;
    grid-auto-flow: column;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    /* Matches the 4px padding, or the first card parks at scrollLeft 4
       and the "at the start" test never fires. */
    scroll-padding: 0 4px;
    scrollbar-width: none;
    overscroll-behavior-x: contain;
    /* overflow clips PAINT but does not make this a containing block,
       so an absolutely positioned descendant would otherwise escape it
       and widen the document. Same note as .reviews__track in v2.css. */
    contain: paint;
  }

  /* Two visible, gaps subtracted so the second card lands flush. */
  .vals__grid { grid-auto-columns: calc((100% - 30px) / 2); }
  .revs__grid {
    grid-auto-columns: calc((100% - 24px) / 2);
    grid-auto-rows: auto;
  }

  .vals__arrow,
  .revs__arrow { display: grid; }
}

@media (max-width: 760px) {
  /* 86%, not 100% — the sliver of the next card is what tells you the
     row scrolls. Same value the home page's reviews row uses. */
  .revs__grid { grid-auto-columns: 86%; }
}

@media (max-width: 960px) {
  .areas__list { columns: 3; }
}

@media (max-width: 720px) {
  .vals { padding: 60px 18px; }
  /* One card with a peek. The 360px cap that used to sit on .vals__card
     is gone — it kept a lone image from swamping the caption baked into
     it, and the img's own max-width: 358px now does that job at every
     width instead of only this one. */
  .vals__grid { grid-auto-columns: 86%; gap: 18px; }
  .vals__head { margin-bottom: 40px; }
  .vals__title { padding: 16px; }

  .revs__band { padding: 52px 18px; }
  .revs__body { padding: 48px 18px 64px; }

  .stats { padding: 36px 18px; }

  .areas { padding: 60px 18px 66px; }
  .areas__title { max-width: none; }
  .areas__list { columns: 2; column-gap: 20px; }
  .areas__list li { font-size: 15px; padding-left: 18px; }
}

@media (max-width: 420px) {
  .areas__list { columns: 1; }
}

/* At this width a 38px arrow either overlaps the card it is meant to
   sit beside or steals width the card needs. Swiping is the native
   gesture here anyway; the dots take over as the signal that there is
   more to the row. Matches the home page's reviews carousel, which
   makes the same swap at the same width. */
@media (max-width: 560px) {
  .vals__arrow,
  .revs__arrow { display: none; }

  .vals__dots,
  .revs__dots { display: flex; }

  .vals__carousel,
  .revs__carousel { gap: 0; }
}


/* Focus ring on the summary, since that is the element that actually
   takes keyboard focus. Offset inward so it is not clipped by the
   card's own rounding. */

/* Chevron drawn in CSS rather than shipped as an icon: two borders
   rotated 45 degrees. Cheaper than an SVG per item and it inherits
   the colour change on hover for free. */


/* The chevron is the only thing that moves, and it is a transform, so
   this is cheap. Still gated: a rotating marker is exactly the kind of
   motion people turn off. */
/* ============================================================
   FINAL CTA BAND

   REDESIGNED, not copied. The client's live CTA is a flat grey
   half-panel with centred black text and two equally-weighted
   buttons; he asked for this one to look nicer. His copy, labels,
   links and number are untouched.

   RESTRAINT PASS. An earlier version of this band was a navy panel
   with a blue radial bloom, an eyebrow, two competing buttons and a
   rule-separated phone row with an icon. It was doing too much and
   read as decorated rather than designed. The reference the client
   pointed at (coconutcleaningco.com, "Join the Club") gets more out
   of far less: photo on one half, a light panel on the other, and
   nothing in it but a heading, a paragraph and ONE flat button, all
   centred with a lot of air. That structure is what is followed here.

   Five deliberate departures from that reference:
     - Sentence case, not its all-caps headline. Every other heading
       on this page is sentence case; matching its restraint is the
       point, not its typography.
     - Panel is --surface, not white. The .areas section directly
       above is #FFFFFF, so a white panel would have no edge. This
       inverts the reference's relationship instead of copying it.
     - Its footnote slot carries the phone number instead, placed
       BELOW the button rather than above it. The number is a real
       conversion path for this business, not a disclaimer, so it
       sits after the primary action rather than in front of it.
     - No glow anywhere. The button is a flat fill.
     - RANGED LEFT IN AN UNEVEN SPLIT, not centred in an even one.
       The straight copy of the reference was built first and shown
       side by side with seven alternatives; it lost. Centred, the
       band read as empty rather than calm — the type was small in a
       wide half and nothing lined up with anything. 58/42 with the
       text ranged left fixes both at once. The eight directions and
       the reasoning are in v2/_lab/cta-directions.html, which is a
       scratch file and is not part of the site.

   EVERY COLOUR PAIR HERE WAS MEASURED, none is below 5.3:1:
     #0B1226 on #F1F5F7 ....... 16.75   heading
     #4C5265 on #F1F5F7 ........ 6.68   body copy
     #3F4655 on #F1F5F7 ........ 8.29   phone note
     #0B1226 on #0FA608 ........ 5.74   button
   That last one is the fix for the live site's actual bug: it sets
   WHITE on #0FA608, which is 3.24:1 and fails AA. The green is his
   and is kept exactly; only the text on it flipped to navy.
   ============================================================ */

.cta {
  display: grid;
  /* NOT an even split. The photograph takes about 58% and the text
     42%, which does two things at once: it gives the picture enough
     width to be looked at rather than merely balanced against, and it
     narrows the text column enough that the headline can go up to
     54px without wrapping into a mess. An even split forced the
     heading down to 46px, and small type in a wide empty half was
     the whole reason the centred version read as unfinished. */
  grid-template-columns: 1.4fr 1fr;
  /* Nothing between the halves — the photograph runs to the seam and
     the panel picks up from it. */
  gap: 0;
  background: var(--surface);
}

.cta__media { position: relative; overflow: hidden; min-height: 420px; }

/* BARELY PUSHED IN, which is the opposite of what this rule used to
   say and is worth explaining. The old photograph was a wide office
   shot with the subject occupying about a third of the frame, so it
   needed scale(1.28) just to stop reading as a small figure in a big
   empty room. The replacement does not have that problem: the junk
   pile fills the frame edge to edge already, so the same 1.28 would
   crop the ends off the pile and throw away the house and the garage
   door behind it — and those are what make the scene read as somebody's
   home rather than a tip.

   1.06 exists only to hide the extreme edges, where a tree trunk on
   the left and a parked-up wall on the right are the weakest parts of
   the frame. The vertical aim sits slightly high, 48%, because the
   bottom of the source is foreground lawn — pulling up trades grass
   for the roofline and the boxes.

   transform-origin tracks object-position so the scale pushes out from
   the same point the crop is aimed at. Change the two together or not
   at all. */
.cta__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 48%;
  transform: scale(1.06);
  transform-origin: 50% 48%;
}

/* Generous padding is still the effect, but the block is set LEFT
   rather than centred. Centred text in a half-panel has no edge to
   line up against, so the heading, the paragraph, the button and the
   phone line each start at a different x and the whole thing drifts.
   Ranged left they share one margin and read as a designed column. */
.cta__panel {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 96px 56px;
  text-align: left;
}

/* Narrower than the old 34rem: with the column at 42% of the band,
   29rem is what keeps the headline to two lines at its new size. */
.cta__inner { max-width: 29rem; }

.cta__title {
  margin: 0 0 22px;
  font-family: var(--font-head);
  font-weight: 800;
  /* Up from a 46px ceiling. The narrower column is what pays for it. */
  font-size: clamp(32px, 4.1vw, 54px);
  line-height: 1.08;
  letter-spacing: -1px;
  color: var(--head-ink);
}

.cta__copy {
  /* No auto margins — it ranges left with everything else. */
  margin: 0 0 36px;
  /* ~60 characters a line, which is the readable measure. */
  max-width: 46ch;
  font-size: 16.5px;
  line-height: 1.65;
  color: var(--body-ink);
}

/* One button, one fill, no shadow. min-height 52 clears the 44px touch
   minimum with room to spare.

   BLUE, NOT GREEN — and the text colour had to move with it. The fill
   was #0FA608 with NAVY type, because white on that green measured
   2.28:1 and navy on it 8.16:1; the contrast table above is what
   picked navy. Neither number survives the swap, so re-deriving rather
   than carrying the navy across:

     white on #2F6FE5   4.64:1   <- passes AA for normal text
     navy  on #2F6FE5   4.01:1

   White wins here, which is the opposite of what won on the green.
   That inversion is the whole reason this is not a one-line change: a
   fill and its type colour are one decision, and keeping navy would
   have shipped the weaker of the two.

   var(--blue) rather than a literal, so this is now the same blue as
   the header's Book Now, every button on the home page, and the
   heading highlight blocks — which is what the swap buys. The green
   was the only one of its kind on the page. */
.cta__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding: 14px 38px;
  border-radius: 999px;
  background: var(--blue);
  font-family: var(--font);
  font-size: 15px;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  text-decoration: none;
  color: #FFFFFF;
  transition: background-color .2s ease;
}
.cta__btn:hover { background: var(--blue-hover); }

/* Live runs TWO buttons in this block — BOOK NOW! and GET A FREE
   ESTIMATE NOW! — and v2 had only the first. The second is restored
   as a ghost so the pair reads as primary + secondary rather than two
   equal pills competing on the same panel. Wraps rather than shrinks:
   the second label is 24 characters and will not sit beside the first
   on a narrow panel. */
.cta__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

/* The panel behind these is LIGHT, not the dark band it looks like in
   the full-width screenshot — the photograph is the left half only.
   So the ghost is a blue outline on white, not a white outline: the
   first pass used white-on-white and the button vanished. */
.cta__btn--ghost {
  background: transparent;
  border: 1.5px solid var(--blue);
  color: var(--blue);
}
.cta__btn--ghost:hover {
  background: rgba(47, 111, 229, .08);
  border-color: var(--blue-hover);
  color: var(--blue-hover);
}

/* The call line is one anchor now, matching live, so it needs to look
   like a link rather than inherit the note's body colour. */
.cta__note .cta__tel {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.cta__note .cta__tel:hover { color: var(--blue); }

/* Quiet by construction: the number is available without competing
   with the button for the eye. No rule above it, no icon — both were
   structure the line does not need. */
.cta__note {
  margin: 22px 0 0;
  font-size: 15px;
  color: #3F4655;
}

.cta__tel {
  font-weight: 700;
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid rgba(63, 70, 85, .35);
}
.cta__tel:hover { border-bottom-color: #0B1226; }

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

/* Stack below 900: the panel's 34rem measure and the photo's crop
   both stop working in a half-column narrower than about 440px. */
@media (max-width: 900px) {
  .cta { grid-template-columns: 1fr; }
  /* Taller than it was, and pushed in a little further than desktop.
     Stacked, the band is a wide short letterbox, so cover keeps the
     full width of the source and slices the top and bottom off it —
     the reverse of the desktop crop. 1.14 pulls the pile back up to
     filling that strip; much more and the house behind it goes, which
     is the bit that says "residential". */
  .cta__media { min-height: 320px; }
  .cta__media img { transform: scale(1.14); }
  .cta__panel { padding: 64px 28px 72px; }
  .cta__inner { max-width: none; }
}

@media (max-width: 480px) {
  .cta__panel { padding: 52px 18px 60px; }
  /* Full width rather than a lone pill floating in a narrow column. */
  .cta__btn { display: flex; width: 100%; padding-inline: 20px; }
  .cta__actions { flex-direction: column; width: 100%; }
}

/* ============================================================
   WEIGHT AND CASE PASS — the About half

   Companion to the block at the end of v2.css; read that one for the
   reasoning. This file only carries what is local to this page, and
   it has to be here rather than there because about.css loads second
   and would otherwise win on source order.

   This page was already most of the way there — .areas__title was
   built at weight 500 from the start. What was out of step was the
   hero above it at 900 and the eyebrows introducing the sections at
   700 caps, which is exactly the pattern the home page had.

   (.vals__title is no longer in that set: Why Trust Us is now a
   one-to-one copy of the client's live section and carries its own
   Poppins 500. .faq__title is gone with the FAQ.)

   .stats__num is left at 700. It is a numeral and 700 is the tier it
   belongs to; it did not need moving.

   THIS PAGE KEEPS DM SANS THROUGHOUT, including .ahero__title. The
   Montserrat headline is scoped to the HOME hero only — it is one
   deliberate exception on one section of one page, not a system
   change, so .ahero__title is not matched to it. If the two heroes
   should agree, that is a decision to take on purpose; it is not one
   this block should make quietly.

   SUPERSEDED — that decision has since been taken, deliberately, and
   in three steps. .areas__title and .cta__title went first, then
   .ahero__title followed; all three now carry the home page's
   Montserrat treatment, and both blocks are at the end of this file.
   Nothing on this page is still on DM Sans by intent.
   ============================================================ */

/* SUPERSEDED by the hero block at the end of this file, which sets
   weight 300. Kept as the record of what 600 was for: it was matched
   to .hero__title's weight on the home page at a time when that
   headline was 600 and set in DM Sans. The home hero has since gone
   to Montserrat 300, so re-matching it means moving this too — the
   number changed, the rule that it tracks the home hero did not. */
.ahero__title { font-weight: 600; }

/* Tracking drops from .1em because these are mixed case now; caps
   tracking on mixed case pulls the words apart.

   .vals__eyebrow and .faq__eyebrow used to be in this list and are
   gone: the FAQ was removed from the page, and Why Trust Us lost its
   eyebrow when that section was rebuilt to match the client's live
   site. .vals__name went the same way — those cards are flat images
   now and carry no live text. */
.ahero__eyebrow,
.areas__eyebrow,
.stats__label {
  font-weight: 600;
  letter-spacing: .01em;
  text-transform: none;
}

/* The mobile tracking override that used to sit here is GONE, and its
   absence is the point rather than an omission — the same retirement
   the home page's hero block records. It read "At 30-34px and weight
   600, -1.6px is too tight to read", and it was correct: a px value
   against a clamped font-size is a different proportion at each end of
   the clamp, and -1.6px at the 34px floor is nearly twice the
   proportion it is at the 68px ceiling. The hero is on an em value
   now, which is the same proportion by definition, so there is
   nothing left for this to correct. */

/* ============================================================
   TWO SECTION TITLES JOIN THE HOME PAGE'S HEADING FACE

   "29 towns across Central Jersey." and "Ready to Clear Your Space?"
   now carry the exact treatment the home page's section titles do —
   the .about__title/.services__title/.area__title group at the end
   of v2.css. Same four declarations, copied rather than shared,
   because those selectors are home-page sections that do not exist
   on this page.

   All four move together. The face on its own is not the change:
   Montserrat at the DM Sans weight of 500 and tracking of -.05em is
   a different heading from the one on the home page, and would read
   as a near-miss rather than a match.

     · FACE var(--font) — Montserrat. Named explicitly, unlike the
       home hero, which gets it by being left out of the --font-head
       list. These two say var(--font-head) in their own rules above,
       so it has to be overridden here.
     · WEIGHT 300, down from 500 and 800. The 800 on the CTA is the
       bigger jump and the one to look at first.
     · TRACKING -.04em, replacing -.05em and -1px. In em because both
       sizes are clamps, so a px value is a different proportion at
       each end.
     · LEADING 1.1 — .areas__title was already there; .cta__title
       moves off 1.08 to sit with the group.

   WEIGHT 300 was originally the one call here worth a second look,
   because the CTA panel was then dark and a light face loses more to
   a dark ground than a heavy one does. The restraint pass moved that
   panel to --surface, so the heading is now navy on light and the
   concern is gone — 300 holds comfortably. It also had precedent
   either way: .area__title on the home page is the same weight in
   white on the blue band, and was signed off there. If it ever reads
   too thin at this size, 400 is the adjustment — not a return to
   800, which would put it back out of step with every other heading.

   .areas__eyebrow is untouched, same as on the home page: a small
   caps-height label is a different job from a display heading. The
   CTA no longer has an eyebrow — the restraint pass removed it.

   Font loading needs nothing: the Montserrat link on this page
   already requests the full 100..900 axis.
   ============================================================ */
.areas__title,
.cta__title {
  font-family: var(--font);
  font-weight: 300;
  letter-spacing: -.04em;
  line-height: 1.1;
}

/* ============================================================
   THE ABOUT HERO JOINS THE HOME HERO

   The last DM Sans heading on this page. The block up in the weight
   and case pass used to say this one stays put — that the Montserrat
   headline was scoped to the home hero alone, and that matching the
   two heroes was "a decision to take on purpose". This is that
   decision, taken on purpose.

   Copied from .hero__title at the end of v2.css, not re-derived:

     face      var(--font)   Montserrat
     weight    300           down from 600
     tracking  -.04em        replacing -1.6px
     leading   1.04          already matched, untouched

   FONT-FAMILY IS NAMED EXPLICITLY, and it has to be. The home hero
   gets Montserrat by OMISSION — .hero__title is simply left out of
   the --font-head selector list in v2.css, so its own rule's
   var(--font) stands. That trick is unavailable here: .ahero__title's
   own rule says var(--font-head), so leaving it out of a list changes
   nothing and the family has to be set.

   THE SIZE IS NOT COPIED. This hero runs clamp(34px, …, 68px) against
   the home hero's clamp(30px, …, 62px), and it keeps its own. The ask
   was the face, and the two headlines are different lengths on
   different photographs — matching the type treatment is what makes
   them read as one system; matching the numbers is not.

   THE EYEBROW ALREADY AGREED and is untouched. .ahero__eyebrow was
   built on var(--font) from the start, deliberately matched to
   .hero__eyebrow, and the weight pass above put both at 600.

   WEIGHT 300 IS SAFE HERE IN A WAY IT WOULD NOT HAVE BEEN BEFORE.
   This headline used to sit straight on the photograph, where a light
   face over hi-vis and pale roller door would have been the worst
   possible combination. It sits on .ahero__card now — a known ground
   at 18.6:1 for white — so the same weight the home hero carries over
   moving footage has an easier job here than it does there.
   ============================================================ */
.ahero__title {
  font-family: var(--font);
  font-weight: 300;
  letter-spacing: -.04em;
}
