/* ==========================================================================
   Breakara Demolition Staten Island
   Breakara family visual system. Inherited from the family's design-reference sibling, which is
   named once, in DESIGN-DNA.md section 1, and deliberately not in bytes this site serves.

   What is inherited is the BRAND: hazard-on-white with a graphite ground,
   one neutral grotesque across everything, 2px buttons, the bordered image
   mat, the '//' page-title motif, the chevron text link, the native FAQ
   disclosure, the hazard copy bar, the graphite footer, the sticky mobile
   call bar.

   V-14 REPAIR (2026-08-16). This header used to claim the family was carried
   partly "by … tick lists" and the file styled a `.list_ok` that rendered on
   zero pages, alongside a complete form stylesheet for a site with no <form>,
   a #get_quote band this site deliberately does not render, and six other
   never-emitted classes. A claim in the build record that is not true of the
   build is worse than a missing one; both the claim and the styles are gone.
   Every selector below now has a rendering path in scripts/build-site.mjs.

   What is NOT inherited is the SITE: this file styles Staten Island's own
   composition — alternating image/text service rows, a display-scale address
   band, and a services index the sibling does not have. Same brand, different
   site. See DESIGN-DNA.md.

   The previous Adova dark system was retired 2026-08-16 and is preserved at
   tag design/adova-dark-v1. Nothing of it survives here: no near-black
   ground, no spruce, no display/body type pairing, no 10px radius, no
   hand-drawn shapes, no SVG step marks.

   Dependency-free: no framework, no plugin CSS, no remote font request.
   ========================================================================== */

/* -------- 0. Type ------------------------------------------------------
   Roboto, self-hosted from this origin. The cluster's face, and the reason it is served from
   here rather than requested from Google is that this site's single-external-origin property
   (the map, and nothing else) is worth more than the convenience: a webfont request would make
   every page depend on a third party to render its own headings, and it would need a second
   entry in the Content-Security-Policy. 100 KB for five weights, swap-displayed so text paints
   immediately in the fallback.

   V1 used the platform system stack. That was defensible on its own and wrong for the cluster:
   the three stronger Breakara properties all set Roboto, so this site rendered the same brand
   in a different voice on every heading. */
@font-face{font-family:Roboto;font-style:normal;font-weight:300;font-display:swap;src:url(/assets/fonts/roboto-300.woff2) format("woff2")}
@font-face{font-family:Roboto;font-style:normal;font-weight:400;font-display:swap;src:url(/assets/fonts/roboto-400.woff2) format("woff2")}
@font-face{font-family:Roboto;font-style:normal;font-weight:500;font-display:swap;src:url(/assets/fonts/roboto-500.woff2) format("woff2")}
@font-face{font-family:Roboto;font-style:normal;font-weight:700;font-display:swap;src:url(/assets/fonts/roboto-700.woff2) format("woff2")}
@font-face{font-family:Roboto;font-style:normal;font-weight:900;font-display:swap;src:url(/assets/fonts/roboto-900.woff2) format("woff2")}

/* -------- 1. Tokens ----------------------------------------------------
   RE-POINTED 2026-08-21, V1 to V2 remediation.

   These were not arbitrary before and they are not arbitrary now. They were WRONG, and the
   proof is in this repository rather than in a preference: the dominant non-neutral pixels in
   `assets-src/brand/logo.png` are #343333 and #f6bd19, byte-for-byte the same as the Hub's and
   Manhattan's logos, and the stylesheet declared #2b2e31 and #f2c300. The header served a
   #f6bd19 wordmark on a #f2c300 interface, on every page, since 2026-08-16.

   So the hue is not being changed to match a sibling. It is being corrected to match this
   site's own brand asset, and the cluster alignment falls out of that for free. */
:root {
  --hazard: #f6bd19;
  --hazard-dark: #dfa806;
  --hazard-ink: #7a5f00;      /* the hue made legible as text on white, 5.9:1 */
  --hazard-line: #f7d97e;
  --hazard-tint: #fdf3d8;
  --graphite: #222;
  --graphite-soft: #3d3c3c;
  --steel: #565a5c;
  --muted: #7b7f81;
  --ink: #343333;
  --concrete: #f9f9f9;
  --rule: #e2e2e0;
  --white: #fff;

  /* The family's hazard hatching. One saturated colour, used structurally, and the only place
     it tints a large area is a call-to-action band or the copy bar. */
  --stripes: repeating-linear-gradient(135deg, rgba(0,0,0,.05) 0 14px, rgba(0,0,0,0) 14px 28px);
  --stripes-light: repeating-linear-gradient(135deg, rgba(255,255,255,.06) 0 14px, rgba(255,255,255,0) 14px 28px);

  /* V-03 / V-04 REPAIR (2026-08-16). Two colours in this system depend on the surface
     underneath them rather than on the component, and both had been solved once and then
     re-broken by the next component that landed on a dark ground: --hazard-ink is a light-
     ground colour (5.81:1 on white, 2.61:1 on graphite) and --hazard is a dark-ground one
     (9.10:1 on graphite, 1.67:1 on white). Scoping them per component is what produced
     F-01 and then V-03 — a .tel-link fixed and a .text-btn missed in the same wrapper.
     They are tokens now, re-pointed ONCE per dark surface below. The custom property
     inherits, so every focusable and every chevron link inside those surfaces is correct
     without naming it. Mechanic taken from the family reference, which does the same. */
  --focus: var(--hazard-ink);
  --link-accent: var(--hazard-ink);
  --link-accent-hover: var(--ink);

  --font: Roboto, system-ui, -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;

  --container: 1170px;
  --gutter: 30px;
  --section: 72px;
  --radius: 10px;
  --radius-sm: 3px;

  --shadow-sticky: 0 2px 10px -2px rgba(0, 0, 0, .41);
  --ease: color .2s ease, background-color .2s ease, border-color .2s ease, box-shadow .2s ease, opacity .2s ease;
}

/* -------- 2. Base ------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

/* 17px/1.65 rather than V1's 15px/1.6. The three stronger properties in this cluster all read
   at 17px, and 15px is a 2015 measure for body text on a page a homeowner reads on a phone
   while standing in the room they want gone. */
body {
  margin: 0;
  background: var(--white);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.65;
  color: var(--steel);
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  color: var(--ink);
  margin: 0 0 16px;
  line-height: 1.2;
  font-weight: 700;
}
h1 { font-size: clamp(2rem, 4.2vw, 3.1rem); font-weight: 900; letter-spacing: -.02em; }
h2 { font-size: clamp(1.55rem, 2.6vw, 2.15rem); letter-spacing: -.01em; }
h3 { font-size: 1.22rem; }
h4 { font-size: 1.02rem; }

p { margin: 0 0 22px; }
p:last-child { margin-bottom: 0; }

a { color: var(--hazard-ink); text-decoration: none; transition: var(--ease); }
/* V-03/V-04: this used to resolve to --ink on every surface, which meant a link on the hero,
   the closing band or the footer turned near-black the moment it was focused — dark on dark,
   and worse than the state it came from. It follows the surface token, so it is --ink on the
   light ground it was written for and --hazard on the graphite ones. */
a:hover, a:focus { color: var(--link-accent-hover); }

/* B-04 REPAIR (2026-08-16). An inline link inside running prose was separated from the text
   around it by colour alone — rgb(122,99,0) against rgb(86,90,92), 1.23:1, same weight, no
   underline — which fails WCAG 1.4.1 Use of Colour (technique G183 wants 3:1 when colour is the
   only cue). Scoped to unclassed anchors inside a paragraph or list item, so every designed
   control keeps its own treatment: .btn, .text-btn, .tel-inline and .tel-link all carry a class
   and are untouched, and .tel-inline already carries its non-colour cue as weight 700. */
.prose p a:not([class]), .prose li a:not([class]),
.measure p a:not([class]), .measure li a:not([class]) {
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}

img { max-width: 100%; height: auto; display: block; }

ul { margin: 0 0 25px; padding-left: 20px; }
li { margin-bottom: 9px; }

hr { margin: 45px 0; border: 0; border-top: 1px dotted var(--rule); }

/* Every dark surface in the site, in one place. Anything placed on one of them takes the
   dark-ground values of both surface tokens. */
.hero, .page-head, .cta, .side-panel, .site-footer {
  --focus: var(--hazard);
  --link-accent: var(--white);
  --link-accent-hover: var(--hazard);
}

/* V-04 REPAIR (2026-08-16). The ring was hazard yellow on every surface: 1.67:1 on white,
   1.51:1 on the concrete panels and 1:1 against the primary button, which is filled with the
   same yellow. Non-text contrast needs 3:1, so a keyboard user saw a usable ring only on the
   graphite bands. It now follows the surface token: 5.81:1 on white, 5.24:1 on concrete,
   3.48:1 against the yellow button fill, 9.10:1 on graphite. */
:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

/* The hazard-filled controls are the one case a single colour cannot cover, because the ring
   has to separate from the fill AND from the ground, and those grounds are white on eight
   pages and graphite inside the closing band. Two concentric bands do it: ink against the
   yellow fill (8.18:1) and a white halo outside it (13.66:1 on the yellow, 9.44:1 on
   graphite). An outline paints over a box-shadow, so the shadow shows only as the outer 3px. */
.btn:focus-visible,
.primary-nav .nav-cta a:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 0;
  box-shadow: 0 0 0 6px var(--white);
}

/* The skip link is a graphite chip that appears over the top of whatever page it is on, so it
   has the same two-neighbour problem inverted, and takes the same two-band answer: hazard
   against the chip (9.10:1), ink against the page behind it (13.66:1 on white). */
.skip-link:focus-visible {
  outline: 3px solid var(--hazard);
  outline-offset: 0;
  box-shadow: 0 0 0 6px var(--ink);
}

.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 20px;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100000;
  background: var(--graphite); color: var(--white);
  padding: 12px 20px; font-weight: 700;
}
.skip-link:focus { left: 0; color: var(--white); }

.measure { max-width: 66ch; }
/* The FAQ answers want a little more room than a prose column but must not run the full
   container; the family reference sets the same block at 820px. */
.measure-wide { max-width: 820px; }

/* F-10 REPAIR: the reset was scoped to .footer-address, so the contact page's <address>
   rendered in browser-default italic. The family reference resets it globally. */
address { font-style: normal; }

/* Family motif: small uppercase label above a heading. */
.eyebrow {
  font-size: 12px;
  font-weight: 900;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--hazard-ink);
  margin: 0 0 10px;
}

.section { padding: var(--section) 0; }
.section--tight { padding: 40px 0; }
.section--flush-top { padding-top: 0; }

.section-head { margin-bottom: 34px; }
.section-head h2 { margin-bottom: 10px; }

/* -------- 3. Buttons and links ----------------------------------------- */
.btn {
  display: inline-block;
  border: none;
  cursor: pointer;
  font-family: inherit;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .02em;
  padding: 13px 30px;
  border-radius: 2px;
  background: var(--hazard);
  color: var(--ink);
  transition: var(--ease);
  text-align: center;
}
/* A button is not a text link, so the generic `a:focus` colour must not reach it: on the
   graphite bands that token resolves to hazard, which would have set a yellow label on the
   yellow fill. Declared BEFORE the hover rule on purpose — equal specificity, so hovering a
   focused button still gets the inverted white-on-ink pair rather than ink on ink. */
.btn:focus { color: var(--ink); }
/* Hover only. V-04: the inversion used to fire on :focus as well, which turned the button ink
   at exactly the moment the focus ring was drawn — and the ring that reads against the yellow
   fill is itself ink, so the indicator vanished into the state it was meant to mark. Focus is
   expressed by the ring; hover is expressed by the fill; neither cancels the other. */
.btn:hover { background: var(--ink); color: var(--white); }

/* Family motif: a "read more" link with a drawn chevron that steps out on hover.
   The sibling uses the same construction; it is one of the clearest family cues.

   V-03 REPAIR (2026-08-16). The colour was hard-coded to --hazard-ink, a light-ground
   olive, and /contact/ puts one of these inside .hero-actions on the graphite-masked hero,
   where it measured 1.37:1 to 3.31:1 composited against the photograph. It reads the
   surface token now, so the hero, the closing band and the footer get the dark-ground
   value without a per-page override — the fix F-01 applied to .tel-link and missed here. */
.text-btn {
  display: inline-block;
  font-weight: 700;
  font-size: 14px;
  color: var(--link-accent);
}
.text-btn::after {
  content: "";
  display: inline-block;
  width: .5em; height: .5em;
  margin-left: .45em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg) translateY(-1px);
  transition: var(--ease), margin-left .2s ease;
}
.text-btn:hover { color: var(--link-accent-hover); }
.text-btn:hover::after { margin-left: .75em; }

.tel-link { font-weight: 700; color: var(--ink); }
.tel-link:hover { color: var(--hazard-ink); }

/* -------- 4. Top utility line ------------------------------------------ */
#top_line {
  height: 30px;
  font-size: 13px;
  border-bottom: 1px solid rgba(0, 0, 0, .1);
  background: var(--white);
  overflow: hidden;
  transition: height .2s ease, opacity .2s ease, visibility .2s ease;
}
#top_line .container { display: flex; align-items: center; justify-content: space-between; height: 30px; }
#tag_line { color: var(--steel); }
#top_links { display: flex; gap: 14px; list-style: none; margin: 0; padding: 0; font-weight: 600; }
#top_links a { color: var(--ink); }
#top_links a:hover { color: var(--hazard-ink); }

/* V-12 REPAIR (2026-08-16). The mobile rule `a[href^="tel:"] { min-height: 44px }` also matched
   this link, which lives in a 30px bar with `overflow: hidden` — so at 768-991 the enlarged
   target was clipped from 44px back to 30px and the rule achieved nothing, while the glyphs
   sat ~4px above the bar's optical centre because the inherited `li { margin-bottom: 9px }`
   was never reset and skewed the flex centring. The link now FILLS the bar instead of trying
   to exceed it: a 30px target that really is 30px, which clears WCAG 2.5.8's 24px floor. The
   comfortable 44px target at those widths is the 54px fixed call bar, which is where the
   enlargement belongs. The reset below is the missing half of the `#top_links` margin reset. */
#top_links { align-self: stretch; }
#top_links li { margin: 0; display: flex; }
#top_links a { display: flex; align-items: center; height: 100%; }
/* The bar clips to animate its height, and an outline is drawn outside the box by definition,
   so a ring with a positive offset would be clipped away on the site's utility-line phone
   link. Drawn inside instead, where the clip cannot reach it. */
#top_links a:focus-visible { outline-offset: -3px; }

/* Collapsed means gone, not merely transparent: an opacity-0 box still holds its link in the
   tab order, so a keyboard user could focus a phone action inside an invisible bar and see a
   focus ring on nothing. `visibility` is discrete, so it flips at the end of the collapse and
   at the start of the expansion, which is what the transition wants either way. */
.site-header.is-stuck #top_line { height: 0; opacity: 0; border-bottom: 0; visibility: hidden; }

/* -------- 5. Header and navigation ------------------------------------- */
/* Sticky, not fixed. A fixed header leaves the flow, so the document has to reserve its
   height from script — and this site's CSP is style-src 'self', which blocks a
   JS-applied inline style outright. Sticky pins the header identically without leaving
   the flow, so there is nothing to reserve and nothing to block. */
.site-header {
  width: 100%;
  background: var(--white);
  position: sticky;
  top: 0;
  z-index: 9999;
}
.site-header.is-stuck { box-shadow: var(--shadow-sticky); }

/* Scroll lock while the off-canvas panel is open. A class, not an inline style, for
   the same CSP reason. */
html.is-locked, html.is-locked body { overflow: hidden; }

/* V-01 REPAIR (2026-08-16). This declared `padding: 14px 0` on the element that also carries
   .container. Both are single-class selectors, so the later shorthand won outright and wiped
   the container's `padding: 0 20px`: computed paddingLeft on .container.header-in was 0px.
   The logo's leftmost pixels touched x=0 and the menu button touched the right edge at every
   width below 1140, while the utility line one row above, the breadcrumb, main and the footer
   all sat at 20px; above 1140 the header content sat 20px OUTSIDE the page's content column.
   Longhand only, so the gutter the container owns is never overwritten by a vertical rhythm
   declaration. The same trap is why the stuck state below is longhand too. */
.header-in {
  display: flex; align-items: center; justify-content: space-between;
  gap: 20px;
  padding-top: 14px;
  padding-bottom: 14px;
  transition: padding .2s ease;
}
.site-header.is-stuck .header-in { padding-top: 8px; padding-bottom: 8px; }

/* The designed lockup is the whole brand element: the business name lives inside the
   artwork, so nothing is typed beside it.

   Constrained by HEIGHT, not width. The family reference is a 3.9:1 lockup and this one is
   3.2:1, so sizing both to the same width would render this one shorter and it would read
   as a thin strip rather than as the brand. Height is what carries optical weight in a
   header; width follows the aspect ratio, so the mark stays correct if the artwork is ever
   re-cut. */
.brand { display: inline-block; line-height: 0; flex: none; }
.brand-logo {
  display: block;
  height: 46px;
  width: auto;
  max-width: 100%;
  transition: opacity .2s ease;
}
.brand:hover .brand-logo { opacity: .82; }
.site-header.is-stuck .brand-logo { height: 40px; }

/* V-02 REPAIR (2026-08-16). Between 992 and ~1090 this row did not fit, and because nothing
   stopped it wrapping, every two-word item broke across two lines ("Structure / Demolition")
   and the CTA — which computed to `display: inline` with 11px/20px padding — fragmented into
   TWO offset yellow rectangles with the label split across them. `getClientRects().length`
   returned 2 for the pill at client widths 977 and 1045.

   Restoring the container gutter in V-01 takes 40px off the row, so the band would have grown
   rather than shrunk; both had to be fixed together. Three rules make fragmentation
   structurally impossible, then the band below buys the width to honour them:

     nowrap on the list   an item can never drop to a second line
     nowrap on the label  a two-word item can never break inside itself
     inline-block on the  an inline box fragments into one rectangle per line; a block-level
       CTA                box is one rectangle by definition and cannot come apart

   DIVERGENCE FROM THE REFERENCE, STATED. The design-reference sibling avoids this with five nav items
   and no separate header phone (its number lives in the utility line). This site keeps six
   items and the header phone, and the reason is that neither is spare here: the sixth item is
   /services/, the hub the sibling does not have and the parent of every service route in this
   site's breadcrumb trail; and the header phone is the only phone action in the sticky chrome
   above 991, because this site's utility line collapses on scroll rather than compressing.
   Dropping either would cost a real path to save a row that measurement shows fits. The cost
   of keeping them is the sizing band below — a smaller nav between 992 and 1199, which is the
   ordinary way a desktop header handles a narrow desktop. */
.primary-nav ul {
  display: flex; align-items: center; flex-wrap: nowrap; gap: 18px;
  list-style: none; margin: 0; padding: 0;
}
.primary-nav li { margin: 0; }
.primary-nav a {
  color: var(--ink);
  font-size: 14px; font-weight: 500;
  padding: 6px 0;
  position: relative;
  white-space: nowrap;
}
.primary-nav a::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 3px; background: var(--hazard);
  transform: scaleX(0); transform-origin: left; transition: transform .2s ease;
}
.primary-nav a:hover::after,
.primary-nav a[aria-current="page"]::after { transform: scaleX(1); }

.primary-nav .nav-cta a {
  display: inline-block;
  color: var(--ink); background: var(--hazard);
  padding: 11px 18px; border-radius: 2px; font-weight: 700;
}
.primary-nav .nav-cta a::after { display: none; }
.primary-nav .nav-cta a:hover { background: var(--ink); color: var(--white); }

.header-phone {
  font-size: 16px;
  font-weight: 900;
  color: var(--ink);
  white-space: nowrap;
  border-bottom: 3px solid var(--hazard);
  padding-bottom: 2px;
}
.header-phone:hover { color: var(--hazard-ink); }

/* The narrow-desktop band. The row carries a 180px lockup, six nav items, the CTA pill and the
   phone, and the content column is 952px at 992 against 1100px at 1140 and above — so the same
   type scale cannot serve both ends. Everything steps down by one notch here and returns at
   1200, where there is room for it. Measured fit, not estimated: see the repair record. */
@media (min-width: 992px) and (max-width: 1199px) {
  .header-in { gap: 14px; }
  .brand-logo, .site-header.is-stuck .brand-logo { height: 40px; }
  .primary-nav ul { gap: 12px; }
  .primary-nav a { font-size: 13px; }
  .primary-nav .nav-cta a { padding: 10px 13px; }
  .header-phone { font-size: 15px; }
}

.menu-toggle {
  display: none;
  background: none; border: 0; cursor: pointer;
  padding: 10px; color: var(--ink);
  min-width: 44px; min-height: 44px;
}
.menu-toggle span {
  display: block; width: 24px; height: 2px; background: var(--ink); margin: 5px 0;
  transition: background-color .2s ease;
}

/* -------- 6. Off-canvas panel ------------------------------------------
   Closed means display:none, mounted by JS only while in use. A panel that is
   merely translated off-screen keeps its links in the tab order and the
   accessibility tree while reporting itself closed. */
.side-panel {
  display: none;
  position: fixed; inset: 0;
  z-index: 10000;
  background: var(--graphite);
  padding: 22px 0 40px;
  overflow-y: auto;
  opacity: 0;
  transition: opacity .25s ease;
}
/* Mounted by JS only while in use; .is-open then fades it in on the next frame. */
.side-panel.is-mounted { display: block; }
.side-panel.is-open { opacity: 1; }

.side-panel-top { display: flex; justify-content: flex-end; align-items: center; min-height: 46px; }
.panel-close {
  background: none; border: 0; color: var(--white);
  font-size: 30px; line-height: 1; cursor: pointer; padding: 4px 10px;
}
.panel-close:hover { color: var(--hazard); }

.side-panel nav ul { list-style: none; margin: 18px 0 0; padding: 0; }
.side-panel nav li { border-bottom: 1px solid rgba(255, 255, 255, .12); margin: 0; }
.side-panel nav a { display: block; color: var(--white); padding: 14px 0; font-size: 16px; font-weight: 500; }
.side-panel nav a:hover { color: var(--hazard); }
.side-panel nav a[aria-current="page"] { color: var(--hazard); }

.panel-contact { margin-top: 26px; color: rgba(255, 255, 255, .8); }
.panel-contact .eyebrow { color: var(--hazard); }
.panel-contact a { color: var(--white); font-size: 24px; font-weight: 900; border-bottom: 3px solid var(--hazard); }
.panel-contact a:hover { color: var(--hazard); }

/* -------- 7. Hero ------------------------------------------------------
   Staten Island's own composition, not the sibling's. The sibling runs a
   left-aligned statement over a horizontal gradient with a meta strip; this
   is centred over a flat mask, with the address band carrying the weight
   immediately beneath it. Same tokens, same type, different page. */
/* REBUILT 2026-08-22. The old hero was a 480px floor plus 72px of padding at each end, centred
   copy at max-width 760 and the form stacked underneath it, which put roughly 1000px between the
   top of the page and the first thing below the hero. The floor is gone, the padding is roughly
   halved, and the copy and the form are two columns, so the section under the hero now breaks the
   fold on a 1280x800 laptop instead of sitting a scroll below it. */
.hero {
  position: relative;
  overflow: hidden;
  background: var(--graphite);
  display: flex; align-items: center;
}
.hero-media { position: absolute; inset: 0; z-index: 0; }
.hero-media img,
.hero-media video { width: 100%; height: 100%; object-fit: cover; display: block; }
.hero::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: rgba(18, 20, 22, .74);
}
.hero .container { position: relative; z-index: 2; padding-top: 34px; padding-bottom: 34px; }

/* One column until there is genuinely room for two. Below 60rem the form follows the copy, which
   is the same order a screen reader and a keyboard get, so nothing depends on the grid. */
.hero-grid { display: grid; grid-template-columns: 1fr; gap: 1.6rem; align-items: center; }
.hero-copy { color: var(--white); }
.hero-copy h1 {
  color: var(--white);
  font-size: clamp(28px, 3.6vw, 40px);
  font-weight: 900;
  letter-spacing: -.015em;
  line-height: 1.08;
  margin-bottom: 12px;
}
.hero-standfirst {
  font-size: 17px;
  color: rgba(255, 255, 255, .9);
  margin: 0 0 18px;
  max-width: 46ch;
}

/* Trust line. Three short facts, never a badge graphic: a drawn crest asserts a credential the
   way a certificate does, and what stands behind these is an operator statement and nothing more.
   The tick is CSS content so the words stay the words in the markup and in the copy sweep. */
.hero-trust {
  display: flex; flex-wrap: wrap; gap: 8px 18px;
  list-style: none; margin: 0 0 20px; padding: 0;
}
.hero-trust li {
  display: inline-flex; align-items: center; gap: 7px;
  margin: 0;
  font-size: 14px; font-weight: 700; letter-spacing: .01em;
  color: var(--white);
}
.hero-trust li::before {
  content: "\2713";
  color: var(--hazard);
  font-size: 15px; font-weight: 900; line-height: 1;
}

/* Call or type, and the "or" says so. The number is the button's own second line rather than a
   separate link, so the whole control is one tap target at the size a thumb needs. */
.hero-call { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; }
.hero-call-btn {
  display: inline-flex; flex-direction: column; align-items: flex-start;
  gap: 1px;
  padding-top: 10px; padding-bottom: 10px;
  line-height: 1.15;
}
.hero-call-kicker { font-size: 11px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; opacity: .82; }
.hero-call-number { font-size: 21px; font-weight: 900; letter-spacing: -.01em; }
.hero-call-or { margin: 0; font-size: 14px; color: rgba(255, 255, 255, .82); }

.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
/* F-01 REPAIR (2026-08-16). This was `.hero-actions .tel-link`, unscoped, and the wrapper was
   reused on the --concrete address band, so the phone number rendered white-on-near-white at
   1.11:1 — invisible, on the primary conversion. White is a hero treatment because only the
   hero has the graphite mask behind it; scope it there. The 404's .error-actions still reuses
   this wrapper on white, so the scope is still load-bearing. */
.hero .hero-actions .tel-link { color: var(--white); align-self: center; }
.hero .hero-actions .tel-link:hover { color: var(--hazard); }
/* Everywhere else the wrapper sits on a light ground and the link keeps the default ink. */
.hero-actions .tel-link { align-self: center; }

/* -------- 8. Interior page head ----------------------------------------
   REMOVED 2026-08-21. `.page-head` was a flat graphite bar and it rendered on
   every interior page. It is replaced by the photographic `.pagehead` in section
   23.12, which carries the same `//` motif at three times the weight. The old
   rules are gone rather than left behind: a stylesheet that keeps the component
   it replaced is a stylesheet nobody can read. */

/* -------- 8b. Breadcrumb (family #position) ----------------------------- */
#position { background: var(--concrete); border-bottom: 1px solid var(--rule); }
#position ol {
  display: flex; flex-wrap: wrap; gap: 8px;
  list-style: none; margin: 0; padding-top: 10px; padding-bottom: 10px;
  font-size: 13px; color: var(--steel);
}
#position li { margin: 0; }
#position li + li::before { content: "/"; margin-right: 8px; color: var(--rule); }
#position a { color: var(--hazard-ink); }
#position a:hover { color: var(--ink); }
#position [aria-current="page"] { color: var(--ink); font-weight: 700; }

/* -------- 9. Address band ---------------------------------------------
   V-14 REPAIR: the family's #get_quote hazard band was styled here and is rendered on no
   page — deliberately, and the reason is recorded in scripts/build-site.mjs at the footer.
   A band the site has decided not to render does not need a stylesheet.

   Staten Island keeps its own strongest argument. The measured position is a
   verified island address in a market whose competitors run from another
   borough or from no address at all; the sibling has no equivalent block.
   Re-expressed in family language — concrete ground, hazard rule, weight-900
   display scale — rather than the retired system's oversized title slot. */
.address-block {
  background: var(--concrete);
  border-left: 6px solid var(--hazard);
  padding: 34px 30px;
}
.address-line {
  display: block;
  font-size: clamp(26px, 4vw, 38px);
  font-weight: 900;
  letter-spacing: -.015em;
  color: var(--ink);
  margin: 0 0 4px;
}
.address-sub { font-size: 17px; font-weight: 700; color: var(--ink); margin: 0 0 16px; }
.address-why { margin: 0; max-width: 62ch; }

/* V-06 REPAIR (2026-08-16). The band's actions borrowed the .hero-actions wrapper, and with it
   `justify-content: center` — a hero treatment. At 1280 the eyebrow, headline, sub-line and
   body all began at x=119 while the two actions floated at x=531…740, unrelated to anything:
   three alignments inside one block. At <=767 the borrowed wrapper switched to
   column/stretch while `.hero-actions .tel-link` kept `align-self: center`, so the number was
   centred above a left-aligned link. This block is left-aligned prose, so its actions are
   left-aligned prose actions — one alignment, at every width. It is also the site's signature
   local module, which is where sloppiness costs most. */
.address-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 24px; margin-top: 20px; }

/* -------- 10. Service rows ---------------------------------------------
   REMOVED 2026-08-21. `.srow` and its four modifiers presented THREE services as
   alternating image/text rows, and the fourth canonical service had no card at
   all. The V2 plan gives all four a page and the home page a four-up grid, so
   the rows render nowhere. Their two genuinely good ideas were kept rather than
   dropped: the bordered image mat lives on in `.figure` and `.card-media`, and
   the alternating beat lives on in `.feature` / `.feature--flip`, which is the
   same composition applied where a page actually has two things to alternate. */

/* -------- 11. Split, figures, bands ------------------------------------ */
.split { display: grid; grid-template-columns: 1fr 1.5fr; gap: 40px; align-items: start; }
.split--stack { grid-template-columns: 1fr; gap: 16px; }

.figure { margin: 0; }
.figure img {
  border: 1px solid var(--rule);
  padding: 4px;
  background: var(--white);
  width: 100%;
}
/* V-07 REPAIR (2026-08-16). This rendered 1100x825 at the full container — 92% of a 900px
   viewport, 23% of the whole /about/ page, and the tallest thing on four pages: taller than
   the 480px hero and taller than the 542px band that was designed to be the biggest element
   there. An inset supporting photograph half again taller than the deliberately full-bleed
   one is an inverted hierarchy. The 4:3 ratio was correct where it came from — .srow-media
   uses it at half width for a 483x362 image — and was reused at full width without being
   re-chosen. Re-chosen here: a 16:9 crop at 760px renders 760x428, which sits below the hero
   and well below the band, and reads as the supporting inset it is. It keeps the family's
   bordered mat and stays flush with the left edge of the copy it supports. */
.figure--contained { max-width: 760px; }
.figure--contained img { aspect-ratio: 16 / 9; object-fit: cover; }
/* Emitted by scripts/build-site.mjs whenever an asset carries a caption. Every caption in
   data/media-registry.json is currently null, so this renders on no page today — it is kept,
   unlike the residue V-14 removed, because it is a live data-driven branch: deleting it would
   either drop a capability the registry documents or leave the build silently ignoring a
   caption someone had written. */
.figure-caption { margin: 10px 0 0; font-size: 13.5px; color: var(--steel); }

.band { position: relative; }
.band img { width: 100%; aspect-ratio: 21 / 9; object-fit: cover; }
.band-caption { margin: 10px 0 0; font-size: 13.5px; color: var(--steel); }

/* -------- 12. Steps ---------------------------------------------------- */
.steps { display: grid; gap: var(--gutter); grid-template-columns: repeat(4, 1fr); }
.step { border-top: 3px solid var(--hazard); padding-top: 16px; }
.step-number {
  display: block;
  font-size: 12px; font-weight: 900; letter-spacing: .18em; text-transform: uppercase;
  color: var(--hazard-ink);
  margin-bottom: 6px;
}
.step h3 { font-size: 17px; margin-bottom: 8px; }
.step p { font-size: 14px; margin: 0; }

/* -------- 13. Callout, info grid, related ------------------------------ */
.callout {
  background: var(--concrete);
  border-left: 3px solid var(--hazard);
  padding: 18px 22px;
  margin: 0 0 25px;
}
.callout p { margin: 0; }

/* V-09 REPAIR (2026-08-16). This was a three-up, and its middle cell reprinted the same
   `<address>` that the location block directly above it already carries, verbatim, under a
   heading that repeats that block's own heading. The restatement is gone; what is left is the
   two facts the page states once. */
.info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px var(--gutter); }
.info-grid p { margin: 0; }

.related { border-top: 1px dotted var(--rule); padding-top: 22px; display: flex; flex-wrap: wrap; gap: 8px 26px; align-items: baseline; }
.related .eyebrow { margin: 0; width: 100%; }

/* -------- 13b. Contact: the call panel and the location split ---------- */
/* Calling is this page's entire job, so the number is set at display scale rather than as a
   line of body copy. The family reference gives its phone the same treatment on its contact
   page; the panel around it is this site's own. */
.call-panel {
  background: var(--concrete);
  border-left: 6px solid var(--hazard);
  padding: 30px;
}
.call-panel .eyebrow { margin-bottom: 6px; }
.phone-xl {
  display: inline-block;
  font-size: clamp(26px, 4vw, 38px);
  font-weight: 900;
  letter-spacing: -.015em;
  color: var(--ink);
  line-height: 1.1;
}
.phone-xl:hover { color: var(--hazard-ink); }
.call-panel-note { margin: 12px 0 0; }

.split--location { grid-template-columns: 1fr 1.2fr; align-items: start; }
.address-block--tight { padding: 26px 24px; }

/* -------- 14. Closing band (family .bg_content) ------------------------ */
.cta {
  background: var(--graphite);
  padding: 76px 0;
  text-align: center;
  color: rgba(255, 255, 255, .88);
}
.cta-inner { max-width: 760px; margin: 0 auto; }
.cta-title { color: var(--white); font-size: 30px; font-weight: 700; margin: 0 0 15px; line-height: 1.25; }
/* Family motif: short rules flanking the band heading. */
.cta-title::before, .cta-title::after {
  content: ""; display: inline-block; width: 34px; height: 2px;
  background: var(--hazard); vertical-align: middle; margin: 0 16px;
}
.cta p { color: rgba(255, 255, 255, .88); max-width: 62ch; margin: 0 auto 25px; }
.cta-actions { display: flex; flex-wrap: wrap; gap: 14px; justify-content: center; align-items: center; }
.cta-actions .tel-link { color: var(--white); }
.cta-actions .tel-link:hover { color: var(--hazard); }

/* -------- 15. FAQ ------------------------------------------------------
   Family motif, native disclosure. `<details>`/`<summary>` rather than a scripted accordion,
   so the keyboard handling, the focus behaviour and the expanded state announced to a screen
   reader all come from the element. The chevron is the same drawn mark as .text-btn, rotated,
   which keeps one chevron language across the site.

   V-14 REPAIR (2026-08-16) removed the section that stood here: a complete form stylesheet —
   .form-note, .form-group, label, label .req, .form-control, textarea.form-control, .hp — for
   a site that ships no <form> anywhere, correctly, because leadCapture.destinationEmail is
   null and a form submitting nowhere would be a dead control. The styles shipped regardless. */
.faq-item {
  border-bottom: 1px dotted var(--rule);
  padding: 16px 0;
}
.faq-item summary {
  cursor: pointer; font-weight: 700; color: var(--ink); font-size: 16.5px;
  list-style: none; display: flex; justify-content: space-between; gap: 16px; align-items: flex-start;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: ""; flex: none; width: 9px; height: 9px; margin-top: 6px;
  border-right: 2px solid var(--hazard-dark); border-bottom: 2px solid var(--hazard-dark);
  transform: rotate(45deg); transition: transform .2s ease;
}
.faq-item[open] summary::after { transform: rotate(-135deg); }
.faq-answer { margin-top: 12px; }
.faq-answer p { margin: 0 0 14px; }
.faq-answer p:last-child { margin-bottom: 0; }

/* -------- 16. Map ------------------------------------------------------ */
.map { line-height: 0; border: 1px solid var(--rule); padding: 4px; background: var(--white); }
.map iframe { display: block; width: 100%; aspect-ratio: 16 / 10; border: 0; }
.map--large iframe { aspect-ratio: 16 / 9; }

/* On graphite the white mat reads as a pasted-on panel. Same component, footer surface. */
.footer-map .map { background: transparent; border-color: rgba(255, 255, 255, .22); padding: 3px; }

/* -------- 17. Footer --------------------------------------------------- */
.site-footer {
  background: var(--graphite);
  color: rgba(255, 255, 255, .78);
  padding: 50px 0 30px;
  font-size: 14.5px;
}
.site-footer h3 { font-size: 17px; font-weight: 700; color: var(--white); margin-bottom: 16px; }
.site-footer a { color: rgba(255, 255, 255, .88); }
.site-footer a:hover { color: var(--hazard); }
/* B-01 REPAIR (2026-08-16). The rule directly above is (0,1,1) and beat `.btn { color: var(--ink) }`
   at (0,1,0), so the one filled hazard button that lives inside the footer lost its own label
   colour and rendered near-white on yellow at 1.57:1, on all nine pages at every width — while the
   identical button in the header and the closing band measured 8.18:1. A button carries its own
   ground, so the surface rule for footer TEXT must not reach it. These two declarations give the
   component its own colours back and change nothing else about the footer.
   Third occurrence of this pattern: F-01 fixed .tel-link, V-03/V-04 fixed the surface tokens. */
.site-footer .btn { color: var(--ink); }
.site-footer .btn:hover { color: var(--white); }
.site-footer .eyebrow { color: var(--hazard); }
.site-footer ul { list-style: none; margin: 0; padding: 0; }
.site-footer li { margin-bottom: 9px; }
/* The footer contract (ae757c86): this site in "Our locations", marked rather than linked. */
.site-footer .footer-here { color: var(--white); font-weight: 700; display: inline-flex; align-items: center; gap: 10px; }
.site-footer .footer-here::before { content: ""; width: 8px; height: 8px; background: var(--hazard); flex: none; }

/* V-11 REPAIR (2026-08-16). The three columns ran contact | map | nav, and because the 347x220
   map column ended ~180px short of the ~400px contact column and the two compressed nav lists
   ended higher still, the whole bottom-right of the band was empty — the last thing the eye
   reaches on every page. The family reference does not have the problem because it keeps the
   map underneath the address in one "Where we are" column and puts that column last, so the
   tallest block closes the row. Same arrangement here: conversion | navigation | location.
   The ragged edge now falls where a reader has already stopped looking. */
.footer-top { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--gutter); }
.footer-logo { height: 54px; width: auto; margin-bottom: 16px; }
.footer-contact .tel-link { color: var(--white); font-size: 20px; font-weight: 900; }
.footer-contact .tel-link:hover { color: var(--hazard); }
.footer-address { font-style: normal; margin: 0 0 16px; }
/* Opening hours, directly under the premises. Same measure and rhythm as the address it follows,
   because it is read as the second half of the same answer: where, and when. */
.footer-hours { margin: 0 0 16px; font-size: .95rem; }
.footer-cta { margin: 18px 0 0; }
.footer-nav-cols { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; }

.footer-bottom {
  margin-top: 34px; padding-top: 18px;
  border-top: 1px solid rgba(255, 255, 255, .14);
  display: flex; flex-wrap: wrap; gap: 10px 24px; justify-content: space-between; align-items: center;
}
.footer-bottom ul { display: flex; gap: 18px; }
.footer-bottom li { margin: 0; }

/* Family motif: the hazard copy bar closing every page. */
#copy {
  background: var(--hazard);
  color: var(--ink);
  padding: 12px 0;
  text-align: center;
  font-size: 12.5px;
  font-weight: 500;
}

/* -------- 18. Back to top, call bar ------------------------------------ */
.scrollup {
  position: fixed; right: 16px; bottom: 16px; z-index: 9997;
  width: 42px; height: 42px;
  border: 0; border-radius: 2px;
  background: rgba(0, 0, 0, .6);
  color: var(--white);
  cursor: pointer;
  display: none;
  align-items: center; justify-content: center;
}
.scrollup.is-visible { display: flex; }
.scrollup:hover { background: var(--hazard); color: var(--ink); }

#call_bar { display: none; }

/* -------- 19. Prose and 404 -------------------------------------------- */
/* V-08 REPAIR (2026-08-16). .prose carried no measure, so on /privacy/ and /terms/ the
   paragraphs ran the full container: measured 1100px at 15px on /privacy/ at 1280, which is
   136 characters a line against the 66ch this site sets everywhere else. Legal pages are the
   quiet end of the site, not a different site, so they get the site's own measure. */
.prose { max-width: 66ch; }
.prose h2 { margin-top: 34px; }
.prose h3 { margin-top: 26px; }

/* A phone number written into a sentence is still a phone action to a reader. The build wraps
   the one display form wherever approved copy states it (see linkifyPhone in build-site.mjs),
   so there is no rendering of the number on this site that a visitor cannot tap. It is set as
   a prose link, not as a .tel-link, because it sits inside a line of body copy. */
.tel-inline { font-weight: 700; }

.error-page { padding: 90px 0; text-align: center; }
.error-actions { margin-top: 34px; }

/* Utility classes rather than style attributes. The CSP is style-src 'self', so an
   inline style is not a shortcut here — it is a rule the browser refuses to apply. */
.panel-address { margin-top: 20px; }
.spacer { height: 56px; }

/* -------- 20. Scroll reveal -------------------------------------------- */
.js [data-reveal] { opacity: 0; transform: translateY(18px); transition: opacity .5s ease, transform .5s ease; }
.js [data-reveal].is-in { opacity: 1; transform: none; }

/* -------- 21. Responsive ----------------------------------------------- */
@media (max-width: 991px) {
  .hero { min-height: 420px; }
  .steps { grid-template-columns: repeat(2, 1fr); }
  /* Conversion and location take the row; the two nav lists, which are the shortest blocks,
     drop underneath and span it. `order` rather than a DOM change, because the source order
     is the one a screen reader and a keyboard follow. */
  .footer-top { grid-template-columns: 1fr 1fr; }
  .footer-nav-cols { grid-column: 1 / -1; order: 3; }
  .srow { gap: 30px; }
  /* Scoped to .site-header on purpose. The mobile tap-target rule below matches
     a[href^="tel:"], which is MORE specific than a bare .header-phone class and would
     otherwise resurrect the hidden header phone at ≤767px — crowding the logo and the
     menu button on exactly the widths where the sticky call bar already carries the
     number. Two classes beat one class plus an attribute. */
  .site-header .primary-nav, .site-header .header-phone { display: none; }
  .menu-toggle { display: block; }

  /* F-06 REPAIR (2026-08-16). The nav went touch-only at 991 while the sticky call bar and the
     44px tap-target rule were scoped to 767, leaving a 768–991 dead band — iPad portrait, most
     phones in landscape — with collapsed navigation, no persistent phone action and 24px phone
     links. The family reference keeps both on ONE boundary; this now does too.

     The rule: the width at which navigation collapses is the width at which the persistent
     phone path must appear. There is no width where a visitor has neither. */
  #call_bar {
    display: block;
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 9996;
    background: var(--hazard);
    box-shadow: 0 -2px 12px rgba(0, 0, 0, .18);
  }
  #call_bar a {
    display: block; text-align: center;
    padding: 14px 16px;
    color: var(--ink); font-weight: 900; font-size: 16px;
  }
  /* Space for the sticky call bar is reserved ONLY when there is a call bar. A Point that has not
     been given its dedicated number renders no bar, and an unconditional 64px would leave a dead
     strip at the bottom of every mobile page and float the scroll-up button above nothing. */
  body.has-call-bar { padding-bottom: 64px; }
  body.has-call-bar .scrollup { bottom: 74px; }

  /* A tappable number should never be a hairline. The exemptions are the controls that set
     their own height and the two that must not be enlarged: a number inside a line of body
     copy (a 44px line-height would tear the paragraph apart) and the utility-line link, whose
     bar is 30px tall and clips — see the V-12 note on #top_links. */
  a[href^="tel:"] { display: inline-block; min-height: 44px; line-height: 44px; font-weight: 700; }
  #call_bar a, .btn[href^="tel:"], #top_links a, .tel-inline { min-height: 0; line-height: inherit; }
  .tel-inline { display: inline; }
  .btn[href^="tel:"] { line-height: 1.4; }
}

@media (max-width: 767px) {
  :root { --section: 44px; --gutter: 24px; }

  body { font-size: 16px; }

  #top_line { display: none; }

  .brand-logo { height: 42px; }
  .site-header.is-stuck .brand-logo { height: 38px; }
  .footer-logo { height: 48px; }

  h1, h2 { font-size: 25px; }
  h3 { font-size: 20px; }
  .cta-title { font-size: 25px; }

  .hero { min-height: 0; }
  .hero .container { padding-top: 52px; padding-bottom: 52px; }

  .page-head { padding: 34px 0 30px; }
  .page-head h1 { font-size: 22px; }

  .srow, .srow--flip, .split, .split--location, .info-grid,
  .footer-top, .footer-nav-cols { grid-template-columns: 1fr; }
  .call-panel { padding: 24px 22px; }
  .srow--flip .srow-media { order: 0; }
  .steps { grid-template-columns: 1fr; }

  .address-block { padding: 26px 22px; }
  /* Stacked, and still one alignment: the actions keep the left edge the copy above them
     sits on rather than stretching or centring. See the V-06 note. */
  .address-actions { flex-direction: column; align-items: flex-start; gap: 14px; }

  .cta { padding: 54px 0; }
  .cta-title::before, .cta-title::after { width: 20px; margin: 0 10px; }

  .map iframe, .map--large iframe { aspect-ratio: 4 / 3; }

  /* A one-line question is a 26px target for a thumb. The padding that carries the row's
     rhythm moves onto the summary, where it is tappable, so the control reaches 44px without
     the block growing. Same construction as the family reference. */
  .faq-item { padding: 6px 0; }
  .faq-item summary { min-height: 44px; padding: 9px 0; }

  .hero-actions { flex-direction: column; align-items: stretch; }
  .hero-actions .btn { display: block; width: 100%; }
}

@media (max-width: 480px) {
  .hero-copy h1 { font-size: 26px; }
  .address-line { font-size: 24px; }
  /* The call control keeps its two lines but stops stretching the number past the card edge. */
  .hero-call-btn { width: 100%; align-items: center; }
  .hero-call-number { font-size: 19px; }
}

/* -------- 22. Motion and print ----------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  .js [data-reveal] { opacity: 1; transform: none; }
}

@media print {
  .site-header, .site-footer, .scrollup, #call_bar, #copy, .side-panel, #position { display: none; }
  /* Without this, every block still waiting on its IntersectionObserver prints blank. */
  .js [data-reveal] { opacity: 1 !important; transform: none !important; }
  body { color: #000; padding-bottom: 0; }
}

/* ==========================================================================
   23. V2 COMPONENTS — added 2026-08-21, V1 to V2 remediation
   ==========================================================================
   These are the sections the V2 plan asks for and the V1 build had no vocabulary
   for: the four-up service grid, the measured local figures, the process band,
   the price/date columns, and the shared cluster coverage treatment.

   They are written in this site's own language rather than copied from a sibling.
   What is shared with the cluster is the BRAND layer above: the hazard hue, the
   ink, Roboto, the hatching, the card radius. What is this site's own is the
   composition: the address block still leads, the local figures are this
   borough's, and there is no borough switcher in the header.
   ========================================================================== */

/* -------- 23.1 Section heads ------------------------------------------- */
.section--tight { padding-top: 34px; padding-bottom: 34px; }
.section-head { max-width: 62ch; margin: 0 0 38px; }
.section-head h2 { margin-bottom: 10px; }
.section-intro { margin: 0; color: var(--muted); font-size: 1.05rem; }
.section-head--onband h2 { color: var(--white); }
.section-head--onband .section-intro { color: rgba(255, 255, 255, .74); }

/* -------- 23.2 Service cards -------------------------------------------
   Four canonical services, four cards. V1 showed three as alternating rows and
   the fourth as a paragraph on an index page that no longer exists. */
.cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gutter); }
.card {
  display: flex; flex-direction: column;
  background: var(--white);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  color: inherit;
  transition: var(--ease), transform .2s ease;
}
.card:hover, .card:focus-visible { border-color: var(--hazard); transform: translateY(-3px); }
.card-media { display: block; }
.card-media img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; }
.card-body { display: block; padding: 20px 20px 22px; border-top: 3px solid var(--hazard); }
.card-title { display: block; font-size: 1.15rem; font-weight: 700; color: var(--ink); margin-bottom: 8px; }
.card-text { display: block; font-size: .95rem; color: var(--steel); margin-bottom: 14px; }
.card-more {
  display: inline-block; font-size: .82rem; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--hazard-ink);
}
.card-more::after { content: " \2192"; display: inline-block; transition: transform .2s ease; }
.card:hover .card-more::after { transform: translateX(4px); }

/* -------- 23.3 Feature: image beside text ------------------------------ */
.feature { display: grid; grid-template-columns: 1fr 1fr; gap: 46px; align-items: center; }
.feature--flip { grid-template-columns: 1fr 1fr; }
.feature--flip .figure { order: 2; }
.feature .figure { max-width: none; margin: 0; }
.feature .figure img { aspect-ratio: 4 / 3; }
.feature-text h2 { margin-bottom: 14px; }

/* -------- 23.4 Measured local figures ----------------------------------
   Every tile is a borough count from a named source. The source line is part of
   the component rather than an optional extra, because a figure without one is
   exactly what three earlier sessions on this repository refused to publish. */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--gutter); }
.stat {
  background: var(--white);
  border: 1px solid var(--rule);
  border-top: 4px solid var(--hazard);
  border-radius: var(--radius-sm);
  padding: 24px 22px;
}
.stat-figure {
  margin: 0 0 4px;
  font-size: clamp(2.1rem, 3.6vw, 2.9rem);
  font-weight: 900; line-height: 1; letter-spacing: -.02em;
  color: var(--ink);
}
.stat-label { margin: 0 0 12px; font-weight: 700; font-size: .95rem; color: var(--ink); }
.stat-body { margin: 0; font-size: .92rem; color: var(--steel); }
.stats-source { margin: 26px 0 0; font-size: .82rem; color: var(--muted); max-width: 92ch; }

/* -------- 23.5 Process band --------------------------------------------
   The family puts a process on a graphite ground. --hazard-ink is a light-ground
   colour, so both accent tokens are re-pointed once here and every focusable and
   chevron inside inherits the correction. */
.steps-band {
  background: var(--graphite);
  background-image: var(--stripes-light);
  padding: var(--section) 0;
  --link-accent: var(--hazard);
  --link-accent-hover: var(--white);
  --focus: var(--hazard);
}
.steps-band .steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gutter); list-style: none; margin: 0; padding: 0; }
.steps-band .step {
  background: var(--graphite-soft);
  border-top: 3px solid var(--hazard);
  border-radius: var(--radius-sm);
  padding: 22px 20px 24px;
}
.steps-band .step-number {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--hazard); color: var(--graphite);
  font-size: .9rem; font-weight: 900; letter-spacing: 0;
  margin-bottom: 14px;
}
.steps-band .step-title { color: var(--white); font-size: 1.02rem; margin-bottom: 8px; }
.steps-band .step p { color: rgba(255, 255, 255, .78); font-size: .92rem; margin: 0; }

/* -------- 23.6 Price / date columns ------------------------------------ */
.columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 34px var(--gutter); }
.column { border-top: 3px solid var(--hazard); padding-top: 18px; }
.column h3 { font-size: 1.08rem; margin-bottom: 10px; }
.column p { font-size: .95rem; margin-bottom: 14px; }

/* -------- 23.7 Cluster coverage ----------------------------------------
   Every covered area, identical wording and identical prominence. The ONLY
   difference between one tile and another is whether it is a link, and nothing
   in this component draws attention to that. The current locality is not a link
   because a link to the page you are on is a dead control. */
.coverage {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px;
}
.coverage li { display: flex; }
.coverage-item {
  display: flex; align-items: center; gap: 9px;
  width: 100%;
  padding: 16px 18px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--white);
  color: var(--ink);
  font-weight: 700;
  transition: var(--ease);
}
a.coverage-item:hover, a.coverage-item:focus-visible { border-color: var(--hazard); background: var(--hazard-tint); color: var(--ink); }
.coverage-item--here { border-color: var(--hazard); border-left-width: 4px; background: var(--hazard-tint); flex-wrap: wrap; }
.coverage-call { flex-basis: 100%; font-size: 13px; font-weight: 600; color: var(--ink); }
.coverage-pin { width: 18px; height: 18px; flex: 0 0 18px; color: var(--hazard-ink); }
.coverage-name { flex: 1 1 auto; }
.coverage-tag {
  font-size: .68rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--hazard-ink); white-space: nowrap;
}
.coverage-note { margin: 22px 0 0; color: var(--steel); }

/* -------- 23.8 Contact: call panel and location ------------------------ */
.call-panel {
  display: grid; grid-template-columns: 1.1fr 1fr; gap: 0;
  border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden;
}
.call-panel-main { padding: 34px 32px; border-left: 6px solid var(--hazard); }
.call-panel-side { padding: 34px 32px; background: var(--concrete); }
.call-panel-side h2 { font-size: 1.15rem; margin-bottom: 14px; }
.call-number { margin: 0 0 18px; }
.call-number-link {
  font-size: clamp(1.7rem, 3.4vw, 2.4rem);
  font-weight: 900; letter-spacing: -.02em; color: var(--ink);
}
.call-number-link:hover { color: var(--hazard-ink); }
.ready-list { list-style: none; margin: 0; padding: 0; }
.ready-list li { position: relative; padding-left: 24px; margin-bottom: 10px; font-size: .95rem; }
.ready-list li::before {
  content: ""; position: absolute; left: 0; top: .55em;
  width: 10px; height: 10px; background: var(--hazard); border-radius: 2px;
}
.location { display: grid; grid-template-columns: 1fr 1.1fr; gap: 42px; align-items: start; }
.location-address { font-style: normal; font-weight: 700; color: var(--ink); line-height: 1.7; }

/* -------- 23.9 Hero and page-head additions ---------------------------- */
.hero-eyebrow {
  display: inline-block;
  background: var(--hazard); color: var(--ink);
  font-size: .72rem; font-weight: 900; letter-spacing: .16em; text-transform: uppercase;
  padding: 6px 12px; border-radius: 2px;
  margin: 0 0 18px;
}
.pagehead-standfirst { max-width: 62ch; margin: 0; }
.btn--ghost { background: transparent; color: var(--white); border: 2px solid var(--white); }
.btn--ghost:hover, .btn--ghost:focus-visible { background: var(--white); color: var(--ink); border-color: var(--white); }
.error-head { text-align: left; }
.error-head .eyebrow { color: var(--hazard-ink); }

/* -------- 23.10 Responsive --------------------------------------------- */
@media (max-width: 1099px) {
  .cards { grid-template-columns: repeat(2, 1fr); }
  .steps-band .steps { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 991px) {
  .feature, .feature--flip { grid-template-columns: 1fr; gap: 28px; }
  .feature--flip .figure { order: 0; }
  .call-panel { grid-template-columns: 1fr; }
  .location { grid-template-columns: 1fr; gap: 28px; }
}
@media (max-width: 767px) {
  :root { --section: 52px; }
  .cards { grid-template-columns: 1fr; }
  .steps-band .steps { grid-template-columns: 1fr; }
  .coverage { grid-template-columns: 1fr 1fr; }
  .section-head { margin-bottom: 28px; }
  .call-panel-main, .call-panel-side { padding: 26px 22px; }
}
@media (max-width: 479px) {
  .coverage { grid-template-columns: 1fr; }
}

/* -------- 23.11 Alternating ground -------------------------------------
   The section rhythm the three stronger properties in this cluster use: a quiet
   tinted ground under the sections that carry secondary weight, so a long page
   has a beat rather than a single unbroken white column. */
.section--tint { background: var(--concrete); }

/* -------- 23.12 Interior page head -------------------------------------
   REPLACES `.page-head` (removed). V1 opened every interior page with a flat
   graphite bar, which read as a breadcrumb strip with a headline in it and gave
   a service page no visual weight at all. This is the hero mechanic at three
   quarters scale, so the pages beneath the home page look like they belong to
   the same site.

   The `//` motif stays. It is this repository's own mark, it survived the Adova
   retirement on purpose, and it costs one pseudo-element. */
.pagehead {
  position: relative;
  overflow: hidden;
  background: var(--graphite);
  min-height: 340px;
  display: flex; align-items: flex-end;
  --link-accent: var(--hazard);
  --link-accent-hover: var(--white);
  --focus: var(--hazard);
}
.pagehead .hero-media { position: absolute; inset: 0; z-index: 0; }
.pagehead .hero-media img { width: 100%; height: 100%; object-fit: cover; }
.pagehead::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(to bottom, rgba(18, 20, 22, .58) 0%, rgba(18, 20, 22, .84) 72%, rgba(18, 20, 22, .9) 100%);
}
.pagehead .container { position: relative; z-index: 2; padding-top: 64px; padding-bottom: 46px; }
.pagehead-inner { max-width: 780px; color: var(--white); }
.pagehead-inner h1 { color: var(--white); font-size: clamp(1.9rem, 3.6vw, 2.7rem); margin-bottom: 14px; }
.pagehead-inner h1::before { content: "//"; color: var(--hazard); margin-right: 10px; }
.pagehead-inner .pagehead-standfirst { color: rgba(255, 255, 255, .9); font-size: 1.08rem; }

/* -------- 23.13 Statement band -----------------------------------------
   V1's band was a photograph with an optional caption printed underneath it in
   13.5px grey, which is how a statement ends up looking like a photo credit.
   The statement goes ON the band. */
.band { position: relative; display: block; }
.band img { width: 100%; aspect-ratio: 21 / 9; object-fit: cover; display: block; }
.band--statement::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to right, rgba(18, 20, 22, .88) 0%, rgba(18, 20, 22, .74) 38%, rgba(18, 20, 22, .4) 72%, rgba(18, 20, 22, .28) 100%);
}
.band--statement .container {
  position: absolute; inset: 0; z-index: 2;
  display: flex; align-items: center;
  left: 50%; transform: translateX(-50%);
}
.band-inner { max-width: 46ch; color: var(--white); }
.band-title {
  margin: 0 0 12px;
  font-size: clamp(1.4rem, 2.6vw, 2.1rem);
  font-weight: 900; line-height: 1.15; letter-spacing: -.015em;
  color: var(--white);
}
.band-inner p { margin: 0; color: rgba(255, 255, 255, .88); }

/* -------- 23.14 FAQ wrapper -------------------------------------------- */
.faq { max-width: 82ch; border-top: 1px dotted var(--rule); }

/* -------- 23.15 Footer -------------------------------------------------
   Superseded by 23.18, which had to solve the row-height problem at the same
   time. Kept as a pointer rather than as a second, weaker declaration of the
   same grid. */

@media (max-width: 991px) {
  .band--statement .container { position: relative; inset: auto; left: auto; transform: none; }
  .band--statement { background: var(--graphite); }
  .band--statement img { position: absolute; inset: 0; height: 100%; }
  .band--statement .container { padding-top: 46px; padding-bottom: 46px; }
  .pagehead { min-height: 260px; }
}
@media (max-width: 767px) {
  .footer-nav-cols { grid-template-columns: 1fr 1fr; }
  .pagehead .container { padding-top: 44px; padding-bottom: 34px; }
}

/* -------- 23.16 Split heading column -----------------------------------
   The heading column of a `.split` held a two-line heading and then 350px of
   nothing, on nine sections. It now carries a kicker and a hazard rule, so the
   column terminates instead of trailing off, and the kicker states something the
   body does not repeat. */
.split { align-items: start; }
.split > div:first-child { position: relative; padding-bottom: 4px; }
.split > div:first-child::after {
  content: ""; display: block; width: 54px; height: 3px;
  background: var(--hazard); margin-top: 18px;
}
.split-kicker { margin: 14px 0 0; color: var(--muted); font-size: 1rem; max-width: 34ch; }

/* -------- 23.17 FAQ block ----------------------------------------------
   Left-aligned at 82ch, the FAQ left the right half of the page empty under a
   heading that spanned it. Centred, which is what the three stronger properties
   in this cluster do with the same block. */
.faq { max-width: 82ch; margin: 0 auto; border-top: 1px dotted var(--rule); }
.section--faq .section-head { margin-left: auto; margin-right: auto; text-align: center; }
.section--faq .section-head .section-intro { margin-left: auto; margin-right: auto; }

/* -------- 23.18 Footer ------------------------------------------------
   Three problems, all from a grid whose row height was set by a 450px map:
   the conversion column trailed 400px of empty graphite, the three navigation
   columns were too narrow to hold "Building Demolition" on one line, and the
   whole band was taller than the FAQ above it. */
.footer-top {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.55fr) minmax(0, 1fr);
  gap: 40px var(--gutter);
  align-items: start;
}
.footer-nav-cols { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; }
.footer-nav-cols ul { list-style: none; margin: 0; padding: 0; }
.footer-nav-cols li { margin-bottom: 9px; }
.footer-nav-cols a { font-size: .95rem; }
.footer-map .map { margin-top: 16px; }
.footer-map .map iframe { width: 100%; height: 220px; display: block; border: 0; }
.footer-cta { margin-top: 18px; }

/* -------- 23.19 Contained measures -------------------------------------
   Long single paragraphs were running the full 1170px container on the address
   band and in prose. Nothing on this site should ask a reader to track a line
   longer than about 75 characters. */
.address-block p { max-width: 72ch; }
.prose { max-width: 66ch; }   /* the V-08 measure, kept: 78ch overrode a repair */
.stats-source { max-width: 88ch; }

/* -------- 23.20 Hero balance -------------------------------------------
   Two buttons rather than two buttons and a bare link, so they share a baseline
   and a height. */
.hero-actions .btn { min-height: 48px; display: inline-flex; align-items: center; }
.cta-actions { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
.cta-actions .btn { min-height: 48px; display: inline-flex; align-items: center; }

/* -------- 23.21 Responsive corrections, last in the cascade ------------
   These have to sit AFTER section 21's media queries, not inside section 23's,
   because the new footer grid in 23.18 is written later in the file than the
   original responsive rules and would otherwise win at every width. The probe
   caught it as a real 138px horizontal overflow at 390, which is the kind of
   defect that only exists because of source order. */
@media (max-width: 991px) {
  .footer-top { grid-template-columns: 1fr 1fr; gap: 32px var(--gutter); }
  .footer-nav-cols { grid-column: 1 / -1; order: 3; grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 767px) {
  .footer-top { grid-template-columns: 1fr; }
  .footer-nav-cols { grid-template-columns: 1fr 1fr; }
  .footer-map .map iframe { height: 200px; }
}
@media (max-width: 479px) {
  .footer-nav-cols { grid-template-columns: 1fr; gap: 8px; }
}

/* -------- 23.22 The hazard strip ---------------------------------------
   One line, one action, on the brand's own yellow with its hatching. It exists
   because the probe measured 9.7 viewports of the home page at 320 with no way
   to act, and because the sticky call bar does not count: a bar a reader has
   learned to ignore is not a call to action.

   Yellow measures 1.67:1 on white, so it never carries text anywhere else on
   this site. Here it is a large ground with --ink on it, which is 9.4:1. */
.strip {
  background: var(--hazard);
  background-image: var(--stripes);
  padding: 26px 0;
  --link-accent: var(--ink);
  --link-accent-hover: var(--ink);
  --focus: var(--ink);
}
.strip-inner {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 18px 30px;
}
.strip-title {
  margin: 0; color: var(--ink);
  font-size: clamp(1.1rem, 1.9vw, 1.4rem); font-weight: 900; letter-spacing: -.01em;
}
.strip-body { margin: 6px 0 0; color: rgba(52, 51, 51, .82); font-size: .98rem; max-width: 62ch; }
.btn--onstrip {
  background: var(--ink); color: var(--white); border: 2px solid var(--ink);
  min-height: 48px; display: inline-flex; align-items: center; white-space: nowrap;
}
.btn--onstrip:hover, .btn--onstrip:focus-visible { background: var(--white); color: var(--ink); }

/* -------- 23.23 Mobile tap spacing -------------------------------------
   The probe counted 18 to 20 controls under 44px, and most of them are the
   footer link lists: 18px of text with a 9px margin is about 27px of target.
   Ordinary links are not enlarged to 44px across the site, because a 44px
   line-height inside a paragraph tears it apart. A footer link list is not a
   paragraph, so it gets the room. */
@media (max-width: 767px) {
  .footer-nav-cols li { margin-bottom: 4px; }
  .footer-nav-cols a { display: block; padding: 13px 0; line-height: 1.35; }
  .footer-bottom li { margin: 0; }
  .footer-bottom a { display: inline-block; padding: 10px 0; }
  .strip-inner { flex-direction: column; align-items: flex-start; }
  .btn--onstrip { width: 100%; justify-content: center; white-space: normal; }
}

/* -------- 23.24 Tap targets, measured rather than guessed ---------------
   Every control the probe named at under 44px on a touch width, fixed at the
   control rather than with a blanket rule. The blanket rule is deliberately NOT
   used: `a { min-height: 44px }` inside running prose tears a paragraph apart,
   which is why the existing tel: rule exempts `.tel-inline`.

   Reported at 390: a.brand 164x42, a.text-btn 115x23, a 143x19 (the utility
   line and the footer's bottom row), and the coverage tiles at 42px. */
@media (max-width: 991px) {
  .brand { display: inline-flex; align-items: center; min-height: 46px; }
  .coverage-item { min-height: 50px; }
  .text-btn { display: inline-flex; align-items: center; min-height: 44px; }
  .address-actions .text-btn { min-height: 44px; }
  #top_links a { display: inline-flex; align-items: center; min-height: 30px; }
}

/* -------- 23.25 One primary action -------------------------------------
   The footer's conversion button was the last filled-hazard control pointing
   somewhere other than the phone. It is the phone now, and /contact/ keeps its
   place in the header navigation and in the footer's Company column. */
.footer-cta .btn { min-height: 48px; display: inline-flex; align-items: center; }

/* ------------------------------------------------------------- lead form --
   The second conversion path. Styled as a quiet panel, never as a rival to the
   phone: the number keeps the filled hazard button, the form's submit is the
   same button at the same weight but it sits inside its own bordered card, so
   the page still has one obvious primary action per region. */
.lead-form {
  background: var(--white);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--hazard);
  padding: 1rem 1rem .85rem;
  /* So .lead-row can ask how wide THIS card is. Where container queries are unsupported the row
     simply stays one column, which is the narrow layout and is never broken. */
  container-type: inline-size;
}
.lead-form-heading { font-size: 1.05rem; margin: 0 0 .7rem; }
.lead-form-lede { color: var(--steel); font-size: .92rem; margin: -.45rem 0 .9rem; }

/* Name and phone share a row wherever the card is wide enough for two usable inputs. 24rem is
   the card width at which each half still holds a formatted phone number without clipping, not a
   viewport breakpoint: the hero form and the contact form are different widths at the same
   viewport, so this has to answer to the container. The hero card is 27rem and pairs; a 375px
   phone gives the card about 21rem and it stays one column. */
.lead-row { display: grid; grid-template-columns: 1fr; gap: 0 .6rem; }
@container (min-width: 24rem) { .lead-row { grid-template-columns: 1fr 1fr; } }

.lead-field { display: flex; flex-direction: column; gap: .25rem; margin: 0 0 .6rem; }
.lead-field label { font-size: .85rem; font-weight: 600; color: var(--graphite-soft); }
.lead-optional { font-weight: 400; color: var(--muted); }
.lead-field input,
.lead-field textarea {
  font: inherit; font-size: .95rem; color: var(--ink);
  background: var(--white);
  border: 1px solid var(--rule);
  padding: .55rem .65rem;
  width: 100%;
  /* Without this a padded 100% input overflows its card on narrow screens. */
  box-sizing: border-box;
}
.lead-field textarea { resize: vertical; min-height: 3.1rem; }
.lead-field input:focus-visible,
.lead-field textarea:focus-visible {
  outline: 2px solid var(--hazard-ink);
  outline-offset: 1px;
  border-color: var(--hazard-ink);
}

.lead-actions { margin: .7rem 0 0; }
.lead-actions .btn { width: 100%; text-align: center; }
.lead-actions .btn[disabled] { opacity: .65; cursor: default; }

/* The submit control, withheld where it cannot work. The served CSP ends form-action 'none', so
   with scripting unavailable this form's native POST is blocked by the browser and the button does
   nothing at all — no navigation, no error, no message. The <noscript> block above it carries the
   phone instead. `.js` is added to <html> by the boot script before first paint, so this selector
   is precisely "scripting did not run". */
html:not(.js) .lead-actions { display: none; }
.lead-noscript { margin: .7rem 0 0; font-size: .9rem; color: var(--ink); }
.lead-noscript a { font-weight: 700; }

/* Empty until something is said, so it takes no vertical space on load. */
.lead-status { margin: .6rem 0 0; font-size: .9rem; }
.lead-status:empty { display: none; }
.lead-status[data-state="ok"] { color: #1c6b3a; font-weight: 600; }
.lead-status[data-state="error"] { color: #a3271c; font-weight: 600; }
.lead-status[data-state="busy"] { color: var(--steel); }

.lead-consent { margin: .6rem 0 0; font-size: .8rem; color: var(--muted); }

/* Off-screen rather than display:none — some bots skip hidden inputs, and this
   one is meant to be filled. aria-hidden and tabindex -1 on the markup keep it
   out of the accessibility tree and the tab order. */
.lead-hp {
  position: absolute;
  left: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}

.hero-form { margin-top: 0; max-width: 30rem; }
.call-panel-form { margin-top: 1.5rem; }

@media (min-width: 60rem) {
  /* The two columns. The copy gets the wider half so the H1 keeps a sensible measure, and the
     form is capped rather than allowed to grow with the viewport, which is what stops it reading
     as the subject of the page on a wide monitor. */
  .hero-grid { grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); gap: 2.75rem; }
  .hero .container { padding-top: 44px; padding-bottom: 44px; }
  .hero-form { max-width: 27rem; justify-self: end; width: 100%; }
  .hero-copy h1 { margin-bottom: 14px; }
  /* The contact panel is a two-column grid; the form spans it rather than
     squeezing into one column beside the ready-list. */
  .call-panel-form { grid-column: 1 / -1; }
}

/* ==========================================================================
   24. THE REFINEMENT PASS — 2026-08-25
   ==========================================================================
   The V2 remediation left this site correct, organised and evenly spaced, and a
   reader could not tell that anybody had designed it. Every gate was green on a
   page that was flat: one shadow in the whole stylesheet, one 18px fade applied
   to every block alike, a type scale whose largest and smallest voices were
   fourteen pixels apart, and imagery presented at the size that fitted rather
   than at the size it was worth.

   Nothing below changes what the site says. The composition, the copy, the
   routes, the facts and the three protected contracts are the ones this
   repository already decided. What changes is weight, depth, pacing and
   movement.

   THE MOTIF IS THE CUT. The logo is a hazard square with a diagonal slice taken
   out of it, and the process band's hatching runs at 135 degrees. That diagonal
   is the only shape language this brand owns and until now it appeared in
   exactly two places. It is now the site's: rules terminate on a diagonal, the
   hatching drifts along its own axis, cards lift up and to the right rather
   than straight up, and blocks arrive on the same angle. One idea, applied
   where it earns its place, rather than one effect per section.

   WHAT WAS DELIBERATELY NOT DONE. The hero keeps no min-height: the 2026-08-22
   rebuild removed a 480px floor so the address band breaks the fold on a
   1280x800 laptop, and putting presence back as height would undo a repair that
   was measured. The empty lower half of a `.split` heading column is left
   empty: 23.16 already decided that column terminates on a rule rather than
   trailing off, which is an editorial sidehead and not a gutter, and the rule
   is now drawn rather than merely present.
   ========================================================================== */

/* -------- 24.1 Tokens the pass needs -----------------------------------
   The stylesheet had exactly one shadow, `--shadow-sticky`, and used it once.
   A site with no elevation scale has no way to say that one surface sits above
   another, so every card, tile and panel read as the same piece of paper.

   The shadows are warm rather than neutral black. This brand is a warm yellow
   on a warm graphite, and a pure-black shadow under a warm surface reads as
   dirt. */
:root {
  --lift-1: 0 1px 2px rgba(28, 24, 16, .05), 0 2px 8px -3px rgba(28, 24, 16, .09);
  --lift-2: 0 2px 5px rgba(28, 24, 16, .06), 0 12px 26px -8px rgba(28, 24, 16, .16);
  --lift-3: 0 4px 10px rgba(28, 24, 16, .07), 0 26px 52px -14px rgba(28, 24, 16, .26);

  --ease-out: cubic-bezier(.22, .61, .36, 1);

  /* One period of the 135deg / 28px hatching measured along the x axis:
     28 / cos(45deg). Translating the pattern by exactly this lands it on
     pixel-identical content, which is what makes the drift seamless. */
  --hatch-step: 39.598px;

  /* V1 set --muted to #7b7f81. Measured: 4.04:1 on white and 3.84:1 on the
     tinted ground it most often appears over, so it failed AA on BOTH, and it
     carries section ledes, the split kicker, the statistics source line and the
     lead form's consent line — all of them ordinary-size running text. #6f7375
     measures 4.79:1 on white and 4.55:1 on --concrete, passes on both, and is
     still visibly the lightest of the three ink tones. Every use of this token
     is on a light ground; the one dark-band case re-points at the component. */
  --muted: #6f7375;
}

/* -------- 24.2 The type crescendo --------------------------------------
   Every display voice on the site sat between 30px and 46px, so a page had no
   loudest moment and the eye had no reason to stop anywhere. The range is now
   wide enough to rank: the hero and the interior page head lead, the section
   headings follow, and the measured figures — the only numbers this site is
   allowed to publish — are the largest type on the page, because they are the
   argument. */
.hero-copy h1 { font-size: clamp(30px, 4vw, 48px); line-height: 1.03; letter-spacing: -.028em; }
.pagehead-inner h1 { font-size: clamp(2rem, 3.9vw, 3rem); line-height: 1.06; letter-spacing: -.026em; }
h1, h2, .band-title, .cta-title, .strip-title { text-wrap: balance; }
h2 { font-size: clamp(1.6rem, 2.9vw, 2.5rem); }
.band-title { font-size: clamp(1.55rem, 3vw, 2.5rem); }
.cta-title { font-size: clamp(1.6rem, 3vw, 2.4rem); }
.strip-title { font-size: clamp(1.15rem, 2.1vw, 1.6rem); }

/* A lede is read, so it takes the readable ink rather than the quietest one. */
.section-intro { color: var(--steel); font-size: 1.06rem; }

/* -------- 24.3 The hero ------------------------------------------------
   Two defects, one composition and one legibility.

   The mask was a single flat `rgba(18,20,22,.74)` over the whole frame, which
   is the cheapest way to make a photograph legible and also the way that throws
   the photograph away: an even veil removes the depth that made the image worth
   commissioning. The scrim is now directional. It is heaviest under the copy
   column, clears across the middle so the demolition in the frame is actually
   visible, and closes again at the bottom edge so the section ends rather than
   stops.

   Contrast was measured on the built page rather than assumed, at the H1's own
   pixels, and is recorded in the session report. The mask under the copy is
   HEAVIER than the flat .74 it replaces, so the directional treatment buys the
   photograph back without spending any legibility. */
.hero::after {
  background:
    linear-gradient(180deg, rgba(12, 14, 16, .58) 0%, rgba(12, 14, 16, .30) 30%, rgba(12, 14, 16, .38) 70%, rgba(12, 14, 16, .74) 100%),
    rgba(14, 16, 18, .66);
}

@media (min-width: 60rem) {
  .hero::after {
    background:
      linear-gradient(102deg, rgba(11, 13, 15, .93) 0%, rgba(11, 13, 15, .86) 30%, rgba(11, 13, 15, .58) 58%, rgba(11, 13, 15, .44) 100%),
      linear-gradient(180deg, rgba(11, 13, 15, .40) 0%, rgba(11, 13, 15, 0) 26%, rgba(11, 13, 15, 0) 58%, rgba(11, 13, 15, .58) 100%);
  }
}

/* THE HOME HERO IS DELIBERATELY NOT GIVEN A KEN BURNS DRIFT, and the reason is
   that it already moves. `hero()` in the build emits a muted looping <video>
   behind a poster, and site.js attaches the 1.2MB clip after load for every
   reader who has not asked for less motion and is not on a metered connection.
   Scaling that clip under its own playback would be two motions arguing, and
   the reader who does NOT get the clip is precisely the reader who told us they
   did not want movement. The hero was the one section of this site that was
   already alive, and the first draft of this pass nearly broke it.

   The NINE INTERIOR PAGE HEADS are the ones that were dead: 23.12 rebuilt them
   as the hero at three quarters scale and then handed each one a still
   photograph. They get the drift. Six per cent of scale across 34 seconds is
   below the rate at which movement is seen as movement, so the band never
   appears to animate; a reader who scrolls back up simply finds the frame has
   opened out. Faster than this and it is a slideshow effect, which is worse
   than a still. */
@keyframes pagehead-drift {
  from { transform: scale(1.015); }
  to   { transform: scale(1.075) translate3d(-1%, -.8%, 0); }
}
.pagehead .hero-media img {
  animation: pagehead-drift 34s var(--ease-out) infinite alternate;
  will-change: transform;
}

/* The estimate card was a white rectangle with square corners sitting flat on a
   photograph, which is the one place on the site where a surface genuinely IS
   above another and the stylesheet had no way to say so. It is now lifted.

   This is also the answer to the probe's "two different actions are styled as
   primary". The call button and the submit control are deliberately the same
   weight — 23.20 and the lead-form note both argue it — and that argument
   depends on the form reading as its own region rather than as a second button
   loose in the hero. The elevation is what makes the region real. */
.lead-form { border-radius: var(--radius); border-top-width: 4px; box-shadow: var(--lift-1); }
.hero-form .lead-form { box-shadow: var(--lift-3); }

/* The contact page's copy of the form is NOT a card, because it is already
   inside one. `.call-panel` is a bordered, rounded, clipped panel and the form
   spans its full width at the foot of it, so giving the form its own radius and
   its own shadow produced a card floating a few pixels inside another card with
   a sliver of panel showing all the way round it. Here the form is a band: it
   goes edge to edge, keeps only the hazard rule that divides it from the panel
   content above, and lets the panel own the corners. Same component, two
   surfaces, and the surface decides. */
.call-panel .lead-form {
  box-shadow: none;
  border-radius: 0;
  border-left: 0; border-right: 0; border-bottom: 0;
  /* 38px, not 32px. `.call-panel-main` carries a 6px hazard border AND 32px of
     padding, so its copy starts 38px in from the panel's content edge. The form
     is directly underneath it and its labels have to start on the same line, or
     the panel has two left margins six pixels apart. */
  padding-left: 38px; padding-right: 32px;
}
.call-panel-form { margin-top: 0; }
@media (min-width: 60rem) {
  .hero-form .lead-form { padding: 1.35rem 1.35rem 1.15rem; }
}
.lead-field input,
.lead-field textarea { border-radius: 2px; transition: border-color .18s ease, box-shadow .18s ease; }
.lead-field input:hover,
.lead-field textarea:hover { border-color: #c9c9c6; }

/* -------- 24.4 The cut -------------------------------------------------
   The hazard rule appears under a sidehead, over a statistic and above a
   column. It was a plain bar in all three places. It now ends on the logo's own
   diagonal and it is DRAWN rather than found already there: the bar scales out
   from its left edge when the block arrives, which is the pass's quietest and
   most-repeated motion moment and the one that ties the three components to
   each other.

   `.is-in` is the class the existing IntersectionObserver already adds, so this
   costs no JavaScript and inherits the reduced-motion path unchanged. */
.split > div:first-child::after,
.column::before,
.stat::before {
  clip-path: polygon(0 0, 100% 0, calc(100% - 4px) 100%, 0 100%);
  transform-origin: left center;
  transition: transform .78s var(--ease-out) .12s;
}
/* THE REST STATE IS SCOPED TO `.js`, AND THAT IS THE WHOLE SAFETY ARGUMENT.
   An unscoped `scaleX(0)` is a rule that is drawn by a script and therefore a
   rule that is ABSENT for a reader whose script never ran: the bar is the
   component's only hazard mark, so the tiles, the columns and the sideheads
   would all lose their accent silently and nothing would report it. `.js` is
   added to <html> by the boot script before first paint, so this says exactly
   "hide it only once something is certainly able to show it again", which is
   the same contract `[data-reveal]` already keeps two sections above. */
.js .split > div:first-child::after,
.js .column::before,
.js .stat::before { transform: scaleX(0); }

.js .split > div:first-child.is-in::after,
.js .column.is-in::before,
.js .stat.is-in::before { transform: scaleX(1); }

.split > div:first-child::after { width: 72px; height: 4px; }

/* The columns carried a full-bleed 3px rule each. Four full-width hairlines in
   a row is a table, not a set of columns, and it was one more instance of the
   uniformity this pass is correcting. The rule becomes the sidehead's mark. */
.column { position: relative; border-top-color: transparent; }
.column::before {
  content: ""; position: absolute; top: -3px; left: 0;
  width: 64px; height: 3px; background: var(--hazard);
}

/* -------- 24.5 The service cards ---------------------------------------
   Four defects, all of them visible in one screenshot at 1440.

   The "MORE ON <SERVICE>" link broke across two lines on the two longest names
   and left the arrow ALONE on the second line, and because the link sat
   directly after the description rather than at the foot of the card, the four
   calls to action landed at four different heights. Both are fixed structurally
   rather than by shortening anything: the body is a column that grows, the link
   is pushed to its bottom, and a non-breaking space glues the arrow to the last
   word so it can never orphan again.

   The cards themselves had a 1px border and nothing else. They now sit above
   the page, the photograph moves under the crop on hover, and the whole card
   travels up and to the right along the brand's diagonal. */
.card { box-shadow: var(--lift-1); transition: transform .34s var(--ease-out), box-shadow .34s var(--ease-out), border-color .2s ease; }
.card:hover, .card:focus-visible { transform: translate3d(3px, -7px, 0); box-shadow: var(--lift-3); }
.card-media { overflow: hidden; }
.card-media img { transition: transform .66s var(--ease-out); }
.card:hover .card-media img,
.card:focus-visible .card-media img { transform: scale(1.06); }
.card-body { display: flex; flex-direction: column; flex: 1 1 auto; }
.card-title { transition: color .2s ease; }
.card:hover .card-title,
.card:focus-visible .card-title { color: var(--hazard-ink); }
/* The arrow orphaned onto a line of its own under the two longest service names.
   A non-breaking space alone did not fix it: 23.2 draws the arrow as an
   `inline-block`, and line breaking is allowed BEFORE an inline-block box no
   matter what character starts it. The arrow goes back to being an ordinary
   inline box glued on with a non-breaking space, which has no break opportunity
   in front of it, and the hover travel moves to `margin-left` - the same
   mechanic `.text-btn` has used since the Adova retirement, so the site now has
   one chevron-travel implementation instead of two. */
.card-more { margin-top: auto; font-size: .75rem; letter-spacing: .03em; }
.card-more::after {
  content: "\00a0\2192";
  display: inline;
  transform: none;
  transition: margin-left .24s var(--ease-out);
}
.card:hover .card-more::after,
.card:focus-visible .card-more::after { transform: none; margin-left: 5px; }

/* -------- 24.6 The measured figures ------------------------------------
   These three numbers are the only quantities this site is permitted to
   publish and they were set at 46px, smaller than a section heading. They are
   now the largest type on the page, in tabular figures so the digits align
   between tiles, and each tile's hazard rule draws itself in on arrival.

   Nothing here counts up. An animated counter is the house style of the trust
   badge, DESIGN-DNA section 5 refuses counters on this site, and these are
   borough counts from the Rent Guidelines Board rather than a record of this
   business's work. The figure is simply printed, at the size it deserves. */
/* NO `overflow: hidden` here. The drawn bar is positioned at top:-4px so that it
   paints OVER the tile's own border rather than under it, and a clipped tile
   removes it entirely — the first build of this pass shipped three tiles whose
   rule never arrived and whose pale placeholder was the only thing on screen. */
.stat { position: relative; box-shadow: var(--lift-1); padding: 28px 24px 26px; border-top-color: var(--hazard-line); }
.stat::before {
  content: ""; position: absolute; top: -4px; left: 0; right: 0;
  height: 4px; background: var(--hazard);
}
.stat-figure {
  font-size: clamp(2.6rem, 5vw, 4rem);
  letter-spacing: -.035em;
  font-variant-numeric: tabular-nums;
  margin-bottom: 8px;
}

/* -------- 24.7 The process band ----------------------------------------
   Four boxes in a row do not read as four stages in an order. On the widths
   where all four share a line the numbers are now joined by a dashed hazard run
   through the gutter, so the band states a sequence rather than a set.

   The offset is the centre line of the numbered disc: a 3px border, 22px of
   padding and half of the 30px disc, with the disc's line box pinned to 1 so
   the inherited 1.65 cannot move it. */
.steps-band .step { position: relative; background-image: linear-gradient(180deg, rgba(255, 255, 255, .045) 0%, rgba(255, 255, 255, 0) 60%); }
.steps-band .step-number { line-height: 1; }

@media (min-width: 1100px) {
  .steps-band .step + .step::before {
    content: "";
    position: absolute; top: 37px; left: calc(var(--gutter) * -1);
    width: var(--gutter); height: 2px;
    background-image: repeating-linear-gradient(90deg, var(--hazard) 0 6px, transparent 6px 11px);
    opacity: .8;
  }
}

/* -------- 24.8 The hazard strip, which now moves -----------------------
   23.22 put this band on the page because the probe measured whole viewports of
   the home page with no way to act. It did its job and then sat perfectly
   still, which on a page with one flat fade is the difference between a brand
   that looks maintained and one that looks generated.

   The hatching drifts along its own 135 degree axis at about eight pixels a
   second. It is the brand's own mark moving in the brand's own direction, it is
   the only continuous motion on the page below the fold, and it stops for a
   reader who has asked for less.

   Seamless by construction: the layer is one hatch period wider than the band
   and travels exactly one period, so the frame it ends on is pixel-identical to
   the frame it started on and there is no reset to see. */
.strip { position: relative; overflow: hidden; background-image: none; }
.strip > .container { position: relative; z-index: 1; }
.strip::before {
  content: ""; position: absolute; top: 0; bottom: 0;
  left: calc(var(--hatch-step) * -1);
  width: calc(100% + var(--hatch-step));
  background-image: var(--stripes);
  animation: hatch-drift 5s linear infinite;
  pointer-events: none;
}
@keyframes hatch-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(var(--hatch-step), 0, 0); }
}

/* -------- 24.9 The questions -------------------------------------------
   Four dotted hairlines centred in a column of white is a table of contents.
   The block is a set of controls and now looks like one: a closed question is a
   quiet tinted row, the open one lifts to white behind a hazard border, and the
   answer opens rather than appears.

   Still `<details>` and `<summary>`. The disclosure semantics, the keyboard
   handling and the announced state all remain the element's, which is the whole
   reason 15 chose it. The height transition is progressive: where
   `interpolate-size` is unsupported the panel simply opens instantly, which is
   the behaviour every reader has today. */
.faq { border-top: 0; }
.faq-item {
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--concrete);
  padding: 2px 20px;
  margin-bottom: 10px;
  transition: border-color .2s ease, background-color .2s ease, box-shadow .3s var(--ease-out);
}
.faq-item:hover { border-color: var(--hazard-line); background: var(--white); box-shadow: var(--lift-1); }
.faq-item[open] { border-color: var(--hazard); background: var(--white); box-shadow: var(--lift-2); }
.faq-item summary { padding: 17px 0; font-size: 1.03rem; }
.faq-item summary::after { width: 10px; height: 10px; margin-top: 7px; transition: transform .3s var(--ease-out); }
.faq-answer { padding-bottom: 20px; }

@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .faq-item::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size .34s var(--ease-out), content-visibility .34s allow-discrete;
  }
  .faq-item[open]::details-content { block-size: auto; }
}

/* -------- 24.10 Coverage, the address block and the map ----------------
   The coverage row is the cluster's shared treatment and section 4 of
   DESIGN-DNA governs it: identical wording, identical prominence, and the only
   difference between one tile and another is whether it is a link. That holds.
   What changes is that all five tiles gain the same presence, and the two
   states a pointer can tell apart, link and current page, stay exactly as
   distinguishable as they were. */
.coverage-item { box-shadow: var(--lift-1); transition: transform .26s var(--ease-out), box-shadow .26s var(--ease-out), border-color .2s ease, background-color .2s ease; }
a.coverage-item:hover, a.coverage-item:focus-visible { transform: translate3d(2px, -4px, 0); box-shadow: var(--lift-2); }
.coverage-pin { transition: transform .26s var(--ease-out); }
a.coverage-item:hover .coverage-pin { transform: translateY(-2px) scale(1.1); }

/* The site's organising argument, and until now the flattest block on the page:
   a tinted rectangle with a hazard edge. It now sits on the page rather than in
   it, and the address itself is set at the scale the argument deserves. */
.address-block { position: relative; box-shadow: var(--lift-1); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.address-line { letter-spacing: -.025em; font-size: clamp(28px, 4.2vw, 42px); }

.map { border-radius: var(--radius-sm); overflow: hidden; background: var(--concrete); }
.footer-map .map { background: rgba(255, 255, 255, .04); }

/* -------- 24.11 Controls -----------------------------------------------
   A button that changes colour on hover and nothing else reads as a link with a
   background. These press. */
.btn { box-shadow: 0 1px 2px rgba(28, 24, 16, .12); transition: var(--ease), transform .2s var(--ease-out); }
.btn:hover { transform: translateY(-2px); box-shadow: var(--lift-2); }
.btn:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(28, 24, 16, .12); }
.btn--ghost { box-shadow: none; }

/* The stuck header gains the brand's own hairline rather than only a shadow, so
   the moment it detaches from the top of the document is stated in the brand's
   colour instead of in grey. */
.site-header { transition: box-shadow .3s var(--ease-out); }
.site-header.is-stuck { box-shadow: 0 2px 0 -1px var(--hazard), var(--shadow-sticky); }

/* -------- 24.11b The sidehead's own breakpoint -------------------------
   `.split` collapsed to one column at 767, which left the 768 to 991 band —
   iPad portrait, and most phones in landscape — running a two-column editorial
   split whose heading column is only about 210 to 350px wide. A display heading
   at 22px breaks over three or four lines in a column that narrow while the
   body beside it reads normally, which is the shape of a sidehead that has run
   out of room rather than a sidehead.

   Same boundary the navigation and the persistent phone path already share
   (F-06, 2026-08-16). Above it there is room for two columns; below it the
   heading spans and the rule sits under it, which is the ordinary editorial
   stack and is what every width under 768 already did. */
@media (max-width: 991px) {
  .split, .split--location { grid-template-columns: 1fr; gap: 22px; }
  .split > div:first-child::after { margin-top: 14px; }
}

/* -------- 24.12 Section rhythm -----------------------------------------
   Every section on the site was 72px top and bottom, so the page had a metre
   and no emphasis. The two bands that are meant to land harder get more room
   than the sections that merely continue. */
.steps-band { padding: calc(var(--section) * 1.15) 0; }
.cta { padding: calc(var(--section) * 1.25) 0; }

/* -------- 24.13 The motion vocabulary ----------------------------------
   One fade of 18px, on everything, at the same speed, is the reveal a page gets
   when nobody chose one. Four families now arrive differently, and the
   difference says what kind of thing is arriving:

     a heading                rises a little, quickly, and gets out of the way
     a card, tile or step     arrives along the cut, up and to the right
     a photograph             rises and settles out of a very slight scale
     the statement band       enters from the left, with its own scrim

   Everything still uses `[data-reveal]` and the observer that already existed.
   No new script, no new dependency, and one implementation to keep honest. */
.js [data-reveal] {
  transform: translate3d(0, 20px, 0);
  transition: opacity .62s var(--ease-out), transform .62s var(--ease-out);
}
.js .section-head[data-reveal] {
  transform: translate3d(0, 12px, 0);
  transition-duration: .46s;
}
.js .card[data-reveal],
.js .stat[data-reveal],
.js .step[data-reveal],
.js .column[data-reveal],
.js .faq-item[data-reveal],
.js .coverage[data-reveal] { transform: translate3d(-10px, 26px, 0); }
.js .figure[data-reveal] { transform: translate3d(0, 24px, 0) scale(.985); }
.js .band-inner[data-reveal] {
  transform: translate3d(-26px, 0, 0);
  transition-duration: .8s;
}
.js [data-reveal].is-in { transform: none; }

/* AND THE ONE COLLISION THAT CREATES, RESTATED RATHER THAN LEFT TO LUCK.
   `.js [data-reveal].is-in` is (0,3,0). A card's hover lift is written at
   `.card:hover`, which is (0,2,0). The card is itself a reveal target, so the
   finished reveal outranks the lift on every card that has arrived — which is
   every card a reader can actually point at — and the hover does nothing at
   all while looking perfectly correct in the source. It was dead for one build
   of this pass and only a measured `getComputedStyle` on a focused card found
   it. Declared here at a weight that wins, next to the rule it loses to. */
.js .card[data-reveal].is-in:hover,
.js .card[data-reveal].is-in:focus-visible { transform: translate3d(3px, -7px, 0); }

/* -------- 24.14 The reduced-motion path, restated ----------------------
   THIS BLOCK IS LOAD-BEARING AND MUST STAY LAST.

   Section 22 already resets `[data-reveal]` under `prefers-reduced-motion`, but
   every transform above is declared later in the cascade at equal or higher
   specificity and would beat it. Without this, a reader who has asked for less
   motion gets a page permanently displaced by 26px on the diagonal, and the
   render probe captures with the preference emulated, so the screenshots that
   are supposed to prove the design would be screenshots of the defect.

   `transform: none` is the identity transform, which is exactly what the drawn
   rules want: a bar that never animates is a bar at full width. */
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal],
  .js .section-head[data-reveal],
  .js .card[data-reveal],
  .js .stat[data-reveal],
  .js .step[data-reveal],
  .js .column[data-reveal],
  .js .faq-item[data-reveal],
  .js .coverage[data-reveal],
  .js .figure[data-reveal],
  .js .band-inner[data-reveal] { opacity: 1 !important; transform: none !important; }

  .js .split > div:first-child::after,
  .js .column::before,
  .js .stat::before { transform: none !important; }

  .pagehead .hero-media img,
  .strip::before { animation: none !important; transform: none !important; }
}

/* ==========================================================================
   25. THE SERVICES SHOWCASE — SiteKit animated-carousel-v1, 2026-08-25
   ==========================================================================
   Declared in data/site-plan.json at project.ui_contracts.services_showcase on
   explicit operator instruction. Until then this repository was a legacy
   candidate: four services in a static grid and no contract, which
   `preflight --post-refine` correctly failed as migration debt.

   WHAT THIS FILE OWNS AND WHAT IT MUST NOT TOUCH. The contract splits cleanly.
   SiteKit's behaviours.js owns the loop, the clone sets on both sides, the
   arrows and every hold on automatic motion. This stylesheet owns the card, how
   many cards a width shows, and the drift speed is one attribute in the markup.
   Re-implementing any of the mechanics here would produce guarantees nothing can
   check, which is the entire reason the contract exists.

   THE ONE THING THAT DECIDES WHETHER IT MOVES AT ALL IS IN THIS FILE.
   `behaviours.js` measures the ORIGINAL cards, not the cloned track:

       var overflows = function () { return setWidth() - track.clientWidth > 1; };

   A set that already fits gets no clones, no controls and NO MOTION, on purpose,
   because a showcase whose cards all fit has nothing to scroll and arrows that
   cannot act read as broken. The previous grid put all four cards inside the
   1170px container exactly. Ported at that width the carousel would have been
   correct, contract-compliant, and permanently still. So the card widths below
   are chosen so that four cards OVERFLOW the track at every width, and the
   comment on each breakpoint is the arithmetic rather than a preference.

   HOVER IS NOT A PAUSE, and that is deliberate rather than an omission. It has
   been the contract since 2026-08-24 and it is what the operator asked for on
   2026-08-25. On a marketing page the pointer rests wherever the reader left it,
   very often over the widest band on the page, so hover-to-pause left the
   showcase frozen through most of a visit and moving only when the mouse
   happened to leave. What still holds the drift, all of it in behaviours.js and
   none of it optional: reduced motion, focus inside the component, a horizontal
   drag or swipe, a hidden tab, and the section being off screen. Each of those
   is a reader genuinely engaged. A resting pointer is not.
   ========================================================================== */

.showcase { position: relative; }

/* -------- 25.1 The controls --------------------------------------------
   Inside [data-carousel], because that is where the contract looks for them.
   Real buttons with real accessible names: an icon-only control that only
   answers a click is a control for a pointer and for nobody else.

   Graphite, not hazard. The band already carries four photographs and the page
   has exactly one filled-hazard primary action per region; a pair of yellow
   arrows beside the service cards would compete with the call button for the
   same attention and win, which is the wrong outcome for a chooser. */
.showcase-nav {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin: -14px 0 18px;
}
.cbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--white);
  color: var(--ink);
  font-size: 17px; line-height: 1;
  cursor: pointer;
  box-shadow: var(--lift-1);
  transition: background-color .2s ease, color .2s ease, border-color .2s ease,
              transform .22s var(--ease-out), box-shadow .22s var(--ease-out);
}
.cbtn:hover { background: var(--hazard); border-color: var(--hazard); color: var(--ink); box-shadow: var(--lift-2); }
/* The travel matches the direction the control moves the track, so the button
   states which way it is about to go before it is pressed. */
.cbtn[data-carousel-prev]:hover { transform: translateX(-2px); }
.cbtn[data-carousel-next]:hover { transform: translateX(2px); }
.cbtn:active { transform: translateY(1px); box-shadow: var(--lift-1); }

/* -------- 25.2 The track ------------------------------------------------
   safeguards.css already makes this a native horizontal scroll container and
   already exempts an autoplaying track from scroll snapping for its whole life.
   Neither is restated here. What is added is spacing, and room for a shadow.

   `overflow-x: auto` computes `overflow-y` to `auto` as well, so a card that
   lifts on hover is clipped by its own track unless the track is padded. The
   negative inline margin puts the cards back on the container's own edge, so
   the row still starts where every other section on the page starts. */
.showcase-track {
  gap: var(--gutter);
  margin: 0 -6px;
  padding: 8px 6px 24px;
  list-style: none;
  /* The arrows are the affordance here, and a scrollbar under a track that is
     always drifting is a control that appears to move on its own. Scrolling by
     finger, trackpad and keyboard is untouched. */
  scrollbar-width: none;
}
.showcase-track::-webkit-scrollbar { display: none; }

/* The card fills its slot, and the slots stretch to the tallest card so the
   four service names sit on one line across the row however the copy falls. */
.showcase-item { display: flex; }
.showcase-item > .card { width: 100%; }

/* -------- 25.3 How many cards a width shows ----------------------------
   Each value is chosen so that FOUR cards are wider than the track, which is
   what keeps the loop alive. The arithmetic is the container width W and a
   30px gutter, four cards: 4 x basis + 3 x 30 must exceed W.

   Percentages resolve against the track, so anything at or above 26% overflows
   on its own. Every value below clears that with room, and the widest
   breakpoint is capped in pixels so a card cannot grow unreadably wide on a
   large monitor. */
.showcase-item { flex: 0 0 84%; }                    /* 4 x 84%  = 336% of W */

@media (min-width: 560px) { .showcase-item { flex-basis: 62%; } }   /* 248% of W */
@media (min-width: 768px) { .showcase-item { flex-basis: 46%; } }   /* 184% of W */
@media (min-width: 992px) { .showcase-item { flex-basis: 40%; } }   /* 160% of W */

@media (min-width: 1200px) {
  /* 29% of the 1170 container is 339px, so the set measures
     4 x 339 + 3 x 30 + 30 = 1476 against a 1170 track: 306px of overflow, which
     is most of a card pitch of real travel. Roughly 3.2 cards are visible, which
     is also the composition that tells a reader the row continues. */
  .showcase-item { flex-basis: clamp(288px, 29%, 342px); }
}

/* -------- 25.4 The pace -------------------------------------------------
   `data-carousel-speed="30"` in the markup, in pixels per second, inside the
   behaviour's 10 to 70 clamp.

   The card pitch at the widest breakpoint is 339px plus a 30px gutter, so a card
   takes 369 / 30 = 12.3 seconds to advance one place. That sits in the eight to
   fourteen seconds the stage asks for: fast enough that the movement is read as
   movement rather than as a rendering fault, slow enough that nobody is chasing
   a heading they were half way through. On a 375px phone the card is 315px and a
   pitch takes 11.5 seconds, so the pace holds across the range instead of
   racing on the width where a card fills the screen. */

/* -------- 25.5 The card, inside a moving track -------------------------
   The card itself is unchanged from 23.2 and 24.5 and is deliberately not
   restyled here: the same component appears in the static grid on /404.html and
   the two must not drift apart. Cards no longer carry `data-reveal` — the
   behaviour clones them at runtime and a clone would never be observed, so it
   would sit at opacity 0 for ever. The reveal is on the showcase root instead,
   which is never cloned. */
@media (max-width: 991px) {
  /* The arrows are a pointer affordance. On touch the track is dragged, and the
     row keeps them anyway because a keyboard reaches them and because a hidden
     control on one width and a visible one on the next is two components. */
  .showcase-nav { margin-top: -6px; }
}
