/* ============================================================
   junk-removal.css — the v2 Junk Removal page

   Loaded AFTER v2.css, and only by v2/junk-removal-page/. Everything
   shared — colour tokens, the header, the footer, the quickbar, the
   type scale — comes from v2.css; this file is only what the service
   page adds.

   DELIBERATELY GENERIC. Every component here is named for what it
   DOES, not for the service it currently describes: .split, .clears,
   .gteaser, .hsteps, .faq, .jrevs, .jcta. The dumpster page needs the
   same nine sections with different copy and different photographs,
   so when it gets built it should load this file and add nothing.
   Nothing below hard-codes the word "junk" in a selector.

   THIS IS A FIRST PASS AND IT IS MEANT TO BE. The brief was to carry
   v1's sections, copy and order over intact and give them the v2
   design language, section by section, later. So the components here
   are correct and consistent rather than elaborate — the section
   rhythm, the type scale, the button shapes and the contrast all
   match the About page, and there is no invention on top of that.

   SECTION ORDER AND BACKGROUND RHYTHM, which is the one thing worth
   holding onto when individual sections get redesigned:

     hero        photograph      <- dark, full-bleed, radial scrim
     what we clear  surface      <- .clears, the photo rail
     residential white           <- .feat, type-led, 3:1 photo strip
     commercial  dark            <- the only dark FILL above the footer
     haul        surface
     gallery     white
     steps       surface
     faq         white
     reviews     surface
     cta         white panel + photograph
     footer      dark

   No two adjacent sections share a background. That is what stops a
   ten-section page from reading as one long scroll.

   THE PAGE IS A LIGHT PAGE AND MUST STAY ONE. That is a standing
   instruction from the client, not a preference this file arrived at.

   RESIDENTIAL WENT LIGHT, AND COMMERCIAL TOOK THE DARK SLOT BACK.
   Both moved together, twice, and the round trip is worth recording
   so it is not undone by halves:

     · Residential was a white .split. It became .rband, a full-bleed
       dark band, and commercial was flipped to white to stop two dark
       sections colliding.
     · That band did not fit — three reasons, all about neighbours
       rather than the band: the light-page instruction; .clears
       directly above showing seventeen photographs; and .clears
       already showing this same truck. See the .feat block.
     · So residential is white again (type-led now, not a split), and
       commercial goes back to dark because nothing else on the page
       breaks up the run between the rail and the footer.

   THE PAIR IS LOAD-BEARING. Residential and commercial must not both
   be light and must not both be dark. If either is ever restyled,
   check the other in the same pass.

   The hero / residential junction that this note used to flag as the
   weak point is no longer one. It was two full-bleed photographs in a
   row; .clears now lands between them, and residential is neither
   full-bleed nor dark.
   ============================================================ */

:root {
  /* One knob for the page's vertical rhythm. Bottom of v2's own range
     (.about is 72, .steps and .showcase are 100) because this page
     runs nine sections where the home page runs eight and the About
     page seven — the same padding on a longer page is more scrolling
     than it is breathing room. */
  --sec-pad: clamp(60px, 6.4vw, 92px);
  --rail: 1200px;

  /* THE AFTER GREEN, AND IT IS NOT var(--accent-green).

     --accent-green #71B745 was picked to sit on ink and on filled
     capsules, where whatever is behind the text carries the
     contrast. Against --surface #F1F5F7 it measures 2.24:1, which
     fails AA for text at any size this page uses it at.

     Before & After needs the same green as BARE TEXT on the tint —
     the word "After" over a rule, with nothing behind it — so it
     needs a value that carries itself. #176928 is 6.20:1 on
     --surface and 6.80:1 on white.

     ⚠ The eyebrow on every tinted section of this page is still on
     --accent-green and still measures 2.24:1. That is a live
     accessibility bug, it predates this token, and this token is
     the value that fixes it when someone takes it on. */
  --after-ink: #176928;
}


/* ============================================================
   SHARED SECTION HEAD — eyebrow / title / copy

   Five of the nine sections open with some combination of these
   three lines, so they are one component rather than five. Values
   are lifted from .about__eyebrow / .about__title / .about__lede in
   v2.css so a heading on this page and a heading on the home page
   are the same object.
   ============================================================ */

/* NO MEASURE CAP ON THE HEAD ITSELF — the section container is the
   only thing that limits it now.

   It used to be 62ch, which sounds like a body measure and is not:
   `ch` resolves against the HEAD's inherited size, not the title's,
   so 62ch landed at 698px while the title inside it renders at 44px.
   That is about 30 characters of display type per line, so six of the
   eight heads on this page broke to two lines when their text would
   have fitted on one — "Full-Service Residential Junk Removal in
   Central NJ" split after "Residential", and "One visit. Loaded,
   swept, and gone." split after "Loaded,".

   .clears__head had already opted out to var(--rail) and is the
   reference for how the rest should look. The cap is gone rather than
   raised so that a head is bounded by the layout it sits in — the
   rail in most sections, a grid column in .split, 860px in .faq — and
   no section has to fight a global number to use its own width.

   THE MEASURE MOVED TO .shead__copy, WHERE IT BELONGS. A title is a
   few words at 44px and wants the room; a paragraph at 17px does not,
   and 1200px of it would be ~145 characters a line. */
.shead { max-width: none; }
.shead--center { margin-inline: auto; text-align: center; }

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

/* THE SAME TREATMENT AS THE HERO, which is also the same treatment as
   every other display heading in v2. This is not a one-off match: the
   home page's section titles, About's "29 towns across Central
   Jersey." and About's CTA heading all carry it, and about.css moved
   them there in three deliberate steps. The values:

     face      var(--font)   Montserrat, not DM Sans
     weight    300           down from 600
     tracking  -.04em        replacing -.6px
     leading   1.1

   TRACKING IN EM, NOT PX, and that is load-bearing rather than
   stylistic. The size is a clamp running 28px to 44px, so a px value
   is a different proportion at each end — -.6px is over twice the
   proportion at 28px that it is at 44px. An em value is the same
   proportion by definition.

   LEADING IS 1.1, NOT THE HERO'S 1.04, and that is deliberate too.
   about.css splits them the same way: 1.04 for the hero headline,
   1.1 for the section titles that share its face and weight. A
   section title wraps to two or three lines in a column of body copy
   and needs the extra air; a hero headline sits alone on a card.

   The size clamp is unchanged and happens to be identical to
   .areas__title's already. Matching the type TREATMENT is what makes
   these read as one system — matching the numbers is not the point,
   and where a heading needs its own size it should keep it. */
.shead__title {
  margin: 0;
  font-family: var(--font);
  font-weight: 300;
  font-size: clamp(28px, 3.2vw, 44px);
  line-height: 1.1;
  letter-spacing: -.04em;
  color: var(--head-ink);
  text-wrap: balance;
}

/* 60rem = 960px, about 115 characters at this size. Wider than the
   62ch the whole head used to sit at, which is what was asked for,
   and still short of the 1200px rail where a lede stops being
   readable. Centred heads get margin-inline:auto below. */
.shead__copy {
  max-width: 60rem;
  /* Insurance at narrow widths, where these ledes run four and five
     lines and a greedy wrap can leave one word alone on the last one.
     It does NOT rescue a two-word tail — the before/after lede was
     ending "the arrows." on its own line and no wrap setting fixes
     that; the sentence was cut instead. Browsers without `pretty`
     wrap exactly as they did before. */
  text-wrap: pretty;
  margin: 20px 0 0;
  font-size: clamp(16px, 1.15vw, 18px);
  font-weight: 500;
  line-height: 1.6;
  color: var(--body-ink);
}
.shead--center .shead__copy { margin-inline: auto; }

/* On the one dark section the same three lines have to flip. Kept as
   a modifier on the SECTION rather than on the head so a section only
   ever declares its colour scheme once. */
.split--ink .shead__title { color: #FFFFFF; }
.split--ink .shead__copy  { color: rgba(255, 255, 255, .82); }


/* ============================================================
   SHARED BUTTONS

   Same geometry as .about__btn in v2.css — 48px tall, 34px sides,
   fully round, 16/700. Repeated rather than reused because .about__
   btn is scoped to a component this page does not use, and a service
   page that renames its buttons to borrow another section's CSS is
   how a design system rots.

   Contrast, measured, all four combinations:
     #FFFFFF on #2F6FE5 .... 4.64   solid, on white
     #0B1226 on #FFFFFF ... 18.42   ghost, on white
     #0B1226 on #71B745 .... 8.06   solid green
     #FFFFFF on #131B2E ... 16.61   ghost, on the dark band
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;             /* clears the 44px touch minimum */
  padding: 0 34px;
  border: 1px solid transparent;
  border-radius: 100px;
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
  text-align: center;
  text-decoration: none;
  transition: background-color .2s linear, border-color .2s linear, color .2s linear;
}

.btn--solid {
  background: var(--blue);
  border-color: var(--blue);
  color: #FFFFFF;
}
.btn--solid:hover { background: var(--blue-hover); border-color: var(--blue-hover); }

.btn--ghost {
  background: transparent;
  border-color: #0B1226;
  color: #0B1226;
}
.btn--ghost:hover { background: #0B1226; color: #FFFFFF; }

/* The outlined button on a dark ground. A navy outline on navy is
   invisible, so the border and label invert and the hover fills
   solid white rather than solid navy.

   Contrast on the hero card's fill:
     #FFFFFF border/label ... 16.6:1
     #0B1226 on #FFFFFF (hover) ... 18.4:1 */
.btn--ghost-light {
  background: transparent;
  border-color: rgba(255, 255, 255, .55);
  color: #FFFFFF;
}
.btn--ghost-light:hover { background: #FFFFFF; border-color: #FFFFFF; color: #0B1226; }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 32px;
}

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


/* ============================================================
   SERVICE HERO — FULL-BLEED PHOTOGRAPH, TYPE CENTRED ON IT

   Structure taken from coconutcleaningco.com's home hero, which is
   the reference this whole v2 build came from. Measured off the live
   page at a 1920 viewport rather than eyeballed:

     band      1905 x 714  (2.67:1), background-image, cover, 50% 50%
     headline  91px / 900 / uppercase / centred / white
               line-height 72.8px — that is 0.80, tighter than 1
     lede      17px / 400 / lh 1.3 / centred / white, full rail width
     button    ONE, centred, 246 x 44, radius 50px, 18px/700,
               padding 12px 24px, solid brand blue with a 1px border
     rhythm    headline, lede, button, then ~195px of open photograph
               below the button before the band ends

   WHAT IS TAKEN AND WHAT IS NOT. The ask was the structure and the
   vibe, not the typography: full-bleed photograph as the ground,
   everything centred on it, one prominent call to action, and the
   band left tall enough that the picture still reads as a picture.

   NOT taken: Mont Heavy at 900 uppercase, the 0.8 leading, and the
   91px size. This page keeps Montserrat 300 at -.04em — the resolved
   About-page treatment every other heading in v2 already carries.
   Copying their face would have made this one page disagree with the
   other two.

   ⚠ THE ONE THING THAT CANNOT BE COPIED IS THE ABSENCE OF A SCRIM.
   Their hero has none: no overlay div, no ::before, no ::after, and
   the h1 carries text-shadow: none. White type sits straight on the
   photograph. They get away with it because their van fills the
   frame in mid-blue — and only just; the top of their headline
   crosses a pale van roof and is marginal there.

   Our photograph has sky across the top and a pale fence behind the
   subject. Measured properly — the source composited through the
   real cover/object-position maths onto a canvas, then sampled every
   2nd row and 3rd column across the boxes the type actually occupies
   (22,752 samples for the headline, 6,525 for the lede) — with no
   scrim at all:

                    mean      worst tenth
     headline      10.69:1       2.90:1
     lede           8.45:1       2.93:1

   The MEANS are fine and always were. The worst tenth is the failure,
   and it is the number that matters: it is the odds a stroke lands on
   the sky or the pale fence. 2.90:1 is under the 3:1 large-text floor,
   and the lede's 2.93:1 is a long way under the 4.5:1 body-copy one.

   RE-MEASURE THESE IF THE BAND HEIGHT CHANGES. They moved when the
   band went from 68svh to 78svh, because a taller band is a different
   crop and therefore different pixels under the type.

   So the scrim is not a stylistic choice and must not be removed to
   get closer to the reference — it is what makes their layout legible
   on our picture. Figures after it are on .shero__scrim.
   ============================================================ */

.shero {
  position: relative;
  isolation: isolate;
  /* Shows while the photograph decodes. Two-stop, never a flat fill,
     per the dark-section rule. */
  background: linear-gradient(180deg, #131B2E 0%, #0F172A 100%);
}

/* THE CROP. Source is 3:2, the band is about 2.7:1, so cover keeps
   the full width and trims top and bottom — the reference's own
   proportion, and the reason this is not the crop the split-band
   version used (that half was portrait-ish and trimmed the sides
   instead).

   50% x, because the type is centred now and the subject should not
   be. He sits right of centre in the source, which puts him clear of
   the headline's middle rather than behind it. 40% y clears his head
   and keeps the truck bed in frame along the bottom. */
.shero__photo {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 40%;
}

/* ONE SOFT ELLIPSE, AND NOTHING ELSE. This started as a radial over a
   full-width linear veil, and the veil was the mistake: it darkened
   the WHOLE frame, corners included, to 33% — which is exactly what
   reads as "the image has a tint on it". The radial alone lands the
   frame's edges at 2%, i.e. the photograph is untouched everywhere
   except directly behind the words.

   THE POINT IS WHERE THE DARKNESS IS SPENT, not how much there is.
   An even veil has to be dark enough for the worst pixel anywhere
   under the type, so it pays that cost across the entire picture. An
   ellipse centred on the type pays it only in the middle, where
   there is text to protect, and the sky and fence at the edges stay
   as bright as they were shot.

   Measured the same way as the note above — the source composited
   through the real cover maths onto a canvas, then sampled across
   the headline and lede boxes separately, since they have different
   floors:

                     no scrim          this scrim         floor
     headline    10.33 / 2.70      12.18 / 4.21            3.0
     lede        11.41 / 3.52      13.68 / 6.31            4.5
                 (mean / worst tenth)

   The headline is 60px, which is large text, so its floor is 3.0 and
   it clears by 40%. The lede is 18px body copy — it fails at 3.52
   unaided and clears at 6.31 with the scrim, which is the single
   clearest argument for keeping one at all.

   Both pass with the picture essentially clean — an earlier
   version bought 9.42 on the headline, which was about twice the
   contrast anyone needed and cost the whole photograph to get.

   THESE NUMBERS DROPPED WHEN THE TYPE GOT WIDER and that was
   accepted rather than corrected. Removing the headline's 18ch cap
   took it from three short lines to two long ones spanning 1272px,
   which pushes the ends of each line out toward the falloff of the
   ellipse — the headline's worst tenth went 5.18 -> 4.21.

   The obvious response is a wider ellipse, and it was measured:
   90% would give 4.56, 100% would give 4.83. NOT TAKEN. A wider
   ellipse darkens the whole mid-zone between the centre and the
   edges, which is the exact thing that made the previous scrim read
   as a tint on the photograph. 4.21 against a 3.0 floor does not
   need buying back, and weight 400 on the headline is already doing
   work the ratio does not capture.

   IF THE PHOTOGRAPH CHANGES, RE-MEASURE. Every candidate in
   /assets/img was ranked for this crop and not one of them clears
   4.5 at its worst tenth unaided — the best managed 4.49 — so some
   scrim is always going to be needed here. */
.shero__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(ellipse 78% 62% at 50% 50%,
                    rgba(8, 14, 28, .55)  0%,
                    rgba(8, 14, 28, .30) 55%,
                    rgba(8, 14, 28, .02) 100%);
}

/* Centred both ways. The reference sits its block slightly ABOVE
   centre and leaves ~195px of open photograph under the button; the
   extra bottom padding here reproduces that without hard-coding a
   pixel value that would only be right at one width. */
.shero__inner {
  position: relative;
  z-index: 2;
  /* 1320, not 1100. The reference's headline runs 1361px wide at a
     1920 viewport — it uses the full content rail and breaks to two
     long lines rather than three short ones. A narrow centred column
     is what makes a centred hero read as a stack of fragments. */
  max-width: 1320px;
  margin: 0 auto;
  /* 78svh, tuned against the reference's own proportion rather than
     picked: their band measures 1905 x 714, a 2.67:1 letterbox. At
     68svh this one came out 1905 x 608 (3.14:1), which is a slot
     rather than a band and left the photograph no room to be looked
     at. 78 lands it within a few pixels of theirs. */
  min-height: clamp(500px, 78svh, 720px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: clamp(64px, 8vh, 104px) 24px clamp(88px, 12vh, 150px);
  text-align: center;
}

/* Breadcrumb. v1 carries one and the reference does not; it stays
   because for a page reached out of search it is the only route back
   up to the site. Quiet enough to skip. */


/* Bigger than the split band's 46px ceiling, because a full-bleed
   centred hero is the one place on the page that can carry it — but
   nowhere near the reference's 91px, which belongs to a 900-weight
   condensed face and would look like shouting at 300.

   18ch keeps it to two lines at the ceiling instead of running the
   full 1100px rail as one long line. */
/* WEIGHT 400, NOT THE 300 EVERY OTHER HEADING ON THIS PAGE CARRIES,
   and the one step is deliberate rather than drift.

   about.css set the rule when it moved the About hero to 300: "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." This is that adjustment.

   THE REASON IS THE GROUND, WHICH IS WHY THE SECTION TITLES DO NOT
   FOLLOW. About's hero can hold 300 because it sits on a solid card;
   this page's section titles hold it because they are navy on white
   or on --surface. This headline is white on a photograph, and a
   photograph is the hard case: the scrim fixes the RATIO but not the
   STROKE. Contrast measures the ground behind a glyph; it says
   nothing about how much glyph there is to see. At 300 the strokes
   are thin enough that a busy background reads through them even at
   5.18:1, which is what "hard to read" was describing.

   400 adds stroke without changing the face, the tracking or the
   size, so the headline still reads as the same object as the section
   titles below it. 500 was tried and starts to look like a different
   typeface next to them. */
/* NO max-width. It used to carry 18ch, which at 60px is about 540px
   and forced this 53-character headline into three short lines
   inside a 1100px rail — the text was being broken by a cap rather
   than by the space available. It now runs the full rail and breaks
   where it runs out of room, which is two long lines.

   text-wrap: balance is doing the rest: it evens the two lines
   instead of leaving a long first line and an orphan. */
.shero__title {
  margin: 0;
  font-family: var(--font);
  font-weight: 400;
  font-size: clamp(30px, 4.4vw, 60px);
  line-height: 1.06;
  letter-spacing: -.04em;
  color: #FFFFFF;
  text-wrap: balance;
}

.shero__accent { color: var(--accent-green); }

/* v1's hero line. Full white rather than the .86 the panel version
   used — on a photograph the extra weight is worth having, and the
   scrim is carrying it rather than a solid fill. */
/* 600, up from 500, for the same stroke-coverage reason as the
   headline — and it matters more here, not less. This is 18px where
   the headline is 60, so there is far less glyph on the screen to
   begin with, and it is the line that sits lowest in the ellipse
   where the scrim is already falling away. */
/* 76ch, up from 56. Same reasoning as the headline — the reference
   runs its lede across the full content rail on one or two long
   lines. 56ch was breaking a 105-character sentence into three. */
.shero__lede {
  margin: 20px auto 0;
  max-width: 76ch;
  font-size: clamp(16px, 1.2vw, 18px);
  font-weight: 600;
  line-height: 1.6;
  color: #FFFFFF;
  /* Without this the long measure left "quote." alone on line two.
     Balance rather than pretty: pretty only protects the LAST line
     from an orphan, and on a two-line centred block what is wanted is
     both lines the same length. */
  text-wrap: balance;
}

/* The reference runs ONE button. This runs two because the phone is
   the second-most-used route on a trade site and v1 puts it in the
   hero copy ("Call now for a free quote") — but they are ranked, not
   equal: solid blue primary, outlined secondary. */
.shero__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
  margin-top: 30px;
}

@media (max-width: 860px) {
  .shero__inner {
    min-height: clamp(440px, 62svh, 560px);
    padding: 52px 20px 68px;
  }
  .shero__title { max-width: none; }
  /* The ellipse WIDENS on a phone rather than darkening. At this size
     the type block is nearly as wide as the frame, so a 78% ellipse
     leaves the ends of the headline out on the bright edges — but the
     answer is more coverage, not more alpha. Peak stays at .55; only
     the radii and the falloff change, so the edges still land light
     and the photograph still reads as a photograph. */
  .shero__scrim {
    background:
      radial-gradient(ellipse 104% 76% at 50% 50%,
                      rgba(8, 14, 28, .55)  0%,
                      rgba(8, 14, 28, .36) 60%,
                      rgba(8, 14, 28, .10) 100%);
  }
}

@media (max-width: 560px) {
  .shero__inner { padding: 44px 18px 56px; }
  .shero__lede { max-width: none; }
  /* Full width and stacked. Two pills side by side in a 339px column
     leaves each about 163px, under the comfortable tap width for a
     label this long. */
  .shero__actions { gap: 10px; width: 100%; }
  .shero__actions .btn { flex: 1 1 100%; }
}


/* ============================================================
   SPLIT — photograph beside copy

   The Residential and Commercial sections are one component used
   twice. Two modifiers:
     .split--rev   photograph on the left instead of the right
     .split--ink   dark band instead of white

   The proportion is .about__inner's, inverted: there the fixed track
   is the 450px portrait and the copy takes the rest, here the photo
   is the flexible half because these are landscape frames that want
   the width.
   ============================================================ */

/* ============================================================
   RESIDENTIAL — TYPE LEADS, THE PHOTOGRAPH FOLLOWS (.feat)

   Named .feat rather than .rband, per this file's rule that
   components are named for what they DO — a type-led feature section
   with a supporting strip — so the dumpster page can use it as-is.
   The old name was both service-specific and, after this pass,
   inaccurate: there is no band any more.

   WHY IT IS LIGHT. This replaced a full-bleed dark band that did not
   fit, and none of the reasons were about the band's own execution:
   the stylesheet carries a standing client instruction that the page
   stays light; .clears sits directly above with seventeen
   photographs; and .clears already shows this same truck. A dark
   eighteenth photograph after all that is not a climax, it is
   fatigue. This section's job is to be the rest between the rail and
   commercial.

   CONTAINED TO THE RAIL, deliberately. .clears is full-bleed and runs
   off the right edge; holding this one to 1200px is what makes the
   page feel like it settles rather than continuing to sprawl.

   WHITE, AND THE CHOICE IS FORCED. --surface is above (clears) and
   white is below (commercial), so this shares a ground with one
   neighbour whichever it picks. White breaks at the junction that
   matters — straight after the busy rail — and commercial is
   separated by being a photograph-and-copy split against this one's
   stacked type, which is the same trick the hero junction uses.
   ============================================================ */

.feat {
  background: #FFFFFF;
  padding: var(--sec-pad) 20px;
}

.feat__inner {
  max-width: var(--rail);
  margin: 0 auto;
}

/* Uncapped. This is a STACKED section — copy above, full-bleed image
   strip below — not a two-column one, so the copy block was never
   competing with anything for width; 46rem was holding the heading to
   roughly half the rail for no layout reason. The paragraph inside it
   still stops at .shead__copy's 60rem. */
.feat__copy { max-width: none; }

/* 3:1, AND THAT RATIO IS THE BOARD'S DOING RATHER THAN A TASTE CALL.
   The sign runs from 10.5% to 53% down the frame. This started at
   21:6, which shows only 42% of the frame's height — not enough to
   hold the board at any offset, so "NJ" was cut off the top wherever
   the window sat. 3:1 shows 50%, and 14% places that window at
   7-57%: the whole board, with a little air either side.

   object-position is the second half of the same calculation. A
   centred crop here would show 25-75% of the frame, which is the
   truck bed and none of the sign. */
.feat__strip {
  display: block;
  width: 100%;
  margin-top: clamp(36px, 4vw, 56px);
  aspect-ratio: 3 / 1;
  object-fit: cover;
  object-position: 50% 14%;
  border-radius: 18px;
  background: #0B1226;   /* holds the box while the file decodes */
}

/* A 3:1 slot on a phone is a letterbox slit — about 110px tall at
   360 wide, which is too little to read anything in. The strip goes
   back to a normal landscape crop there, and the object-position
   holds so the board stays whole. */
@media (max-width: 720px) {
  .feat__strip { aspect-ratio: 16 / 10; }
}

.split {
  position: relative;
  background: #FFFFFF;
  padding: var(--sec-pad) 20px;
}

/* THE DARK SECTION RULE, APPLIED. A flat dark fill is what the
   display pipeline spatial-dithers into a visible diagonal hairline
   across the full width of the band. A two-stop gradient plus a low
   noise layer gives the dither nothing to latch onto. The stops are
   only five luminance steps apart — it does not need to read as a
   gradient, it needs to not be flat. */
.split--ink {
  background: linear-gradient(180deg, #131B2E 0%, #0F172A 100%);
}

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

.split__inner {
  position: relative;
  z-index: 1;
  max-width: var(--rail);
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(32px, 4vw, 56px);
  align-items: center;
}

/* Source order is copy-then-photo in both instances, so the reversed
   one reorders visually and leaves the DOM alone. A screen reader
   and a keyboard both meet the heading before the picture either
   way, which is the order that makes sense read aloud. */
.split--rev .split__media { order: -1; }

.split__media { margin: 0; }

/* aspect-ratio holds the frame before the file decodes so the copy
   beside it never jumps. 4:3 rather than the sources' own ratios
   because the two photographs are 1280x854 and 958x940 — left alone
   they would set two different heights for the same component. */
.split__photo {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  border-radius: 20px;
  background: #0B1226;
  object-fit: cover;
  box-shadow: 0 20px 44px rgba(11, 18, 38, .16);
}
.split--ink .split__photo { box-shadow: 0 20px 44px rgba(0, 0, 0, .38); }

.split__copy { min-width: 0; }
/* The copy column is not a reading-measure problem here — it is half
   of a 1200px rail, so ~55ch at the widest. No cap needed. */
.split__copy .shead { max-width: none; }



/* ============================================================
   BEFORE & AFTER — A BAND WIDER THAN THE RAIL (.bax)

   Direction C out of v2/_lab/before-after-refine.html. Three jobs on
   a horizontal snap track; each job is its before and its after side
   by side, each labelled UNDER the frame, on a band that is wider
   than every other section on this page.

   ------------------------------------------------------------
   WHY IT LEAVES THE RAIL, WHICH IS THE ONE THING NOT TO UNDO

   Two 16:9 halves inside var(--rail) are capped at 592x333. That is
   not a styling result, it is arithmetic — (1200 - 16) / 2 = 592,
   and 592 x 9/16 = 333 — so no radius, shadow, gap or padding change
   can move it. The section stood 685px tall and gave 333 of that to
   the photographs: the heading, the padding and the dot row were
   taking more of the section than the work it exists to show.

   The band is min(1560px, 100% - 48px) instead. Each half lands at
   771x434, which is +70% in area, and the section spends its height
   on photographs rather than on chrome.

   ⚠ 1560 IS EXPRESSED AS A max-width ON .bax__band, NOT AS 100vw
   ANYWHERE. On Windows 100vw includes the scrollbar gutter, so
   min(1560px, 100vw - 48px) overflows by the scrollbar width and the
   whole page picks up a horizontal scroll. Percentages resolve
   against the padding box, which already excludes it.

   ⚠ THIS IS THE ONLY SECTION ON THE PAGE THAT LEAVES THE RAIL, and
   it only reads as emphasis while that stays true. A second breakout
   makes both look like drift instead. New sections go in var(--rail).

   ------------------------------------------------------------
   NOTHING IS DRAWN ON A PHOTOGRAPH

   The previous version put six things on two images: a badge pill on
   each half, a caption pill across the seam, and two navy disc
   arrows floating over the frames. The caption was the clearest
   failure — 639px wide against a 592px half, so it was wider than
   one image and physically could not sit on only one of them.

   Every one of those moved out. Labels sit under their frame, the
   caption sits under the band, the arrows sit up in the head row.
   There is no z-index anywhere in this section any more and no
   backdrop-filter; if either comes back, something has been put back
   on top of a photograph.

   ------------------------------------------------------------
   THE LABEL IS A COLUMN HEADING, SO IT IS A WORD ON A RULE

   Before and after are properties of the COLUMNS — the left frame is
   always the before — so the label does not need to be a badge
   pinned to an image. It is the word, small and letterspaced, over a
   2px rule the width of the frame. The rule carries the colour the
   filled capsule used to.

   ⚠ THE AFTER GREEN IS #176928 AND IT IS NOT var(--accent-green).
   As white text on a filled green capsule, the capsule carried the
   contrast and --accent-green #71B745 was fine. As bare text on
   --surface it has to carry itself, and #71B745 on #F1F5F7 is
   2.24:1 — a clear AA failure. #176928 is 6.20:1 on the same tint.
   Do not "consolidate" this back onto the token.

   ------------------------------------------------------------
   THE HEAD IS LEFT-ALIGNED AND THE CONTROL SITS OPPOSITE IT

   The brief was that the section looked machine-made. The specific
   cause was the stack: centred eyebrow, centred 300-weight display
   heading, centred lede, symmetric grid, centred dot row — three
   sections of this page in a row doing exactly that. It is the shape
   every layout generator emits. One flex row with the head at the
   left and the arrows at the right breaks it, and costs nothing.

   The lede paragraph went with the change and that was approved; see
   the block comment on the section in junk-removal-page/index.html
   for what it said and why it is not missed.
   ============================================================ */

/* Its own padding rather than --sec-pad: the section is one image
   band and one control, and the standard section padding was adding
   ~80px of air to a block that is trying to fit inside a fold.

   THE TINT IS LOAD-BEARING. The page alternates white and --surface
   down its whole length, and three of the six photographs are bright
   interiors — the emptied room is near-white wall to wall — whose
   edges dissolve into a white page with only a radius holding them.

   THE 24px SIDE PADDING IS THE OTHER HALF OF THE BAND WIDTH. The
   band is max-width 1560 inside this, so the effective width is
   min(1560px, 100% - 48px). Change this padding and the band's
   breathing room at mid widths changes with it. */
.bax {
  background: var(--surface);
  padding: clamp(34px, 3.4vw, 46px) 24px;
}

/* THE BAND. Wider than var(--rail) on purpose — see the header note.
   It is also the carousel root: v2.js looks for the track, the
   arrows and the optional count underneath [data-carousel], and all
   of them are inside this. */
.bax__band {
  max-width: 1560px;
  margin-inline: auto;
}

/* Kept as a utility even though the fieldset that used it is long
   gone. Nothing else on this page hides text from sight but not from
   assistive tech; the next thing that needs to will want this rather
   than a second copy of it. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- head row: title left, control right ---- */

/* align-items: end so the arrows sit on the title's baseline block
   rather than floating against the eyebrow. */
.bax__top {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(20px, 2.4vw, 40px);
  margin-bottom: clamp(20px, 2.2vw, 30px);
}

/* ⚠ NO max-width HERE, AND THAT IS A FIX RATHER THAN AN OMISSION.
   This element used .shead--left, which carries max-width: 62ch. ch
   resolves against the element's OWN font-size, and this is a plain
   div inheriting 16px — so 62ch was about 550px, and it was squeezing
   a 44px display heading into half the space it needed. The title
   wrapped to two lines and looked like a copy problem when it was a
   units problem.

   The title is a single line of display type; it does not want a
   measure. If something here ever needs one, put it on that element,
   not on this container, and put it in rem. */
.bax__head {
  min-width: 0;
}

/* Not .shead__eyebrow, because that one is --accent-green and this
   sits on the tint. Same size and tracking, compliant colour. */
.bax__eyebrow {
  margin: 0 0 12px;
  font-size: clamp(15px, 1.2vw, 18px);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--after-ink);
}

/* Same treatment as every other display heading in v2 — Montserrat
   at 300, tracking in em, leading 1.1 — see the long note on
   .shead__title. It is a separate rule only so it can opt out of the
   centring and the measure.

   ⚠ text-wrap: balance IS DELIBERATELY ABSENT. balance splits a line
   that fits into two more even ones, which is the exact thing this
   heading was changed to stop doing. It holds one line down to about
   760px on its own; below that .bax__top stacks and hands it the
   full band, and on a phone it wraps because 35 characters of 28px
   type do not fit in 342px. That is arithmetic, not a regression. */
.bax__title {
  margin: 0;
  font-family: var(--font);
  font-weight: 300;
  font-size: clamp(28px, 3.2vw, 44px);
  line-height: 1.1;
  letter-spacing: -.04em;
  color: var(--head-ink);
}

/* ---- the control ---- */

.bax__nav {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
}

/* v2.js writes "1 <span>/ 3</span>" in here. tabular-nums so the
   readout does not change width as it counts and nudge the arrows
   sideways on every step. */
.bax__count {
  font-size: clamp(12px, 1vw, 13px);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--head-ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  margin-right: 4px;
}
.bax__count span { color: var(--body-ink); }

/* OUTLINED ON THE TINT, NOT FILLED ON A PHOTOGRAPH. The old pair
   were 60px navy discs with a blur, sitting on the images at the
   outer edges — the single most template-shaped thing in the
   section. Off the photographs they no longer need to fight a
   background, so they can be what they are: a 44px ring in the head
   row. 44 is the touch-target floor and is not decorative. */
.bax__arrow {
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1.5px solid rgba(11, 18, 38, .20);
  border-radius: 50%;
  background: transparent;
  color: var(--head-ink);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background-color .2s linear, border-color .2s linear, color .2s linear, opacity .2s linear;
}
.bax__arrow:hover {
  background: var(--head-ink);
  border-color: var(--head-ink);
  color: #FFFFFF;
}
/* --blue, not the white the old ones used. That white ring existed
   because the arrows were drawn on photographs and the page's blue
   disappeared against half of them. They are on the tint now, where
   blue is the page's ring and white would be invisible. */
.bax__arrow:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
.bax__arrow[disabled] { opacity: .3; pointer-events: none; }
.bax__arrow svg {
  width: 20px; height: 20px; fill: none; stroke: currentColor;
  stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round;
}

/* ---- the track ---- */

/* No radius, no shadow, no background — it is the scroll clip and
   nothing else. The radius lives on the images, which is what makes
   them read as two photographs rather than as one panel. */
.bax__stage {
  position: relative;
  overflow: hidden;
}

/* Same snap-track shape as .clears__track, so v2.js's step()
   measures it the same way: one slide per view, no gap, snap to
   start. Horizontal padding would move the snap edge and break that
   measurement, so there is none. */
.bax__track {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.bax__track::-webkit-scrollbar { display: none; }
/* Inset, because the stage clips overflow and a ring drawn outside
   the track's box would be cut off at the section edge. */
.bax__track:focus-visible { outline: 3px solid var(--blue); outline-offset: -3px; }

.bax__slide { scroll-snap-align: start; }

/* ---- the two halves ---- */

/* A REAL GUTTER, AND THE 4px HAIRLINE THAT WAS HERE FIRST IS WHY.
   Both frames in a pair are the same kind of space shot at the same
   camera height — that is deliberate, it is how the pairs were
   chosen — so with a hairline between them the garage before and the
   garage after read as ONE WIDE PHOTOGRAPH OF A GARAGE. The first
   person to see it said exactly that: "it's just one image". Two
   separately rounded frames with tint between them cannot be misread
   that way. Do not close this gap. */
.bax__pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(12px, 1.2vw, 18px);
}

/* No aspect-ratio and no overflow here: the ratio and the radius are
   on the image itself, which leaves the figure free to be a plain
   two-row box holding the frame and its label. Putting overflow
   hidden back here would clip the label off, since the label is a
   child of this figure and sits outside the frame. */
.bax__half { margin: 0; }

/* 16:9 because all six files are already cut to it — object-fit has
   nothing to crop, which is the only reason these can be told to
   cover without losing the anchor each pair is built on. */
.bax__shot {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 14px;
  background: var(--head-ink);
}

/* ---- the label, under its frame ---- */

.bax__label {
  margin: 11px 0 0;
  padding-top: 9px;
  border-top: 2px solid currentColor;
  font-family: var(--font);
  font-size: clamp(11px, 1vw, 13px);
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  line-height: 1;
}
.bax__label--before { color: var(--head-ink); }
/* 6.20:1 on --surface. See the header note before changing it. */
.bax__label--after  { color: var(--after-ink); }

/* ---- the caption, under the band ---- */

/* Left-aligned and in the flow. It used to be a centred pill on the
   seam, which is where it did the most damage. It travels inside the
   slide so it stays with the job it describes and needs no script to
   follow the track. */
.bax__cap {
  margin: clamp(14px, 1.5vw, 20px) 0 0;
  max-width: 74ch;
  font-size: clamp(14px, 1.15vw, 16px);
  line-height: 1.5;
  font-weight: 500;
  color: var(--body-ink);
}
.bax__cap b { color: var(--head-ink); font-weight: 700; }

/* ---- narrow ---- */

/* STACKED AT 820, AND THE NUMBER WAS ARGUED DOWN FROM 900.

   SIMULTANEITY IS THIS SECTION'S POINT, so the stack threshold buys
   frame size at the cost of the one thing the arrangement exists to
   provide, and it should be as low as the frames can stand.

   At 900 side by side the band is ~852 and each half is 417x235 —
   which is almost exactly what SHIPS TODAY at that width (422x237),
   and the current section keeps both frames side by side down to
   700. So stacking at 900 would not have been fixing the small-image
   complaint at all; it would have been trading away both states at a
   width where the images are no worse than they already are, and
   tripling the section height to do it (~500px to ~1300px).

   820 is where a half drops to about 379 wide. Below that the
   comparison really is two thumbnails and the stack earns itself.
   Above it, both states stay on screen.

   The head row stacks with it so the title gets the whole band back
   and holds one line for another ~140px of width. */
@media (max-width: 820px) {
  .bax__top {
    flex-direction: column;
    align-items: stretch;
    gap: 18px;
  }
  .bax__nav { justify-content: flex-end; }
  .bax__pair {
    grid-template-columns: 1fr;
    gap: 16px;
  }
}

@media (max-width: 700px) {
  .bax { padding-inline: 16px; }
  .bax__cap { font-size: 14px; }
}

/* There is no state change left in this section to preserve — the
   crossfade went with the pill toggle, and nothing is layered over a
   photograph any more. This is the track and the arrows' colour. */
@media (prefers-reduced-motion: reduce) {
  .bax__arrow { transition: none; }
  .bax__track { scroll-behavior: auto; }
}

/* ============================================================
   GALLERY TEASER

   v1 runs this as a centred heading, a paragraph and a button, with
   no photographs in it at all — on a section whose entire job is to
   say "look at the difference". Four tiles were added for that
   reason and no other; the copy and the button are v1's, unchanged.
   ============================================================ */

.gteaser {
  background: #FFFFFF;
  padding: var(--sec-pad) 20px;
  /* Moved up from .gteaser__inner. The inner now wraps only the
     section head — the pile and the button are its siblings — so the
     centring has to live on the section or the button goes left. */
  text-align: center;
}

.gteaser__inner {
  max-width: var(--rail);
  margin: 0 auto;
}

/* ---- OVERLAPPING PRINTS (.gteaser__field) ----

   Replaces a four-up equal grid. Picked from _lab/gteaser-directions
   .html, variant E, against five others.

   WHY THE GRID WENT. Four boxes the same size give the eye no entry
   point, so the row read as four slots that had to be filled rather
   than as work worth showing — and it was the THIRD centred,
   symmetrical block in a row after Before & After. White borders,
   small rotations and real overlap read as photographs on a table
   instead of as a CMS grid, which is a better fit for a family
   trades business than a portfolio grid is.

   ⚠ THE ROTATIONS ARE NOT RANDOM AND MUST NOT BE EVENED OUT. Three
   lean one way and one the other; making them symmetrical or equal
   turns a pile back into a pattern, which is the thing this is here
   to avoid.

   PHOTO ORDER IS COMPOSITION-DRIVEN — see the note on the markup.
   Print 1 is the bottom of the pile and loses its right-hand third,
   so it takes the one photograph with no single subject to bury.

   ⚠ TRANSFORM, NOT top/left, ON HOVER. The straighten-and-lift is a
   rotate/scale on the compositor; animating the offsets instead
   would relayout four absolutely-positioned boxes every frame. */
/* NEARLY THE FULL RAIL, AND NOT 820. It was capped at 820 to match
   .bax__stage, which put 190px of empty white down each side of a
   1200 rail — measured, and it was the entire complaint. The pile
   itself was already tight inside its box: the union of the four
   prints came to 835x472 against an 820x461 field, so there was no
   slack to reclaim internally. The only white worth removing was
   outside it.

   1140 leaves 30px either side and makes every print ~39% wider.

   ⚠ THE RATIO WENT 16:9 -> 2.35:1 IN THE SAME MOVE, and it has to.
   Widening at a fixed 16:9 would have taken the field from 461 to
   641 tall and added ~180px to a section that is already the second
   tallest on the page. Flattening it takes the height back to ~485
   while the photographs still get the full 39%, so this trade is
   bigger pictures for the SAME footprint rather than bigger pictures
   for more scrolling.

   The floor is about 2.5:1 — past that the rotated bounding box of
   print 1 runs out the bottom of the field. 2.35 keeps a little
   room. */
.gteaser__field {
  position: relative;
  /* 1560, the same break-out .hsteps__inner already uses, so the page
     has two widths and not three. At 1440 this resolves to 1400 — the
     viewport less the section's own 20px padding — which is the "use
     more of the screen" ask taken literally. */
  width: min(100%, 1560px);
  margin: clamp(30px, 3.2vw, 44px) auto 0;
  /* 2.5:1 is the floor, not a taste call: past it print 1's ROTATED
     bounding box runs out the bottom of the field. Worked from
     0.05H + (0.3525W·cos3.2 + 0.47W·sin3.2) <= H, which gives
     H >= 0.398W. Change a width or a top offset above and this
     number has to be re-derived. */
  aspect-ratio: 2.5 / 1;
}

.gteaser__print {
  position: absolute;
  display: block;
  border-radius: 12px;
  /* The border IS the print. It is white on a white section, so the
     shadow is what separates it from the page — do not drop one
     without the other. */
  border: 6px solid #FFFFFF;
  box-shadow: 0 16px 34px rgba(11, 18, 38, .24);
  background: var(--surface);
  cursor: pointer;
  /* TRANSFORM ONLY. See the hover note below — nothing else here may
     be transitioned. */
  transition: transform .2s cubic-bezier(.2, .7, .3, 1);
}
/* The image carries its own radius now instead of the print clipping
   it. overflow:hidden had to go so ::after can paint a shadow OUTSIDE
   the box; 6px is the 12px outer radius less the 6px border. */
.gteaser__print img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 6px;
}

/* THE DEEPER SHADOW IS A SEPARATE LAYER THAT FADES, NOT A box-shadow
   THAT ANIMATES, and that is the actual fix for the jank.

   Transitioning box-shadow makes the browser re-rasterise a 34px
   blur across a ~660x490 box on EVERY FRAME, four of them if the
   pointer crosses several prints. Transitioning opacity on a layer
   whose shadow was rasterised once is a compositor job and costs
   nothing per frame. Same picture, and the frames actually land. */
.gteaser__print::after {
  content: "";
  position: absolute;
  inset: -6px;                       /* out to the border box */
  border-radius: 12px;
  box-shadow: 0 26px 54px rgba(11, 18, 38, .34);
  opacity: 0;
  transition: opacity .2s ease;
  pointer-events: none;
}

/* RE-TUNED FOR THE WIDER, FLATTER FIELD RATHER THAN INHERITED. Two
   things changed and both had to:

   The TOP OFFSETS are percentages of a field that is now 485 tall
   instead of 641, so the old 6%/12% would have collapsed the stagger
   to nothing.

   The WIDTHS went up four points each. Screenshotting the field and
   counting near-white pixels put the old 43/41/39/37 at 19.2% white
   inside its own box — the corners the pile does not reach. At
   47/45/43/41 that is 14.5%, and checking the render at each step,
   this is the last size where no crew member's head goes behind
   another print. Going further starts costing subjects, which is a
   worse trade than a white corner. */
.gteaser__print:nth-child(1) { --rot: -3.2deg; left: 0;   top: 5%;   width: 47%; aspect-ratio: 4/3; transform: rotate(var(--rot)); z-index: 1; }
.gteaser__print:nth-child(2) { --rot:  1.6deg; left: 28%; top: 0;    width: 45%; aspect-ratio: 4/3; transform: rotate(var(--rot)); z-index: 3; }
.gteaser__print:nth-child(3) { --rot:  3.4deg; right: 0;  top: 11%;  width: 43%; aspect-ratio: 4/3; transform: rotate(var(--rot)); z-index: 2; }
.gteaser__print:nth-child(4) { --rot:  2.2deg; left: 16%; bottom: 0; width: 41%; aspect-ratio: 4/3; transform: rotate(var(--rot)); z-index: 4; }

/* THE PRINT NO LONGER STRAIGHTENS ON HOVER, AND THAT IS THE POINT.

   It used to go rotate(-3.2deg) -> rotate(0deg) scale(1.03). On a
   660px-wide print a 3.2 degree rotation swings the far corner about
   37px, so the whole card visibly slewed sideways under a pointer
   that had not moved — and the z-index snapped to the front on frame
   one while that slew took 350ms, so the layer change and the motion
   disagreed with each other. That mismatch is what read as janky. It
   was never a hit-testing problem: sweeping the pointer across the
   pile produced exactly one enter and one leave per print, with no
   flicker at any edge.

   What replaces it is a lift straight up. --rot carries each print's
   own angle through, so the tilt is preserved rather than animated,
   and the only things moving are 10px of Y and 2% of scale — small
   enough that 200ms lands before you finish noticing, which is what
   makes the instant z-index change read as intentional.

   ⚠ KEEP rotate(var(--rot)) FIRST IN THE HOVER TRANSFORM. Drop it
   and the print snaps upright again, which is the bug this replaced. */
.gteaser__print:hover,
.gteaser__print:focus-visible {
  transform: rotate(var(--rot)) translateY(-10px) scale(1.02);
  z-index: 6;
}
.gteaser__print:hover::after,
.gteaser__print:focus-visible::after { opacity: 1; }

.gteaser__print:focus-visible { outline: 3px solid var(--blue); outline-offset: 4px; }

.gteaser__cta { margin-top: clamp(30px, 3.4vw, 44px); }

/* THE OVERLAP NEEDS ROOM. Below this the prints are small enough that
   the covered thirds start eating the subject rather than just the
   background, so it gives up and becomes a plain 2-up. That is the
   honest failure mode; a pile of four postage stamps is not.

   ⚠ :nth-child(n), NOT A BARE .gteaser__print. A media query adds no
   specificity, so .gteaser__print (0,1,0) loses to the four
   .gteaser__print:nth-child(N) rules (0,2,0) above and every print
   keeps its 46%/44%/42%/40% width inside its grid cell — four
   thumbnails floating in a 2x2 with the rest of the cell empty.
   :nth-child(n) matches all four and ties the specificity, so source
   order decides and this wins.

   inset:auto clears their left/top/right/bottom, and aspect-ratio has
   to stay because absolute positioning was what gave each print its
   height — without it the img's height:100% has nothing to resolve
   against. */
@media (max-width: 820px) {
  .gteaser__field {
    position: static;
    width: 100%;
    aspect-ratio: auto;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
  }
  .gteaser__print:nth-child(n) {
    --rot: 0deg;
    position: static;
    inset: auto;
    width: 100%;
    aspect-ratio: 4 / 3;
    transform: none;
    border-width: 4px;
    box-shadow: 0 10px 24px rgba(11, 18, 38, .18);
  }
  /* --rot is 0 here, so the lift would still work; it is dropped
     anyway because there is no pile to lift out of and no hover on a
     touch screen to trigger it. */
  .gteaser__print:hover,
  .gteaser__print:focus-visible { transform: none; }
  .gteaser__print::after { display: none; }
}

/* The lift is the whole interaction, so this removes the MOTION and
   keeps the state: a hovered print still comes to the top of the pile
   and still gets its heavier shadow, it just does not animate there.
   The resting rotations stay — they are the layout, not an effect. */
@media (prefers-reduced-motion: reduce) {
  .gteaser__print,
  .gteaser__print::after { transition: none; }
  /* The tilt stays — it is the layout. Only the lift goes, and the
     deeper shadow still switches on, so a hovered print is still
     marked out, just without travelling to get there. */
  .gteaser__print:hover,
  .gteaser__print:focus-visible { transform: rotate(var(--rot)); }
}


/* ============================================================
   HSTEPS — how it works, four numbered cards

   NOT the home page's .steps component, and the reason is the count.
   That one is three circular photographs on a three-column grid and
   there are exactly three step photos in /assets/img. v1's junk page
   runs FOUR steps, the fourth of which ("Clean Up") is the one that
   closes the sale, so dropping it to reuse a component would be
   letting the CSS edit the copy.

   Numbers instead of photographs, from a counter rather than from
   markup — the sequence is presentational, and typing "1 2 3 4" into
   the HTML is how a reordered list ends up numbered 1 2 4 3.
   ============================================================ */

.hsteps {
  background: var(--surface);
  padding: var(--sec-pad) 20px;
}

/* WIDER THAN THE PAGE RAIL, AND ONLY THIS SECTION IS.

   Every other section on the page sits in var(--rail), 1200px. The
   four steps were leaving ~352px of empty gutter either side on a
   1905px screen, and four centred columns in the middle of that read
   as a narrow strip rather than as a row.

   1560 is "15% more on each side" taken literally: +180px per side
   against the 1200 rail, +360 overall. The 20% version is 1680 — one
   number to change if this is still not enough. Below about 1600 the
   value stops mattering, since the section is width-capped by the
   viewport and the 20px section padding instead.

   ⚠ IT NO LONGER LINES UP WITH ITS NEIGHBOURS. "Four Simple Steps."
   now starts 180px left of "Frequently Asked Questions" and of every
   other section title on the page, because those are all still on the
   1200 rail. That is the cost of the wider row and it was asked for
   knowingly. Reverting is this one declaration back to var(--rail);
   widening the rest of the page instead is the other direction, and
   that one is a --rail change at the top of this file. */
.hsteps__inner {
  max-width: 1560px;
  margin: 0 auto;
}

/* ------------------------------------------------------------
   THE CARDS ARE GONE, AND THE LAYOUT IS NOW THE LIVE SITE'S.

   Measured off njindooroutdoor.com/junk-removal-page rather than
   eyeballed, because the section renders white-on-white with
   animations disabled and a screenshot of it is a blank rectangle.
   getComputedStyle on the real nodes gives:

     band        #1F2A2E, full width
     title       40px / 700 / #FFFFFF / centred
     columns     four, 284px each, 48px column gap
     icon        161x150, NATIVE SIZE, square, no radius, no box
     name        24px / 700 / #F7F7F7 / centred
     copy        16px / 24px line-height / #F7F7F7 / centred

   So: no card, no border, no rounded box, no tinted fill. The icon
   sits directly on the background at roughly ten times the area it
   had at 56px, and every line is centred under it.

   ONE DELIBERATE DEPARTURE — THE BAND STAYS LIGHT. The live one is
   #1F2A2E and this is var(--surface), because of the standing
   "THE PAGE IS A LIGHT PAGE AND MUST STAY ONE" instruction recorded
   at the top of this file, and because .steps sits between two light
   sections in the rhythm table there. Flipping it is this one
   background plus the two type colours below; the layout does not
   care either way.
   ------------------------------------------------------------ */
.hsteps__grid {
  list-style: none;
  margin: 44px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* 48px was the live value against its 284px columns. The columns
     are ~348px here, so the gap grows with them — holding 48 across a
     wider row is what would have made the extra width read as four
     columns drifting apart rather than as a wider row. */
  gap: 44px 56px;
  counter-reset: hstep;
}

.hsteps__item {
  counter-increment: hstep;
  /* Everything centred under the icon, which is what makes a column
     read as a step rather than as a card that lost its border. */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

/* NATIVE SIZE AT THE TOP OF THE CLAMP, deliberately. 150px is what
   the live site renders these at, and going bigger than the source
   would only magnify what is already a resolution problem.

   aspect-ratio rather than a matching height: the envelope is
   172x149 where the other three are ~161x151, so a square box would
   letterbox one of the four differently from the rest. contain plus
   the source ratio keeps all four optically the same size.

   ⚠ THESE ARE 161px SOURCES SHOWN AT UP TO 150px. On a 2x screen
   that asks for 300px of pixels and gets 161, so they will look soft
   — noticeably softer than at the 56px they were before. The live
   site has exactly the same problem at exactly the same size; this
   is copied, not introduced. The fix is four inline SVGs of the same
   four symbols, which would also drop ~5KB of raster. */
.hsteps__ico {
  display: block;
  /* 161 is the source's own pixel width and therefore the ceiling.
     Going past it would only scale a 161px raster up, which buys
     apparent size by spending sharpness the section cannot spare. If
     these ever become SVG the cap comes off. */
  width: clamp(104px, 12vw, 161px);
  height: auto;
  aspect-ratio: 161 / 150;
  margin-bottom: 26px;
  object-fit: contain;
}

.hsteps__name {
  margin: 0 0 10px;
  font-family: var(--font);
  font-weight: 700;
  font-size: clamp(20px, 1.9vw, 24px);
  line-height: 1.25;
  letter-spacing: -.2px;
  color: var(--head-ink);
}

.hsteps__copy {
  margin: 0;
  /* Was the live site's 284px measure. Up to 320 with the wider row,
     so the extra track width goes into the text as well as into the
     gaps — otherwise "Our team arrives on time…" still wraps to three
     lines in a 348px column and the row looks padded rather than
     wider. Not raised further: past ~340 the longest of the four
     drops to two lines while the others stay at three, and the four
     columns stop bottoming out together. */
  max-width: 320px;
  font-size: 16px;
  line-height: 1.5;
  color: var(--body-ink);
}


/* ============================================================
   FAQ — native disclosure widgets

   <details>/<summary>, not buttons with aria-expanded and a script.
   The element ships the open/close state, the keyboard handling and
   the screen-reader announcement already, and v1's version needed
   twelve lines of markup and a JS handler to reimplement all three
   slightly worse. Nothing on this page loads a script for the FAQ.

   The answers are also in the page's FAQPage JSON-LD, which is only
   legitimate because the questions genuinely render here — the home
   page deliberately dropped that block for exactly that reason.
   ============================================================ */

.faq {
  background: #FFFFFF;
  padding: var(--sec-pad) 20px;
}

.faq__inner {
  max-width: 860px;
  margin: 0 auto;
}

.faq__list { margin-top: 40px; }

.faq__item {
  border-bottom: 1px solid #E4E7EE;
}
.faq__item:first-child { border-top: 1px solid #E4E7EE; }

.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 22px 4px;
  cursor: pointer;
  list-style: none;                  /* kills the default triangle */
  font-family: var(--font);
  font-weight: 600;
  font-size: clamp(16px, 1.4vw, 19px);
  line-height: 1.4;
  color: var(--head-ink);
  transition: color .2s linear;
}
/* Safari needs this one separately; it does not honour list-style
   on a summary. */
.faq__q::-webkit-details-marker { display: none; }
.faq__q:hover { color: var(--blue); }
.faq__q:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }

/* Plus that becomes a minus. Two bars, the vertical one rotated to
   nothing when the item opens — transform only, so it animates on
   the compositor. */
.faq__icon {
  position: relative;
  flex: none;
  width: 22px;
  height: 22px;
}
.faq__icon::before,
.faq__icon::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 14px;
  height: 2px;
  border-radius: 1px;
  background: var(--blue);
  transform: translate(-50%, -50%);
  transition: transform .22s ease, opacity .22s ease;
}
.faq__icon::after { transform: translate(-50%, -50%) rotate(90deg); }
.faq__item[open] .faq__icon::after { transform: translate(-50%, -50%) rotate(0deg); opacity: 0; }

.faq__a {
  margin: 0;
  padding: 0 4px 26px;
  max-width: 68ch;
  font-size: 16px;
  line-height: 1.7;
  color: var(--body-ink);
}

@media (prefers-reduced-motion: reduce) {
  .faq__q { transition: none; }
  .faq__icon::before,
  .faq__icon::after { transition: none; }
}


/* ============================================================
   JREVS — three reviews

   The About page's .revs runs six on a dark band under a flat blue
   heading plate, because that section is a 1:1 copy of the client's
   live design. This one is three on white cards, matching v1's junk
   page, which carries three.

   REPRODUCED VERBATIM, TYPOS INCLUDED. Same warning as everywhere
   else these appear: publishing a review in different words than the
   customer wrote is what the FTC endorsement rule prohibits.
   ============================================================ */

.jrevs {
  background: var(--surface);
  padding: var(--sec-pad) 20px;
}

.jrevs__inner {
  max-width: var(--rail);
  margin: 0 auto;
}

.jrevs__grid {
  list-style: none;
  margin: 44px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

/* Flex column with the attribution pushed to the bottom, so three
   quotes of very different lengths still line their names up. */
.jrevs__card {
  display: flex;
  flex-direction: column;
  padding: 32px 30px;
  border: 1px solid #E4E7EE;
  border-radius: 20px;
  background: #FFFFFF;
}

/* INLINE SVG, NOT /assets/img/reviews-stars.svg, and the reason is
   the colour. That file is the client's own and it is hard-coded
   #DDFF00 — a chartreuse that works on the About page, where it sits
   on a dark band and reads as a highlight, and looks like a washed
   out lime on white. These cards say "Google review", and the v2 home
   page already sets its review stars to Google's own #FBBC04. Same
   glyph and same colour as that component, so the two agree.

   The shared SVG file is deliberately left alone: the About page
   depends on it looking exactly as it does. */
.jrevs__stars {
  display: flex;
  gap: 3px;
  margin-bottom: 18px;
}
.jrevs__stars svg {
  width: 17px;
  height: 16px;
  flex: none;
  fill: #FBBC04;
}

.jrevs__quote {
  margin: 0 0 26px;
  font-size: 16px;
  line-height: 1.65;
  color: var(--body-ink);
}

.jrevs__who {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: auto;
  padding-top: 20px;
  border-top: 1px solid #EDEFF4;
}

.jrevs__avatar {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface);
}

.jrevs__name {
  font-family: var(--font);
  font-size: 15px;
  font-weight: 600;
  color: var(--head-ink);
}

.jrevs__src {
  display: block;
  margin-top: 2px;
  font-size: 13px;
  font-weight: 500;
  color: #6A7185;
}


/* ============================================================
   JCTA — closing band

   Same construction as the About page's .cta: photograph on one side,
   copy panel on the other, ranged left. Rebuilt here rather than
   shared because about.css is not loaded on this page — see the note
   at the top of the file.

   ONE DEVIATION from About's version, and it is the background. That
   one panels on --surface, which would put this page's eighth grey
   section directly against its ninth. White instead.

   Contrast on #FFFFFF:
     #0B1226 ... 18.42   heading
     #4C5265 .... 8.11   copy
     #0B1226 on #71B745 ... 8.06   button
   ============================================================ */

.jcta {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: 0;
  background: #FFFFFF;
  border-top: 1px solid #E9ECF2;
}

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

/* 78%, not centred. The crew stand in the lower third of the frame
   under a tall pine, so a centred crop fills the band with tree and
   cuts the row of faces off at the bottom edge. This lands the whole
   group, the van and its door lettering inside the band. */
.jcta__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 78%;
}

.jcta__panel {
  display: flex;
  align-items: center;
  padding: clamp(48px, 6vw, 88px) clamp(28px, 4vw, 56px);
}

/* 33rem, where the About page's equivalent is 29. That page's band
   carries ONE button; this one carries two, and at 30rem the pair
   measured 482px against a 480px column and wrapped — one button per
   line, for two pixels. 33rem holds them side by side and still keeps
   the heading to two lines. */
.jcta__inner { max-width: 33rem; }

.jcta__eyebrow {
  margin: 0 0 14px;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent-green);
}

/* Joins the same group — see the note on .shead__title. This one was
   the biggest jump: DM Sans 800 at -1px down to Montserrat 300 at
   -.04em. It is exactly the move about.css made on its own .cta__
   title, and for the same reason: a closing band set in a different
   face at nearly three times the weight of every other heading on the
   page read as a different site's section. */
.jcta__title {
  margin: 0 0 20px;
  font-family: var(--font);
  font-weight: 300;
  font-size: clamp(30px, 3.9vw, 50px);
  line-height: 1.1;
  letter-spacing: -.04em;
  color: var(--head-ink);
}

.jcta__copy {
  margin: 0 0 32px;
  max-width: 46ch;
  font-size: 16.5px;
  line-height: 1.65;
  color: var(--body-ink);
}

.jcta__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
}

/* The green button is the About page's, navy-on-green rather than
   white-on-green — white on #0FA608 is 3.24:1 and fails AA. Same
   green, legible text. */
.jcta__btn--go {
  background: #0FA608;
  border-color: #0FA608;
  color: #0B1226;
}
.jcta__btn--go:hover { background: #12B80A; border-color: #12B80A; }


/* ============================================================
   RESPONSIVE

   Three stops, and each one is where a specific grid stops working
   rather than a round number:

     1040  four-across becomes two-across (steps, gallery) and the
           two-column splits stack. 1200/4 = 300px cells, and the
           step cards need ~250 of usable width after their padding.
     860   three reviews become one; the CTA's two halves stack.
     560   the haul list drops to a single column.
   ============================================================ */

@media (max-width: 1040px) {
  .split__inner {
    grid-template-columns: minmax(0, 1fr);
    gap: 34px;
  }
  /* The reorder has to be undone when stacked, or the reversed
     instance puts its photograph above its own heading. */
  .split--rev .split__media { order: 0; }

  .hsteps__grid { grid-template-columns: repeat(2, 1fr); }
  .jrevs__grid { grid-template-columns: repeat(2, 1fr); }
  /* Odd card out spans both columns rather than sitting half-width
     next to a hole. */
  .jrevs__card:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

@media (max-width: 860px) {
  .jcta {
    grid-template-columns: minmax(0, 1fr);
  }
  .jcta__media { min-height: 300px; order: -1; }
  .jrevs__grid { grid-template-columns: minmax(0, 1fr); }
  .jrevs__card:last-child:nth-child(odd) { grid-column: auto; }
}

@media (max-width: 640px) {
  :root { --sec-pad: 52px; }

  /* The hero is NOT in here. It carries About's own two breakpoints,
     720 and 400, up beside the component — moving them into this
     block would have meant re-deriving numbers that are only correct
     because they match. */
  .btn-row .btn,
  .jcta__actions .btn { flex: 1 1 100%; }

  .split__photo { border-radius: 16px; }

  /* TWO COLUMNS ON A PHONE, NOT ONE, and the icon stays ABOVE its
     name rather than beside it.

     The rule this replaces did the opposite: one column, icon moved
     into a second grid track beside the title. That existed to stop
     four stacked CARDS running to nearly three phone screens — each
     measured 209-233px on a 375px screen. The cards are gone and the
     icon is now the point of the section, so laying it on its side
     at 44px would be throwing away the change that was just asked
     for.

     Two columns gets the height back instead, and gets it back
     harder than the sideways trick did: four steps become two rows,
     the icon stays centred over its own name, and the whole section
     fits in roughly one screen. Each column is ~160px at 375px, so
     the icon runs at the clamp's 104px floor with room either side.

     The centring comes from .hsteps__item itself and needs nothing
     here. */
  .hsteps__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 34px 18px; }
  .hsteps__ico { margin-bottom: 16px; }
  .hsteps__name { margin: 0 0 7px; }
  .hsteps__copy { font-size: 15px; }
}


/* ============================================================
   WHAT WE CLEAR — the photo rail (.clears)

   Section two, directly under the hero, mirroring the band in the
   same slot on the live site. Seventeen square photographs of job
   types running off the right edge, each captioned, on a dark band
   with one pair of round arrows.

   THE PHOTOGRAPHS ARE THE CLIENT'S OWN, pulled from the live site
   rather than re-matched from /assets/img/gallery-*. That matters:
   on the live site each label is generated from the image's own alt
   attribute, so the photo and the category are guaranteed to agree.
   Pairing our existing gallery shots against the same sixteen names
   by hand produced things like a garden shed captioned "Attics".

   FOUR THINGS ARE DELIBERATELY NOT THE LIVE VERSION'S:

     · IT HAS A HEADING. The live band opens with photographs and
       nothing else — no eyebrow, no title, no sentence. Directly
       under a hero that is the worst place on the page to say
       nothing, so it takes the standard .shead like every other
       section here.

     · THE LABEL SITS INSIDE THE PHOTO, bottom-left, inset from both
       edges and over a real gradient scrim. The live one hangs a
       plate off the card's bottom-RIGHT corner, overlapping nothing
       and aligned to nothing, which reads as a z-index accident. The
       scrim is what lets white type sit on an unknown photograph.

     · CONTROLS ARE ONE CENTRED GROUP. The live version splits them —
       dots bottom-left, arrows bottom-right — so neither corner
       reads as "the controls".

     · A COUNT REPLACES THE DOTS. Seventeen dots is not a position
       readout, it is a texture. See the [data-carousel-count] note
       in v2.js, which this is the first consumer of.

   ⚠ RHYTHM — IT CUTS BOTH WAYS, READ THE BLOCK AT THE TOP OF THIS
   FILE FIRST. That block sets the rule that no two adjacent sections
   share a background, and flags the hero/residential junction as the
   page's weak point: after the hero rebuild those two became two
   full-bleed photographs in a row, separated only by the direction of
   their scrims.

   WHAT THIS SECTION FIXES: it lands between them, so that junction no
   longer exists. Two photographs with a captioned rail between them
   is a stronger separation than the composition trick the note above
   was relying on.

   WHAT IT INTRODUCED, AND HOW THAT WAS RESOLVED: this band shipped
   dark, and with .rband below it also dark the new junction was
   dark-to-dark. The top of the page then ran hero + this + rband =
   2155px of unbroken dark before anything light, which is what "the
   whole theme should be light" was reacting to.

   The fix predicted here — swap the gradient for var(--surface) and
   flip the four LIGHT-FLIP colours — has been applied. It cost
   nothing else, exactly as this note said it would.
   ============================================================ */

/* LIGHT NOW. This band was navy and the block above documented the
   swap in advance — "if the top of the page starts reading as one
   region, swap the gradient for background: var(--surface) and flip
   the four colours marked LIGHT-FLIP". That is what happened and
   this is that swap.

   NO GRADIENT AND NO NOISE LAYER ANY MORE, and both go for the same
   reason: the dark-section rule exists because the display pipeline
   spatial-dithers a flat dark charcoal into a visible diagonal
   hairline. --surface is #F1F5F7. There is nothing at that luminance
   for a dither pattern to show up in, so the two-stop gradient and
   the noise overlay were solving a problem this section no longer
   has. .clears__noise is deleted rather than left dormant.

   The photographs are unaffected — their labels sit on a gradient
   scrim burned into each card, not on the section, so they still
   read white on their own ground. */
.clears {
  position: relative;
  overflow: hidden;
  padding: var(--sec-pad) 0;
  background: var(--surface);

  /* THE RAIL USES THE SCREEN, NOT THE 1200px CONTENT RAIL.

     This used to be `max(20px, (100vw - var(--rail)) / 2)`, which
     lined the first card up with every other section's left edge. On
     a 1905px viewport that is a 352px inset — and because the rail
     deliberately bleeds off the RIGHT, the result was a large empty
     margin on one side and cards running under the edge on the other.
     Lopsided rather than aligned, and it wasted about a card and a
     half of visible width.

     A small symmetric-feeling inset instead. The rail is a full-bleed
     element now: it starts near the left edge and still runs off the
     right, which is what tells you it scrolls. */
  --clears-gut: clamp(16px, 2.2vw, 44px);
}

.clears > * { position: relative; }

.clears__head {
  max-width: var(--rail);
  margin: 0 auto clamp(28px, 3.2vw, 46px);
  padding-inline: 20px;
}
/* Flipped with the band. These now inherit the same navy/grey every
   other section head on the page uses, so nothing about this heading
   is special-cased any more. */
.clears__head .shead__title { color: var(--head-ink); }
.clears__head .shead__copy  { color: var(--body-ink); }

.clears__track {
  list-style: none;
  margin: 0;
  padding-inline: var(--clears-gut);
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(30vw, 380px);
  gap: clamp(12px, 1.5vw, 22px);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: var(--clears-gut);
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.clears__track::-webkit-scrollbar { display: none; }
.clears__track:focus-visible { outline: 3px solid var(--accent-green); outline-offset: 4px; }

.clears__card { scroll-snap-align: start; min-width: 0; }

.clears__shot {
  position: relative;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 14px;
  background: #1A2338;      /* holds the box while the photo loads */
}
/* THE BOTTOM 15% OF EVERY SOURCE FILE IS CROPPED OFF, and this is
   not framing — it is removing a label.

   The live site does NOT overlay its captions in CSS. Every exported
   file has its own blue "01. HOMES" plate burned into the pixels,
   sitting bottom-right over a strip of letterbox padding. That is
   also why the plate appears to overhang the card there: it does not,
   it is inside an image that has padding baked into it. Rendering
   these untouched under our own caption printed every label twice.

   Measured on haul-homes.webp (459x424): the plate occupies source y
   372-417, i.e. the bottom 12.3%, with a pale letterbox strip below
   it. 119% height anchored to the top drops the bottom ~16% outside
   the square, which clears both with margin to spare.

   THE REAL FIX IS CLEAN ORIGINALS FROM THE CLIENT. These files are
   459x424 — under half the pixels a 380px card wants on a 2x screen —
   and cropping makes that worse. Replace them and this rule goes. */
.clears__shot img {
  width: 100%;
  height: 119%;
  object-fit: cover;
  object-position: 50% 0;
  display: block;
}

/* The scrim. Without it white type on an unknown photograph is a
   coin toss — several of these frames are bright grass and sky at
   exactly the point the caption sits. */
.clears__shot::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 58%;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(11, 18, 38, 0) 0%, rgba(11, 18, 38, .82) 100%);
}

.clears__plate { position: absolute; left: 18px; right: 18px; bottom: 15px; z-index: 1; }
.clears__n {
  display: block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
  color: var(--accent-green);
  margin-bottom: 3px;
  font-variant-numeric: tabular-nums;
}
.clears__name {
  display: block;
  font-family: var(--font);
  font-size: clamp(17px, 1.6vw, 23px);
  font-weight: 400;
  letter-spacing: -.02em;
  line-height: 1.15;
  color: #FFFFFF;
  text-transform: capitalize;
}

.clears__foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  margin-top: clamp(26px, 3vw, 40px);
  padding-inline: 20px;
}

/* 48px, not v2's 30px .reviews__arrow. That row sits beside three
   visible cards on a light ground; this is the only control on a
   full-bleed dark band, and 44 is the floor for a touch target. */
.clears__arrow {
  width: 48px;
  height: 48px;
  flex: none;
  padding: 0;
  /* Flipped with the band: a white outline on #F1F5F7 is invisible.
     Navy on white, which is also what .btn--ghost does elsewhere on
     this page, so the two controls agree. #0B1226 on #FFFFFF is
     18.42:1. */
  border: 1px solid rgba(11, 18, 38, .22);
  border-radius: 50%;
  background: #FFFFFF;
  color: var(--head-ink);
  display: grid;
  place-items: center;
  transition: background-color .2s linear, opacity .2s linear;
}
.clears__arrow:hover { background: var(--head-ink); border-color: var(--head-ink); color: #FFFFFF; }
.clears__arrow:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.clears__arrow[disabled] { opacity: .3; pointer-events: none; }
.clears__arrow svg {
  width: 19px; height: 19px; fill: none; stroke: currentColor;
  stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round;
}

.clears__count {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--head-ink);
  font-variant-numeric: tabular-nums;
}
/* The "/ 17" half stays quieter than the current number. #6A7185 on
   #F1F5F7 is 5.0:1, so it is still legible rather than decorative. */
.clears__count span { color: #6A7185; font-weight: 600; }

@media (max-width: 620px) {
  .clears__track { grid-auto-columns: min(76vw, 330px); }
  .clears__plate { left: 15px; right: 15px; bottom: 13px; }
}

@media (prefers-reduced-motion: reduce) {
  .clears__track { scroll-behavior: auto; }
}


/* ============================================================
   FINAL CTA — PORTED FROM about.css

   ⚠ THIS IS A COPY, NOT A NEW DESIGN. Every value below came off
   v2/about/'s .cta at the client's request that this page's band
   match it one-to-one. If that one changes, this one has to change
   with it — there is no shared stylesheet holding the two together,
   because .cta lives in about.css and this page does not load it.
   Moving both into v2.css is the fix if a third page ever wants it.

   ONE DELIBERATE DIFFERENCE FROM THE SOURCE, and it is a tidy-up
   rather than a change: about.css sets .cta__title twice — once here
   in the CTA block at font-head/800/-1px, then again eighty lines
   later in its "weight and case pass" at var(--font)/300/-.04em. The
   second wins. Only the winning values are written below, so what
   renders is identical and there is no dead declaration to trip over.
   Verified against the About page with getComputedStyle: 54px,
   weight 300, -2.16px tracking, 59.4px leading.

   ⚠ v2/dumpster/ NOW HAS ITS OWN .cta AND ITS OWN COPY OF THIS BLOCK,
   in dumpster.css. It does not load this file, so the two cannot
   collide — but they can drift. There are three copies of this
   component now (about.css, here, dumpster.css), which is two more
   than there should be: moving it into v2.css is the fix.

   The reasoning behind the numbers is in about.css and is not
   duplicated here; the short version is that the split is 58/42
   rather than even so the photograph is worth looking at and the
   headline can run 54px without wrapping badly, and that white on
   var(--blue) is 4.64:1 where navy on it is 4.01:1.
   ============================================================ */

.cta {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: 0;
  background: var(--surface);
}

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

/* 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%;
}

/* Ranged left, not centred: centred text in a half-panel has no edge
   to line up against, so the heading, paragraph, button and phone
   line each start at a different x. */
.cta__panel {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 96px 56px;
  text-align: left;
}

.cta__inner { max-width: 29rem; }

.cta__title {
  margin: 0 0 22px;
  font-family: var(--font);
  font-weight: 300;
  font-size: clamp(32px, 4.1vw, 54px);
  line-height: 1.1;
  letter-spacing: -.04em;
  color: var(--head-ink);
}

.cta__copy {
  margin: 0 0 36px;
  max-width: 46ch;
  font-size: 16.5px;
  line-height: 1.65;
  color: var(--body-ink);
}

.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); }
.cta__btn:focus-visible { outline: 3px solid var(--head-ink); outline-offset: 3px; }

/* Live runs TWO buttons in this block — BOOK NOW! and GET A FREE
   ESTIMATE NOW! — and v2 had only the first. Same treatment as the
   About page's closing CTA. The panel behind them is var(--surface),
   a light ground, so the ghost is a blue outline rather than the
   white one a dark band would want. */
.cta__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

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

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

/* Stack below 900: the panel's measure and the photo's crop both stop
   working in a half-column narrower than about 440px. Stacked, the
   band is a wide short letterbox, so cover keeps the full width and
   slices top and bottom — the reverse of the desktop crop. 1.14 pulls
   the pile back up to filling that strip. */
@media (max-width: 900px) {
  .cta { grid-template-columns: 1fr; }
  .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%; }
}

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