/* ============================================================================
   CAMP POOL BUILDERS — v5
   The v3 architecture, art-directed. Not a redesign.

   Everything structural is unchanged: the pill masthead, the hero stage with
   its white copy panel and corner scroll cue, the pale-blue record strip, the
   alternating image/text compositions with overlapping insets, the blue band
   with its portrait strip and wave, the nine-project grid, the two-ways
   section, the process row, the 2x2 proof block, the closing panel and the
   footer. Same order, same colours, same storytelling.

   What changed is proportion, alignment and chrome:

     · ONE container and ONE grid. Every section is .wrap > content, so the
       page has one master left edge and one master right edge instead of
       nine sections each finding their own.
     · Type down 4-35% by level, display weight 700 -> 600.
     · Card chrome cut: radius 14 -> 6, padding 16 -> 12, shadow to a third,
       and the two written testimonials lose their card entirely.
     · Space between components up, space inside them down.

   Section references (§n) point at DESIGNSYSTEM.md v2.
   ========================================================================= */


/* ---- 0. Base ------------------------------------------------------------ */

@font-face{ font-family:'Fraunces'; font-style:normal; font-weight:300;
            font-display:swap; src:url('fonts/fraunces-300.woff2') format('woff2'); }
@font-face{ font-family:'Fraunces'; font-style:italic; font-weight:300;
            font-display:swap; src:url('fonts/fraunces-300i.woff2') format('woff2'); }
@font-face{ font-family:'Plus Jakarta Sans'; font-style:normal; font-weight:400;
            font-display:swap; src:url('fonts/jakarta-400.woff2') format('woff2'); }
@font-face{ font-family:'Plus Jakarta Sans'; font-style:normal; font-weight:500;
            font-display:swap; src:url('fonts/jakarta-500.woff2') format('woff2'); }

*, *::before, *::after{ box-sizing: border-box; }
html{ overflow-x: clip; -webkit-text-size-adjust: 100%; }

/* Never scroll-behavior:smooth in CSS — it also applies to scrollTo() and any
   programmatic scroll, then fights the browser's own smoothing (§5.11). */
body{
  margin: 0; width: 100%; max-width: 100%; overflow-x: hidden;
  background: var(--paper); color: var(--ink);
  font-family: var(--font-b); font-size: var(--t-body); font-weight: 400;
  line-height: var(--lh-b);
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
}
img, video, svg{ display: block; max-width: 100%; }
ul, ol{ list-style: none; margin: 0; padding: 0; }
h1, h2, h3, h4, p, figure, blockquote, dl, dd{ margin: 0; }
a{ color: inherit; text-decoration: none; }
button, input{ font: inherit; color: inherit; }

:where(a, button, input, [tabindex]):focus-visible{
  outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px;
}
::selection{ background: var(--accent); color: #fff; }

.vh{ position:absolute; width:1px; height:1px; margin:-1px; padding:0;
     overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; }
.skip{ position:absolute; left:-9999px; top:0; z-index:100; }
.skip:focus{ left:var(--pad); top:10px; background:var(--paper);
             padding:10px 16px; border-radius:var(--r-s); }


/* ---- 1. The container --------------------------------------------------- */

/* The single most important fix. One max-width, one inset, one class — and
   every section uses it, so a vertical guide dropped down the page hits the
   same left edge in the hero, the story, the work grid, the proof block and
   the footer. */
.wrap{
  width: 100%; max-width: var(--wrap);
  margin-inline: auto; padding-inline: var(--pad);
}

/* Space BETWEEN sections is large; space inside them is not. */
main > section{ position: relative; padding-block: var(--sec-lg); }
.band-tint{ background: var(--tint); }


/* ---- 2. Type ------------------------------------------------------------ */

/* Every display rule states its own weight; none inherits (§3.6). */
.d{
  font-family: var(--font-d); font-weight: var(--wd);
  line-height: var(--lh-d); letter-spacing: var(--tr-d);
  color: var(--ink);
}
h1.d{ line-height: var(--lh-hero); letter-spacing: var(--tr-hero); }
h2.d{ font-size: var(--t-h2); }
h3.d{ font-size: var(--t-h3); }

/* Emphasis is the serif ITALIC in the accent, never a heavier weight. A weight
   change inside a display line breaks the optical rhythm of the setting; an
   italic of the same weight is a change of voice rather than of volume, which
   is what emphasis actually is. --accent-lg is a LARGE-TEXT-ONLY colour
   (3.98:1 on paper), so it only ever appears inside .d (§4.3). */
.d em{ font-style: italic; font-weight: var(--wd); color: var(--accent-lg); }

.bd{ font-size: var(--t-body); line-height: var(--lh-b);
     max-width: var(--measure); color: var(--ink-72); text-wrap: pretty; }
.bd + .bd{ margin-top: var(--sp-3); }

.eb{ font-size: var(--t-meta); letter-spacing: var(--tr-e);
     text-transform: uppercase; font-weight: 500; line-height: 1;
     color: var(--accent); }
.meta{ font-size: var(--t-meta); letter-spacing: .09em;
       text-transform: uppercase; font-weight: 500; color: var(--ink-55);
       line-height: 1.4; }
.cap{ font-size: var(--t-small); line-height: 1.58; color: var(--ink-55);
      text-wrap: pretty; }

/* The kicker was 37px in v3 — nearly the size of the heading it introduced.
   At 24px it reads as the label it is, and the heading below it finally has
   room to be dominant. */
.kick{ font-family: var(--font-d); font-weight: 400; font-size: var(--t-kick);
       line-height: 1.25; letter-spacing: -.008em; color: var(--ink-55); }

/* One lede block, used by every section: label, heading, optional intro.
   Tight inside, generous below. */
.shead{ margin-bottom: var(--sp-7); }
.shead .eb + .d, .shead .kick + .d{ margin-top: var(--sp-2); }
.shead .d{ max-width: 26ch; }
.shead .bd{ margin-top: var(--sp-4); max-width: 56ch; }
.shead.center{ text-align: center; }
.shead.center .d, .shead.center .bd{ margin-inline: auto; }

/* Kevin, at the only size his own site can supply. Both photographs of him on
   camppoolbuilders.com exist at 100x150 and nowhere larger, so a portrait is
   the honest use and a plate is not — at 64px the crop is sharp on a 2x screen
   and the picture reads as a byline, which is what it is. */
/* The picture cannot get much bigger than this and stay honest: 100x150 is
   the whole of it, and a second super-resolution pass to reach 130px smeared
   the eyes and turned the shrubs behind him into streaks. So the PRESENCE comes
   from the block rather than from the photograph. It sits on its own rule, the
   name is set in the serif at heading scale, and his one-line promise runs
   underneath as a caption. The portrait is 104px, sourced from 256, which is
   the largest size that is still sharp on a 2x screen. */
.byline{
  margin-top: var(--sp-6); padding-top: var(--sp-4);
  border-top: 1px solid var(--rule);
  display: flex; align-items: center; gap: var(--sp-4);
  max-width: 46ch;
}
.avatar{
  width: 104px; height: 104px; flex: none; border-radius: 50%;
  overflow: hidden; background: var(--tint);
  /* a ring with a paper gap inside it, so the portrait reads as mounted
     rather than as a cropped thumbnail */
  box-shadow: 0 0 0 5px var(--paper), 0 0 0 6px rgba(20,100,124,.32);
}
.avatar img{ width: 100%; height: 100%; object-fit: cover; }
.byline-txt{ display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.byline-txt b{ font-family: var(--font-d); font-weight: var(--wd);
               font-size: var(--t-h3); color: var(--ink); line-height: 1.05;
               letter-spacing: var(--tr-d); }
.byline-txt i{ font-style: normal; font-size: var(--t-meta); letter-spacing: .1em;
               text-transform: uppercase; color: var(--accent); }
.byline-txt em{ font-style: normal; font-size: var(--t-small); line-height: 1.5;
                color: var(--ink-72); margin-top: 4px; }

.num{ font-family: var(--font-d); font-weight: var(--wd);
      font-size: var(--t-small); line-height: 1; letter-spacing: .06em;
      color: var(--accent); display: block; margin-bottom: var(--sp-3); }

.stat{ font-family: var(--font-d); font-weight: var(--wd);
       font-size: var(--t-stat); line-height: 1; letter-spacing: -.02em;
       color: var(--ink); display: block; }


/* ---- 3. One button system ----------------------------------------------- */

/* Three only: filled, text + arrow, and the circular icon. Same type, same
   height, everywhere on the page. ~10% smaller than v3, so a call to action
   no longer reads as a large UI component sitting on the photography. */
.btn{
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-b); font-weight: var(--wm); font-size: var(--t-nav);
  line-height: 1; letter-spacing: .01em;
  padding: 13px 22px; border-radius: var(--r-s); border: 0; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color var(--dur-fast) var(--ease);
}
.btn-solid{ background: var(--accent); color: var(--accent-ink); }
.btn-solid:hover{ background: #10556A; }
.btn-ink{ background: var(--ink); color: #fff; }
.btn-ink:hover{ background: #1B3C68; }

.btn-text{
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-b); font-weight: var(--wm); font-size: var(--t-nav);
  color: var(--ink); padding-block: 13px;
  transition: color var(--dur-fast) var(--ease);
}
.btn-text:hover{ color: var(--accent); }
.btn-text svg{ width: 16px; height: 16px; flex: none;
               transition: transform var(--dur-fast) var(--ease); }
/* The chip moves, the button does not: it reads as the control anticipating
   the click, and the cursor never loses its target (§5.6). */
.btn-text:hover svg{ transform: translateX(4px); }

.circ{
  width: 46px; height: 46px; flex: none; border-radius: 50%;
  background: var(--ink); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background-color var(--dur-card) var(--ease);
}
.circ:hover{ background: var(--accent); }
.circ svg{ width: 17px; height: 17px;
           transition: transform var(--dur-fast) var(--ease); }
.circ:hover svg{ transform: translateX(3px); }

/* One caption pill, under every build portrait. */
.pill{
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--paper); color: var(--ink);
  font-family: var(--font-b); font-weight: var(--wm); font-size: var(--t-meta);
  letter-spacing: .02em; line-height: 1.3;
  padding: 8px 15px; border-radius: var(--r-s); text-align: center;
}


/* ---- 4. Photography ----------------------------------------------------- */

/* One frame, one radius, one placeholder ground. --tint is declared, so a
   frame never flashes paper-white under its artwork (§8.2). */
.fr{
  position: relative; overflow: hidden;
  border-radius: var(--r); background: var(--tint);
}
.fr img{ width: 100%; height: 100%; object-fit: cover; }

/* The one permitted scale() on a raster: transient, user-initiated, and it
   ends. Persistent scale resamples the layer every frame (§5.8). */
.fr-hover img{ transition: transform var(--dur-img) var(--ease); }
.fr-hover:hover img{ transform: scale(1.03); }

/* Parallax by TRANSLATION. A frame is either .px or .fr-hover, never both:
   script writes an inline transform on .px images and it would beat the hover
   scale, leaving a component whose hover silently does nothing. */
.px > img{
  position: absolute; left: 0; top: -8%; width: 100%; height: 116%;
  object-fit: cover; transform: translate3d(0,0,0);
}

/* Ratios by ROLE, so no image's own dimensions dictate the layout. */
.r-4x3  { aspect-ratio: 4 / 3; }
.r-3x2  { aspect-ratio: 3 / 2; }
.r-sq   { aspect-ratio: 1; }
.r-work { aspect-ratio: 4 / 3; }
.r-plate{ aspect-ratio: 5 / 4; }
.r-por  { aspect-ratio: 4 / 5; }

/* ONE inset treatment, used by every overlapping photograph on the page.
   In v3 each was slightly different — different width, mount and overlap —
   and the inconsistency was visible. The white surround is padding on this
   wrapper, never on the frame: a .fr carrying its own padding reports
   (--r + pad) to the radius histogram and lands one frame off the others. */
.inset{
  position: absolute; z-index: 2;
  right: -5%; bottom: -8%; width: 36%;
  padding: 5px; background: var(--paper);
  border-radius: calc(var(--r) + 5px);
  box-shadow: var(--lift);
}
.inset .fr{ aspect-ratio: 1; }
.duo-rev .inset{ right: auto; left: -5%; }

/* The card. Less of everything: a third of the shadow, 6px instead of 14, and
   12px of padding instead of 16 — so it reads as a photograph with a caption
   rather than as a UI component. */
.card{
  background: var(--paper); border-radius: var(--r);
  padding: var(--sp-2);
  box-shadow: var(--lift);
  display: flex; flex-direction: column;
  transition: transform var(--dur-card) var(--ease),
              box-shadow var(--dur-card) var(--ease);
}
.card:hover{ transform: translateY(-2px); box-shadow: var(--lift-h); }
.card .fr{ margin-bottom: var(--sp-3); }
.card .stat + .cap{ margin-top: var(--sp-2); }


/* ---- 5. Motion ---------------------------------------------------------- */

/* Nothing fades: an element dissolving up from zero opacity is
   indistinguishable from one still downloading (§5.3). Distances are shorter
   than v3 to match the smaller components. */
.rv, .rv-l, .rv-r, .rv-s{ opacity: 1; }
.rv  { transform: translate3d(0, var(--rv-text), 0); }
.rv-l{ transform: translate3d(calc(var(--rv-side) * -1), 0, 0); }
.rv-r{ transform: translate3d(var(--rv-side), 0, 0); }
.rv-s{ transform: translate3d(0, var(--rv-text), 0); }
.card.rv, .card.rv-s{ transform: translate3d(0, var(--rv-card), 0); }
.card.rv-l{ transform: translate3d(calc(var(--rv-card) * -1), 0, 0); }
.card.rv-r{ transform: translate3d(var(--rv-card), 0, 0); }
.plate.rv-l{ transform: translate3d(calc(var(--rv-plate) * -1), 0, 0); }
.plate.rv-r{ transform: translate3d(var(--rv-plate), 0, 0); }
.plate.rv-s{ transform: translate3d(0, var(--rv-plate), 0); }


/* ---- 6. Masthead -------------------------------------------------------- */

/* The pill stays. ~10% smaller, on the page grid, and with a lighter ground.
   Lives at body level: a position:fixed child of the hero stage would be
   trapped by that element's overflow:hidden (§6.2). */
.nav{
  position: fixed; z-index: 60; top: 0; left: 0; right: 0;
  --nav-top: clamp(12px, 1.5vw, 20px);
  padding-top: var(--nav-top);
  /* Padding is CONSTANT between all three states. A masthead that shrinks its
     padding when it takes its ground reflows the bar on every direction
     change, and the whole page reads as though it is still loading (§6.7). */
  transition: transform var(--dur-nav) var(--ease);
}
.nav.up{ transform: translateY(calc(-100% - var(--nav-top))); }

/* Nothing at the top of the page. The white pill read as a widget parked on
   the photograph rather than as the head of a document — over film, the
   masthead should be the film with type on it. It takes a ground only once
   the page has scrolled, where there is content behind it to separate from. */
.navbar{
  min-height: 56px;
  display: grid; grid-template-columns: auto minmax(0,1fr) auto;
  align-items: center; gap: var(--sp-4);
  padding: 9px 9px 9px 0;
  background: transparent;
  border-radius: var(--r-s);
  transition: background-color var(--dur-ground) var(--ease),
              box-shadow var(--dur-ground) var(--ease),
              padding var(--dur-ground) var(--ease);
}
.nav.solid .navbar{
  background: rgba(251,252,253,.97);
  padding-left: 18px;
  box-shadow: 0 8px 24px -12px rgba(18,42,76,.22);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
          backdrop-filter: saturate(140%) blur(12px);
}

/* White over the film, ink once it has a ground. Both states are sampled
   against the frames they actually sit on (§4.5). */
.navlinks a{ color: #fff; }
.nav.solid .navlinks a{ color: var(--ink-72); }
.nav .btn-solid{ background: rgba(255,255,255,.14); color: #fff;
                 box-shadow: inset 0 0 0 1px rgba(255,255,255,.42); }
.nav .btn-solid:hover{ background: rgba(255,255,255,.24); }
.nav.solid .btn-solid{ background: var(--accent); color: var(--accent-ink);
                       box-shadow: none; }
.nav.solid .btn-solid:hover{ background: #10556A; }
.navtoggle span{ background: #fff; }
.nav.solid .navtoggle span{ background: var(--ink); }
/* Ink on an opaque pill in all three states — no colour flip to flash, and no
   masthead text ever sits over photography without a ground of its own, which
   removes §4.5's blind spot from the header entirely. */
.brand{ display: flex; align-items: center; }
.brand img{ width: auto; height: clamp(30px, 2.6vw, 38px); }

.navlinks{ justify-self: center; display: flex; align-items: center;
           gap: clamp(14px, 2.2vw, 34px); }
.navlinks a{
  position: relative; display: inline-block;
  font-size: var(--t-nav); font-weight: 500;
  padding-block: 9px;
  transition: color var(--dur-fast) var(--ease);
}
.nav.solid .navlinks a:hover,
.nav.solid .navlinks a[aria-current="true"]{ color: var(--accent); }
.navlinks a:hover, .navlinks a[aria-current="true"]{ color: #fff; }
.navlinks a::after{
  content: ''; position: absolute; left: 0; right: 0; bottom: 3px; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-card) var(--ease);
}
.navlinks a:hover::after,
.navlinks a[aria-current="true"]::after{ transform: scaleX(1); }
.nav .btn{ justify-self: end; }

/* The burger takes the same grid slot the desktop CTA does; a centred burger
   under a left-aligned wordmark reads as an unfinished layout (§6.4). */
.navtoggle{
  display: none; grid-column: 3; justify-self: end;
  width: 44px; height: 44px;
  align-items: center; justify-content: center; flex-direction: column; gap: 6px;
  background: none; border: 0; cursor: pointer; padding: 0;
  -webkit-tap-highlight-color: transparent;
}
/* Two lines, not three: with three the middle bar has to fade during the
   morph and the fade always shows as a flicker at the crossing point. */
.navtoggle span{ display: block; width: 22px; height: 1.5px; border-radius: 2px;
                 background: var(--ink); transition: transform .4s var(--ease); }
.navtoggle[aria-expanded="true"] span:nth-child(1){ transform: translateY(3.75px) rotate(45deg); }
.navtoggle[aria-expanded="true"] span:nth-child(2){ transform: translateY(-3.75px) rotate(-45deg); }

/* The panel wipes, it does not pop: `hidden` is display:none and a display
   flip has nothing to transition, so script holds the attribute back until
   the close has played (§6.5). */
.navpanel{
  position: fixed; inset: 0; z-index: 70; background: var(--paper);
  overscroll-behavior: contain; overflow-y: auto;
  padding-bottom: var(--sp-6);
  display: flex; flex-direction: column;
  transform: translateY(-100%);
  transition: transform var(--dur-panel) var(--ease);
}
.navpanel.open{ transform: none; }
.navpanel[hidden]{ display: none; }
.navpanel .phead{ display: flex; align-items: center; min-height: 78px;
                  padding-inline: var(--pad); }
.navpanel .phead img{ width: auto; height: 34px; }
.navpanel ul{ display: flex; flex-direction: column; margin-top: var(--sp-4); }
/* Permitted 1px line: a divider BETWEEN items inside one component (§0.4). */
.navpanel li + li{ border-top: 1px solid var(--rule); }
.navpanel a:not(.btn){
  display: block; padding: 14px var(--pad);
  font-family: var(--font-d); font-weight: var(--wd); font-size: var(--t-h3);
  transition: color var(--dur-fast) var(--ease);
}
.navpanel a:not(.btn):hover{ color: var(--accent); }
.navpanel .pfoot{ margin-top: auto; padding: var(--sp-5) var(--pad) 0; display: flex; }
.navpanel .pfoot .btn{ flex: 1; }
/* Keyed to the CLASS, not :not([hidden]) — the attribute lags the class by
   the length of the close, and an attribute selector would strand the items
   in place for half a second and then drop them (§6.5). */
.navpanel li{ transform: translateY(8px); transition: transform .45s var(--ease); }
.navpanel.open li{ transform: none; }
.navpanel.open li:nth-child(1){ transition-delay: 140ms; }
.navpanel.open li:nth-child(2){ transition-delay: 180ms; }
.navpanel.open li:nth-child(3){ transition-delay: 220ms; }
.navpanel.open li:nth-child(4){ transition-delay: 260ms; }
.navpanel.open li:nth-child(5){ transition-delay: 300ms; }
.navpanel.open li:nth-child(6){ transition-delay: 340ms; }
/* The close control lives INSIDE the panel: .nav is a stacking context, so
   nothing within it can lift above a panel in the root context (§6.5). */
.navclose{
  position: absolute; top: clamp(16px, 2vw, 26px); right: var(--pad);
  width: 44px; height: 44px; background: none; border: 0; cursor: pointer;
}
.navclose::before, .navclose::after{
  content: ''; position: absolute; inset: 50% auto auto 50%;
  width: 20px; height: 1.5px; background: var(--ink);
}
.navclose::before{ transform: translate(-50%,-50%) rotate(45deg); }
.navclose::after { transform: translate(-50%,-50%) rotate(-45deg); }


/* ---- 7. Hero ------------------------------------------------------------ */

/* The film is the hero. It runs edge to edge and full height, a dark wash is
   laid over it, and the copy sits directly on the picture — no white panel.
   That panel was doing two jobs: guaranteeing contrast, and giving the type an
   edge to start from. The wash now does the first and .wrap does the second,
   so the photograph is no longer cut in half by a white rectangle. */
#top{ padding-block: 0; }
.hero{
  /* svh, not vh: on iOS vh is the tallest the viewport ever gets, so a 100vh
     hero is cropped by the address bar on load and only becomes correct after
     the first scroll. svh is the smallest state and therefore always fits. */
  height: 100svh; min-height: 560px;
  display: flex;
}
.hero .stage{
  position: relative; flex: 1; min-width: 0;
  background-color: var(--media-dark);
  display: flex; align-items: stretch;
}
.stage-media{ position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.mp-art{ position: absolute; left: 0; right: 0; top: -4%; height: 108%; }
.mp-art > *{ width: 100%; height: 100%; object-fit: cover; }
.mp-still{ position: absolute; inset: 0; }
/* Opacity, NOT display. `display:none` was a deadlock on iOS: Safari will not
   decode a video it is not painting, so `canplay` never fired, so the class
   that would have revealed it was never added, so the film simply never
   appeared on an iPhone. The original reason for display was that a laid-out
   <video> with no decoded frame can paint black over the still — but an
   element at opacity 0 paints nothing at all, so it solves that AND lets
   Safari decode. The still is the LCP element and always paints underneath.
   The fade is what turns the swap from a cut into a dissolve. */
.mp-vid{ position: absolute; inset: 0; opacity: 0;
         transition: opacity .7s var(--ease); }
.mp-vid.ready{ opacity: 1; }

/* The contrast guarantee, not decoration. Weighted to the bottom because all
   the type is in the lower third; the top stays open so the picture is still a
   picture. Every stop below is solved for the ratio the element above it
   needs and then sampled off the rendered frame (§4.5). */
.wash-scrim{
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    linear-gradient(to top,
      rgba(6,18,34,.90)   0%,
      rgba(6,18,34,.84)  18%,
      rgba(6,18,34,.72)  38%,
      rgba(6,18,34,.46)  60%,
      rgba(6,18,34,.16)  80%,
      rgba(6,18,34,.06) 100%),
    /* a second, gentler pass from the left: the copy column is on that side
       and a bright pool in the left third is the one thing that beats a
       bottom-up wash */
    linear-gradient(to right,
      rgba(6,18,34,.34)   0%,
      rgba(6,18,34,.14)  42%,
      rgba(6,18,34,0)    72%),
    /* A band under the masthead, which now has no ground of its own. Solved
       for the worst case rather than eyeballed: the film pushes into open sky,
       so the ground behind the links reaches near-white, and at that point
       even pure white text sits at 3.98:1 against a 4.5 requirement. 255 less
       249a puts the ground at 96 for a=.64, which is 6.3:1 and holds for every
       frame. Stops are in px, not percentages, so the band stays tied to the
       masthead rather than to the height of the hero (§4.6). */
    linear-gradient(to bottom,
      rgba(6,18,34,.66)   0px,
      rgba(6,18,34,.64)  84px,
      rgba(6,18,34,.34) 150px,
      rgba(6,18,34,0)   240px) top / 100% 240px no-repeat;
}

/* Oversized and translated, never scaled — a 24s scale() drift on the first
   build held the transform above 1.03 for its whole visible window, so the
   video was never once drawn at its true size (§5.8). */
/* On the CONTAINER, not on each child. Running it on `.mp-art > *` gave the
   still and the video one timeline each, and the video's did not start until
   the element was first painted — several seconds in. So the film arrived
   already offset from the still it was replacing and restarted the 26s travel
   from the top, which read as a jump at an arbitrary moment on every load.
   One animation on the parent moves both layers together, and the dissolve
   underneath it has nothing left to give away. */
@keyframes drift{ from{ transform: translate3d(0, 1.5%, 0); } to{ transform: none; } }
.mp-art{ animation: drift 26s cubic-bezier(.15,0,.85,1) forwards; }

/* The copy rides the page's own container, so its left edge is the same line
   the record strip, the work grid and the footer resolve to. */
/* The copy block is centred in the space below the masthead, not in the whole
   stage. Centring it in the stage is what the arithmetic wants and not what the
   eye wants: it put an equal 200px above and below, but the top 90 of that is
   masthead, so the visible gap above read as 110 against 200. Adding the
   masthead's height to the top padding makes the two VISIBLE gaps equal, which
   is the thing being judged (§7.3). */
.hz-wrap{
  position: relative; z-index: 2;
  width: 100%; align-self: stretch;
  padding-top: calc(clamp(56px, 8vh, 112px) + var(--nav-h));
  padding-bottom: clamp(56px, 8vh, 112px);
  display: flex; align-items: center;
}
.hz{
  min-width: 0; max-width: 52ch;
  display: flex; flex-direction: column; align-items: flex-start;
  color: #fff;
}
/* Lightened from #BEE6F2. At 4.52 against a 4.5 requirement the label cleared
   on the frame it was sampled on and on no other — a wash has to hold for
   every frame of the film, not the one that happened to be paused. */
.hz .eb{ color: #D6EFF8; }
.hz h1{ font-size: var(--t-hero); margin-top: var(--sp-3);
        max-width: 15ch; color: #fff;
        /* a hairline of shadow, not a glow: it buys about half a stop of
           separation on the one frame of film where the wash is thinnest,
           and is invisible everywhere else */
        text-shadow: 0 1px 24px rgba(4,14,28,.45); }
.hz h1 em{ color: #6FD0EE; }
.hz .deck{ font-size: var(--t-deck); margin-top: var(--sp-3);
           max-width: 46ch; color: rgba(255,255,255,.90); line-height: 1.62;
           text-shadow: 0 1px 18px rgba(4,14,28,.40); }
.hz .row{ display: flex; align-items: center; flex-wrap: wrap;
          gap: var(--sp-2) var(--sp-4); margin-top: var(--sp-5); }
.hz .btn-text{ color: #fff; }
.hz .btn-text:hover{ color: #6FD0EE; }

/* On the guide, at the foot of the stage. It sits outside the centred block so
   that block can own the vertical middle. */
.scrollcue{
  position: absolute; right: var(--pad); bottom: clamp(28px, 4.5vh, 64px);
  width: 52px; height: 52px; border-radius: 50%;
  background: rgba(255,255,255,.14); color: #fff;
  border: 1px solid rgba(255,255,255,.40);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  display: flex; align-items: center; justify-content: center;
  transition: background-color var(--dur-card) var(--ease),
              border-color var(--dur-card) var(--ease);
}
.scrollcue:hover{ background: var(--accent); border-color: var(--accent); }
.scrollcue svg{ width: 17px; height: 17px;
                transition: transform var(--dur-card) var(--ease); }
.scrollcue:hover svg{ transform: translateY(3px); }

/* Every line starts at a different moment and lands on the SAME frame —
   delay + duration = 1900ms on each. A hero that trails off resolves as
   several events; one that lands together resolves as one gesture (§7.7). */
@keyframes heroUp{ from{ transform: translateY(24px); } to{ transform: none; } }
.hz .eb  { animation: heroUp 1.50s var(--ease) .40s forwards; }
.hz h1   { animation: heroUp 1.40s var(--ease) .50s forwards; }
.hz .deck{ animation: heroUp 1.30s var(--ease) .60s forwards; }
.hz .row { animation: heroUp 1.20s var(--ease) .70s forwards; }
#main-nav{ animation: heroUp 1.30s var(--ease) .60s forwards; }
.scrollcue{ animation: heroUp 1.10s var(--ease) .80s forwards; }
/* fill-mode:forwards keeps holding the property after the intro, so the
   sticky masthead has nothing to animate until script hands it back (§7.7). */
.hz.settled, .hz .settled, #main-nav.settled{ animation: none; }


/* ---- 8. The record strip ------------------------------------------------ */

/* Reads as the opening of the next section rather than a second hero: a
   shorter band, and compact cards. */
#record{ padding-block: var(--sec); }
/* Four figures on one rule. No cards, no shadows, no thumbnails: the numbers
   are the content and a container around each one only competes with them.
   minmax(0,…) on every track because the default minimum is min-content, so
   one unbreakable string widens its track and drags the right edge off the
   guide at some widths only, which is why that bug survives review (§1.3). */
.record{
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-5) var(--colgap); align-items: start;
}
.record .score b{ font-size: var(--t-record); color: var(--accent);
                  display: flex; align-items: baseline; gap: 6px; }
/* the unit rides the figure at a fraction of its size, so "2 to 3 wks" still
   reads as one number rather than as a number and a word */
.record .score b i{ font-style: normal; font-size: .40em; letter-spacing: .04em;
                    color: var(--ink-55); font-family: var(--font-b);
                    font-weight: 500; text-transform: uppercase; }
.record .score .cap{ margin-top: var(--sp-3); max-width: 30ch;
                     color: var(--ink-72); }
.record .score + .score{ position: relative; }
/* one hairline between items, never around them (§0.4) */
.record .score + .score::before{
  content: ''; position: absolute; left: calc(var(--colgap) / -2); top: 4px;
  bottom: 4px; width: 1px; background: var(--rule);
}


/* ---- 9. The alternating story blocks ------------------------------------ */

/* One layout, mirrored. 7/5 and 5/7 of the same 12-track grid, so the two
   blocks are variations on ONE composition rather than two layouts that
   merely resemble each other. */
.duo{
  display: grid; grid-template-columns: repeat(12, minmax(0,1fr));
  column-gap: var(--colgap); row-gap: var(--sp-6);
  align-items: center;
}
/* Both on row 1 explicitly. Auto-placement is SPARSE by default: in the
   mirrored block the picture claims columns 6-12 first, the cursor is then
   past column 5, and the copy — which asks for columns 1-4 — is dropped into
   a second row instead. The composition silently becomes a stack, and only at
   the one breakpoint where it is supposed to be side by side. */
.duo-pic, .duo-copy{ grid-row: 1; }
.duo-pic { grid-column: 1 / 8; position: relative; }
.duo-copy{ grid-column: 9 / 13; }
.duo-rev .duo-pic { grid-column: 6 / 13; }
.duo-rev .duo-copy{ grid-column: 1 / 5; }
.duo + .duo{ margin-top: var(--sec); }

/* The story column is supporting text beside a photograph — ~40ch, not the
   full width of its track. */
.duo-copy .bd{ max-width: var(--measure-story); }
.duo-copy .d{ max-width: 15ch; }
.duo-copy .d + .bd{ margin-top: var(--sp-3); }
.duo-copy .btn-text{ margin-top: var(--sp-3); }

/* The hallmark row: punctuation between compositions, not a fifth section. */
.marks{
  margin-top: var(--sec);
  display: grid; grid-template-columns: repeat(4, minmax(0,1fr));
  gap: var(--colgap); align-items: start;
}
.mark .fr{ margin-bottom: var(--sp-3); }
.mark b{ display: block; font-family: var(--font-d); font-weight: var(--wd);
         font-size: var(--t-h3); line-height: 1; letter-spacing: -.02em;
         color: var(--accent-lg); }
.mark span{ display: block; margin-top: var(--sp-2);
            font-size: var(--t-small); line-height: 1.5; color: var(--ink-55); }


/* ---- 10. The blue band -------------------------------------------------- */

/* Water, built rather than painted. A flat fill reads as a coloured box; what
   reads as water is several near-identical tones lying over each other at
   different scales.

   The repeating gradients are GONE. Two of them crossing was meant to give a
   slow caustic, and at every opacity I tried it read as a grid of diagonal
   crosses instead: a repeating linear gradient has one period and one angle, so
   it cannot help but be regular, and regular is the one thing water is not.
   Six soft radial blooms at different sizes and positions carry it instead,
   with nothing in the layer stack that repeats. */
.band-wash{
  position: relative;
  color: var(--ink);
  background:
    /* light entering top left, pooling bottom right */
    radial-gradient(120% 90% at 12% 0%,
      var(--wash-lit) 0%, rgba(244,250,252,0) 55%),
    radial-gradient(110% 85% at 88% 100%,
      var(--wash-3) 0%, rgba(190,220,232,0) 60%),
    /* four shallower blooms, none of them the same size or on a grid */
    radial-gradient(58% 40% at 68% 18%,
      rgba(255,255,255,.55) 0%, rgba(255,255,255,0) 62%),
    radial-gradient(44% 52% at 26% 62%,
      rgba(140,203,224,.26) 0%, rgba(140,203,224,0) 68%),
    radial-gradient(72% 34% at 92% 46%,
      rgba(255,255,255,.42) 0%, rgba(255,255,255,0) 70%),
    radial-gradient(38% 46% at 44% 88%,
      rgba(109,185,214,.20) 0%, rgba(109,185,214,0) 72%),
    /* the surface horizon across the middle */
    linear-gradient(178deg,
      rgba(207,230,238,0) 0%, var(--wash-2) 46%, rgba(207,230,238,0) 82%),
    var(--wash);
}
.band-wash .shead .kick{ color: var(--ink-55); }
.band-wash .d{ color: var(--ink); }
.band-wash .d em{ color: var(--wash-em); }
.band-wash .cap{ color: var(--ink-72); }
.band-wash .bd{ color: var(--ink-72); }

/* Five equal frames. The first column used to carry a card and a picture while
   the four beside it carried one photograph each, which meant one column was
   always a different shape from the rest of the row. The card's words are the
   section's own introduction, so they belong above the heading, and the strip
   is then simply five of the same thing. */
.buildsgrid{
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--colgap); align-items: start;
}
.bcol{ display: flex; flex-direction: column; align-items: center; }
/* Paper on water: the pill has to lift off a ground that is already pale, so
   it is a solid white chip with a hairline rather than a translucent one. */
.bcol .pill{ margin-top: var(--sp-3); background: var(--paper);
             color: var(--ink); box-shadow: 0 1px 6px -2px rgba(18,42,76,.18); }

/* The arrow is a page-level control, centred under the whole grid. */
.buildsmore{ margin-top: var(--sp-6); display: flex; justify-content: center; }
.buildsmore .circ{ background: var(--ink); color: #fff; }
.buildsmore .circ:hover{ background: var(--accent); }

/* The surfaces, generated by tools/foam.py. Every ground change on the page
   carries one: a coloured band opens out of the paper above it and closes into
   the paper below it, so no two sections ever meet on a straight line.

   .foam-up is the same shape flipped, which is what lets one symbol serve both
   directions: flipped, the water rises from the crest and fades to paper at the
   top, so the paper section above flows into paper, then water, then the band.
   The two shapes are also mirrored on x, which gives four apparent waves from
   two symbols; the same wave repeated down a page is immediately obvious. */
.foam-defs{ position: absolute; width: 0; height: 0; overflow: hidden; }

.foam{
  position: absolute; left: 0; right: 0; z-index: 1;
  width: 100%; height: var(--foam-h);
  display: block; pointer-events: none;
}
/* 2px of overhang. The height is a clamp so the element's edge lands mid
   device row at most widths, and anything still painting under the crest at
   that row leaks through the partial coverage; the swell behind the wave did
   exactly that until tools/foam.py stopped filling it to the floor. The
   overhang is the second half of that fix, and it is free: the wave's outer
   band is pure paper, and z-index 1 puts it above the next section's
   background rather than under it. */
.foam-down{ bottom: -2px; }
.foam-up{ top: -2px; transform: scaleY(-1); }
/* Mirrored on x, which is the cheap half of the variety: two generated shapes
   and a flip read as four different crests down the page. */
.foam-mirror{ transform: scaleX(-1); }
.foam-up.foam-mirror{ transform: scaleY(-1) scaleX(-1); }

/* The two intensities are NOT set here. Colouring the gradient from custom
   properties on each instance does not work: a gradient referenced with url()
   resolves its variables where the gradient lives, so the stops came back
   rgb(0,0,0). The tones are baked into four symbols by tools/foam.py, sharing
   one copy of the geometry. .foam-deep and .foam-soft stay in the markup as a
   record of which is which. */

/* Bubbles held under the surface. CSS gradients rather than circles in the
   symbol because that viewBox is stretched with preserveAspectRatio="none" so
   a wave can span any width, which would turn every circle into an oval. A
   background-size in px stays round whatever the element does. */
.foam-bubbles{
  position: absolute; left: 0; right: 0; z-index: 2;
  height: var(--foam-h); pointer-events: none;
  background-repeat: no-repeat;
  background-image:
    radial-gradient(circle, rgba(255,255,255,.62) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.50) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.44) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.58) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.38) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.52) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.34) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.46) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.30) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.55) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.36) 0 45%, rgba(255,255,255,0) 62%),
    radial-gradient(circle, rgba(255,255,255,.48) 0 45%, rgba(255,255,255,0) 62%);
  background-size:
    6px 6px, 4px 4px, 3px 3px, 7px 7px, 3px 3px, 5px 5px,
    2px 2px, 4px 4px, 3px 3px, 6px 6px, 2px 2px, 5px 5px;
  background-position:
    7% 62%, 15% 44%, 23% 71%, 31% 51%, 39% 78%, 47% 58%,
    55% 46%, 63% 74%, 71% 55%, 79% 66%, 87% 48%, 94% 70%;
}
.foam-bubbles.foam-down{ bottom: 0; }
.foam-bubbles.foam-up{ top: 0; transform: scaleY(-1); }
.foam-bubbles.foam-mirror{ transform: scaleX(-1); }
.foam-bubbles.foam-up.foam-mirror{ transform: scaleY(-1) scaleX(-1); }

/* The bands make room for whichever edges carry a wave, so no content ever
   sits under one. */
.band-tint{ position: relative; }
#record{ padding-bottom: calc(var(--sec) + var(--foam-h)); }
#builds{ padding-block: calc(var(--sec-lg) + var(--foam-h)); }
#ways, #reviews{ padding-block: calc(var(--sec-lg) + var(--foam-h)); }


/* ---- 11. The work grid -------------------------------------------------- */

/* Identical gutters, one aspect ratio, one caption structure. No card: the
   caption sits under the frame on the same baseline for all nine. */
.workgrid{
  display: grid; grid-template-columns: repeat(3, minmax(0,1fr));
  gap: var(--sec) var(--colgap);
}
.wk figcaption{
  margin-top: var(--sp-3);
  display: grid; grid-template-columns: auto minmax(0,1fr);
  column-gap: var(--sp-3); align-items: baseline;
}
.wk .n{ font-size: var(--t-meta); font-weight: 500; letter-spacing: .1em;
        color: var(--ink-55); font-variant-numeric: tabular-nums; }
.wk figcaption span{ font-family: var(--font-b); font-weight: var(--wm);
                     font-size: var(--t-small); color: var(--ink);
                     transition: color var(--dur-card) var(--ease); }
.wk figcaption i{ grid-column: 2; margin-top: 2px; font-style: normal;
                  font-size: var(--t-meta); letter-spacing: .04em;
                  color: var(--ink-55); }
.wk:hover figcaption span{ color: var(--accent); }

/* All nine tiles are now photographs on the same frame with the same caption
   underneath. The ninth used to be a tinted card because the archive had no
   Wexford photograph — one odd tile in a grid of eight reads as a gap, not as
   a note. */


/* ---- 12. Renovation ----------------------------------------------------- */

.reno{
  margin-top: var(--sec);
  display: grid; grid-template-columns: repeat(12, minmax(0,1fr));
  column-gap: var(--colgap); row-gap: var(--sp-5); align-items: center;
}
.reno-pics{ grid-column: 1 / 7; display: grid; grid-template-columns: 1fr 1fr;
            gap: var(--colgap); }
.reno-copy{ grid-column: 8 / 13; }
.reno-copy .d{ font-size: var(--t-h3); max-width: 14ch; }
.reno-copy .bd{ margin-top: var(--sp-3); max-width: var(--measure-story); }
.reno-copy .btn-text{ margin-top: var(--sp-3); }


/* ---- 13. Process -------------------------------------------------------- */

.steps{ display: grid; grid-template-columns: repeat(4, minmax(0,1fr));
        gap: var(--colgap); align-items: start; }
.step .fr{ margin-bottom: var(--sp-3); }
.step .n{ display: block; font-family: var(--font-b); font-size: var(--t-meta);
          font-weight: 500; letter-spacing: .1em; color: var(--ink-55);
          font-variant-numeric: tabular-nums; margin-bottom: 0; }
.step h3{ margin-top: var(--sp-2); font-family: var(--font-b);
          font-weight: var(--wm); font-size: var(--t-small);
          line-height: 1.35; color: var(--ink); }
.step .cap{ margin-top: var(--sp-2); }


/* ---- 14. Proof ---------------------------------------------------------- */

/* Rebuilt. The 2x2 was two written notes of our own copy sitting at the same
   weight as two real reviews, which made the block read as four testimonials
   when only two of them are. There are exactly two published quotes and no
   more, so this is built around two rather than padded out to a grid: the
   number leads, the two reviews follow at equal size, and our own framing is
   demoted to a closing line. */

.scores{
  display: grid; grid-template-columns: repeat(3, minmax(0,1fr)) minmax(0,1.4fr);
  gap: var(--sp-5) var(--colgap);
  padding-bottom: var(--sp-6); border-bottom: 1px solid var(--rule);
}
.score{ display: flex; flex-direction: column; gap: 4px; }
.score b{ font-family: var(--font-d); font-weight: var(--wd);
          font-size: var(--t-stat); line-height: 1; letter-spacing: -.02em;
          color: var(--ink); }
/* The company's own figure is the one that carries the mark's colour; the two
   comparisons stay in ink so the block reads as a scoreboard and not as three
   equal claims. */
.score:first-child b{ color: var(--accent); }
.score span{ font-size: var(--t-small); color: var(--ink-72); margin-top: 2px; }
.score i{ font-style: normal; font-size: var(--t-meta); letter-spacing: .1em;
          text-transform: uppercase; color: var(--ink-55); }
.score-note{ justify-content: flex-end; }
.score-note .cap{ max-width: 40ch; }

.quotes{
  margin-top: var(--sec);
  display: grid; grid-template-columns: repeat(2, minmax(0,1fr));
  gap: var(--colgap);
}
.vquote{ position: relative; }
.vquote .fr{ aspect-ratio: 4 / 3; background: var(--media-dark); }
/* Text over photography: the scrim is not decoration, it is the contrast
   guarantee. Sampled on the rendered page under the glyph lines (§4.5). */
.vquote .fr::after{
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(6,20,38,.74) 0%,
                                      rgba(6,20,38,.34) 44%,
                                      rgba(6,20,38,.26) 62%,
                                      rgba(6,20,38,.62) 88%,
                                      rgba(6,20,38,.80) 100%);
}
.vquote blockquote{
  position: absolute; z-index: 2; top: 0; left: 0; right: 0;
  padding: clamp(20px, 2.2vw, 34px);
  font-family: var(--font-d); font-weight: var(--wm); font-size: var(--t-h3);
  line-height: 1.24; letter-spacing: -.015em; color: #fff; max-width: 18ch;
}
.vquote figcaption{
  position: absolute; z-index: 2; left: 0; right: 0;
  bottom: clamp(16px, 1.8vw, 26px); text-align: center;
  font-size: var(--t-meta); letter-spacing: .12em; text-transform: uppercase;
  font-weight: 500; color: rgba(255,255,255,.94);
}

.closing-note{
  margin-top: var(--sec); margin-inline: auto; max-width: 62ch;
  text-align: center; color: var(--ink-72);
}


/* ---- 15. Closing -------------------------------------------------------- */

.closer{
  display: grid; grid-template-columns: repeat(12, minmax(0,1fr));
  column-gap: var(--colgap); row-gap: var(--sp-5); align-items: stretch;
}
/* Capped at its own source width, at every size. This photograph exists at
   740px and nowhere larger; with no container cap its column reaches 850 at
   1920 and keeps growing, so without the ceiling the closing image is the one
   picture on the page drawn over native — softness exactly where the page
   should land hardest (§5.8). */
/* No cap any more: the replacement photograph is 929px where the old one was
   740, so the picture fills its half of the grid instead of stopping short of
   it and leaving the panel beside it looking oversized. */
.closer-pic{ grid-column: 1 / 7; min-height: clamp(400px, 34vw, 620px); }
/* Adjacent, with only the column gap between them — the picture and the
   invitation are one object, and a five-column trench down the middle read as
   two unrelated halves. The panel takes the tint so the pairing is legible as
   a pairing even where the photograph runs light at its edge. */
.closer-panel{
  grid-column: 7 / 13;
  background: var(--tint); border-radius: var(--r);
  padding: clamp(32px, 3.8vw, 72px);
  display: flex; flex-direction: column; justify-content: center;
}
.closer-panel .d{ font-size: var(--t-h2); max-width: 16ch; margin-top: var(--sp-2); }
.cta-steps{ margin-top: var(--sp-5); display: flex; flex-direction: column;
            gap: var(--sp-4); }
.cta-steps li{ display: grid; grid-template-columns: auto minmax(0,1fr);
               gap: var(--sp-3); align-items: start; }
.cta-steps b{
  font-family: var(--font-b); font-weight: var(--wm); font-size: var(--t-meta);
  letter-spacing: .1em; color: var(--ink-55); padding-top: 3px;
  font-variant-numeric: tabular-nums;
}
.cta-steps h3{ font-family: var(--font-b); font-weight: var(--wm);
               font-size: var(--t-small); line-height: 1.35; color: var(--ink); }
.cta-steps p{ margin-top: 3px; font-size: var(--t-small); line-height: 1.55;
              color: var(--ink-72); overflow-wrap: anywhere; }
.cta-steps a{ transition: color var(--dur-card) var(--ease); }
.cta-steps a:hover{ color: var(--accent); }
.closer-panel .row{ margin-top: var(--sp-5); display: flex; flex-wrap: wrap;
                    align-items: center; gap: var(--sp-2) var(--sp-4); }


/* ---- 16. Footer --------------------------------------------------------- */

/* Less dense than v3: more air above, a single rule instead of a card border,
   and link lists set on their own leading rather than the body's. */
.foot{ padding-block: var(--sec) var(--sp-5); }
.foot-top{
  display: grid; grid-template-columns: minmax(0,1.2fr) minmax(0,1fr);
  gap: var(--sp-6) clamp(32px, 5vw, 88px);
  align-items: start;
  padding-top: var(--sp-6); border-top: 1px solid var(--rule);
}
.fbrand img{ width: auto; height: clamp(40px, 3.6vw, 52px); }
.fbrand .cap{ margin-top: var(--sp-3); max-width: 40ch; }
.fnav{ display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
       margin-top: var(--sp-5); }
.fnav a{ font-family: var(--font-b); font-weight: var(--wm);
         font-size: var(--t-small); color: var(--ink);
         transition: color var(--dur-card) var(--ease); }
.fnav a:hover{ color: var(--accent); }

.fform .d{ font-size: var(--t-h3); }
.fform .cap{ margin-top: var(--sp-3); max-width: 38ch; }
.nl{ margin-top: var(--sp-4); position: relative; display: flex; }
.nl input{
  flex: 1; min-width: 0; padding: 15px 54px 15px 18px;
  border: 1px solid var(--rule); border-radius: var(--r-s);
  background: var(--paper); font-size: var(--t-small);
  transition: border-color var(--dur-fast) var(--ease);
}
.nl input::placeholder{ color: var(--ink-55); }
.nl input:focus{ border-color: var(--accent); }
.nl button{
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: 40px; height: 40px; border-radius: 50%; border: 0; cursor: pointer;
  background: var(--accent); color: #fff;
  display: flex; align-items: center; justify-content: center;
  transition: background-color var(--dur-fast) var(--ease);
}
.nl button:hover{ background: var(--ink); }
.nl button svg{ width: 15px; height: 15px; }
.nl-note{ font-size: var(--t-small); color: var(--ink-72);
          margin-top: var(--sp-2); min-height: 1.5em; }

.fcols{
  margin-top: var(--sec); padding-top: var(--sp-5);
  border-top: 1px solid var(--rule);
  display: grid;
  grid-template-columns: repeat(3, minmax(0,1fr)) minmax(0,1.2fr);
  gap: var(--sp-5) var(--colgap); align-items: start;
}
.fcols h3{ font-size: var(--t-meta); letter-spacing: var(--tr-e);
           text-transform: uppercase; font-weight: 500; color: var(--accent);
           margin-bottom: var(--sp-3); }
/* Link lists must not inherit body leading — at 1.62 a footer list reads as a
   paragraph, which is one of the two things that makes a footer feel loose
   (§10.5). One gap value across every list on the page. */
.fcols ul{ display: flex; flex-direction: column; gap: 9px; }
.fcols li{ line-height: 1.35; }
.fcols a, .fcols span{ display: inline-block; font-size: var(--t-small);
                       color: var(--ink-72); }
.fcols a{ transition: color var(--dur-card) var(--ease); }
.fcols a:hover{ color: var(--accent); }
.brk{ overflow-wrap: anywhere; }

.fico{ display: flex; gap: 8px; margin-top: var(--sp-4); }
.fico a{ width: 40px; height: 40px; border-radius: 50%;
         border: 1px solid var(--rule);
         display: inline-flex; align-items: center; justify-content: center;
         color: var(--ink-72);
         transition: color var(--dur-card) var(--ease),
                     border-color var(--dur-card) var(--ease); }
.fico a:hover{ color: var(--accent); border-color: var(--accent); }
.fico svg{ width: 16px; height: 16px; fill: none; stroke: currentColor;
           stroke-width: 1.4; }

/* Exactly three items: space-between puts the first on the left guide, the
   last on the right, and the third optically centred (§10.3). */
.bot{
  margin-top: var(--sp-6); padding-top: var(--sp-4);
  border-top: 1px solid var(--rule);
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-3);
  font-size: var(--t-meta); letter-spacing: .09em; text-transform: uppercase;
  color: var(--ink-55);
}
.bot a{ transition: color var(--dur-card) var(--ease); }
.bot a:hover{ color: var(--accent); }


/* ---- 17. Responsive -----------------------------------------------------
   | component     | >=981 | 861-980 | 721-860 | 361-720 | <=360 |
   | record cards  |   4   |    4    |    4    |    2    |   1   |
   | hallmarks     |   4   |    4    |    4    |    2    |   2   |
   | story blocks  |  7/5  | stacked | stacked | stacked | stack |
   | builds strip  |  1+4  |   2x2   |   2x2   |   2x2   |  2x2  |
   | work grid     |   3   |    2    |    2    |    2    |   2   |
   | renovation    |  6/5  | stacked | stacked | stacked | stack |
   | process       |   4   |    2    |    2    |    2    |   1   |
   | proof         |  2x2  |    1    |    1    |    1    |   1   |
   | closing       |  6/5  | stacked | stacked | stacked | stack |
   | footer cols   |   4   |    2    |    2    |    2    |   2   |
   ------------------------------------------------------------------------ */

@media (max-width: 980px){
  /* display:none on navigation is only ever half a change — the replacement
     is written in the same rule (§11.3). */
  .navlinks, .nav .btn{ display: none; }
  .navtoggle{ display: flex; }


  .hero{ height: 100svh; min-height: 560px; }
  .hz{ max-width: 34ch; }

  /* The image earns the scroll, so it leads (§11.2). */
  .duo-pic, .duo-copy,
  .duo-rev .duo-pic, .duo-rev .duo-copy{ grid-column: 1 / -1; grid-row: auto; }
  .duo-pic{ order: -1; margin-bottom: var(--sp-5); }
  .duo-copy .bd, .duo-copy .d{ max-width: none; }
  .inset, .duo-rev .inset{ right: 0; left: auto; bottom: -7%; width: 30%; }

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

  .reno-pics, .reno-copy{ grid-column: 1 / -1; }
  .scores{ grid-template-columns: repeat(2, minmax(0,1fr)); }
  .score-note{ grid-column: 1 / -1; }
  .quotes{ grid-template-columns: minmax(0,1fr); row-gap: var(--sp-6); }

  .closer-pic, .closer-panel{ grid-column: 1 / -1; }
  /* the cap stays: 740px is the whole of this photograph and the stacked
     container is wider than that from 800px up */
  .closer-pic{ min-height: 0; aspect-ratio: 16 / 9; }

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

@media (max-width: 720px){
  .hero{ height: 100svh; }
  .hz{ max-width: none; }
  /* The cue is a desktop affordance: on a touch screen the gesture is already
     the obvious one, and the control only competes with the call to action. */
  .scrollcue{ display: none; }
  .hz-wrap{ padding-top: calc(var(--sp-5) + var(--nav-h)); padding-bottom: var(--sp-5); }

  .cards4, .marks{ grid-template-columns: repeat(2, minmax(0,1fr)); }
  .workgrid{ gap: var(--sp-6) var(--colgap); }

  /* ---- The footer, compacted (§10.6) ------------------------------------
     It ran to 1480px on a 390px screen — one and three quarter screens of
     footer, most of it air. Nothing here removes a link or a line of copy;
     the height came out of gaps that were tuned for a four-column desktop
     layout and then inherited by a stack, where they read as holes:

       .foot      padding-block  --sec (60px) above
       .foot-top  --sp-6 (64px) row gap, --sp-6 padding-top
       .fnav      --sp-5 (40px) margin-top
       .fcols     --sec (60px) margin-top + --sp-5 padding-top
       .bot       --sp-6 (64px) margin-top, then three stacked 44px rows

     A section-sized gap INSIDE one component is the mistake: --sec is the
     measure between components, and using it between a footer's own blocks
     tells the eye they are unrelated.

     Second pass, asked for again: every remaining gap down one more step.
     484px of the 1253 was still whitespace, so the scale below is explicit
     rather than inherited — one 28px rhythm between blocks, 12 to 16 inside
     them — instead of reaching for --sp-5 and --sp-6, which are page-scale
     tokens that a footer column does not need. */
  .foot{ padding-block: 32px 20px; }
  .foot-top{ gap: 28px; padding-top: 28px; }
  .fbrand .cap{ margin-top: 12px; }
  .fnav{ margin-top: 14px; gap: 0 var(--sp-4); }
  .fform{ padding-top: 28px; border-top: 1px solid var(--rule); }
  .fform .cap{ max-width: none; margin-top: 12px; }
  .nl{ margin-top: 18px; }
  /* The note reserved 1.5em of empty status line on every load to avoid a
     shift. It is empty until the form is submitted, and a shift the visitor
     caused while looking straight at the note is not the shift that rule is
     about — so the 32px goes back to the footer. */
  .nl-note{ min-height: 0; margin-top: 8px; }
  .fcols{ margin-top: 28px; padding-top: 28px; gap: 28px var(--colgap); }

  /* Two columns, as the reference does it: BUILD | SERVICE, SERVING | CONTACT.
     Stacking four lists is what turns a footer into a scroll. */
  .fcols{ grid-template-columns: repeat(2, minmax(0,1fr)); }
  .fwide{ grid-column: auto; }
  /* A rule under each heading gives the pair a shared baseline, so two
     columns of different length still read as one row. */
  .fcols h3{ margin-bottom: 8px; padding-bottom: 6px;
             border-bottom: 1px solid var(--rule); }

  .fico{ margin-top: 12px; }
  /* 28 characters of email address in a 167px column. `overflow-wrap:anywhere`
     kept it inside the box but broke it mid-word — "camppoolbuildersll / c.com"
     — which is worse than small. One step down fits the domain on its own
     line and the break lands after the @, where a reader expects it.
     `anywhere` stays, not `break-word`: only `anywhere` shrinks the element's
     min-content width, and without that the inline-block just grew past its
     167px column instead of wrapping inside it. */
  .fwide .brk{ font-size: .78rem; }
  /* One line, as the reference does it. Stacked, the two links were 44px each
     on top of the copyright — 124px of bottom rule — and both of them repeat
     the number and the address that sit in the Contact column a finger's
     width above. Nothing becomes unreachable; a duplicate stops being paid
     for twice. */
  .bot{ margin-top: 28px; padding-top: 14px;
        flex-direction: row; align-items: baseline; gap: 0; }
  /* .foot .bot, not .bot: the touch block below sets `.bot a{display:inline-block}`
     for tablets, it is later in the file, and at equal specificity later wins. */
  .foot .bot a{ display: none; }
}

@media (max-width: 360px){
  .cards4, .steps{ grid-template-columns: minmax(0,1fr); }
  .reno-pics{ grid-template-columns: minmax(0,1fr); }
}

/* A hero that runs a little long beats content that cannot be read (§7.5). */
@media (max-width: 720px) and (max-height: 720px){
  .hero{ height: auto; min-height: 100svh; }
  .hz-wrap{ padding-top: calc(var(--sp-4) + var(--nav-h)); padding-bottom: var(--sp-4); }
  .hz h1{ margin-top: var(--sp-2); font-size: clamp(1.7rem, 7.6vw, 2.2rem); }
  .hz .deck{ margin-top: var(--sp-2); font-size: .9rem; line-height: 1.5; }
  .hz .row{ margin-top: var(--sp-3); }
}

@media (hover: none){
  .fr-hover:hover img{ transform: none; }
  .card:hover{ transform: none; box-shadow: var(--lift); }
  .navlinks a[aria-current="true"]::after{ transform: scaleX(1); }
  /* 44px hit areas on touch, bought with padding on the footer lists whose
     gap then goes to zero — so the pitch is exactly 44px and no two hit areas
     overlap (§15.2). */
  .fcols ul{ gap: 0; }
  /* Links only. The Serving column is five plain <span>s — place names, not
     controls — and padding them to a 44px target bought 100px of footer for
     something nobody can tap. A hit area is for things that are hit. */
  .fcols a{ padding-block: 13px; }
  .fcols li:has(> span){ padding-block: 4px; }
  /* The four controls still under the 44px floor, three of them the page's
     primary calls to action. None of these changes anything you can see: the
     buttons already have 13px of padding and only needed a floor, the submit
     circle grows 4px inside a 54px field, and the masthead link gains hit
     area around a logo that keeps its own height. */
  .fico a{ width: 44px; height: 44px; }
  .btn{ min-height: 44px; }
  .nl button{ width: 44px; height: 44px; right: 5px; }
  .brand{ padding-block: 7px; }
  .fnav a{ display: inline-block; padding-block: 12px; }
  .bot a{ display: inline-block; padding-block: 13px; }
  .cta-steps a{ display: inline-block; padding-block: 12px; }
}


/* ---- 18. Reduced motion (§5.10) -----------------------------------------
   Neutralised at the token and base-class level, never per component. */

@media (prefers-reduced-motion: reduce){
  .rv, .rv-l, .rv-r, .rv-s{
    opacity: 1 !important; transform: none !important; transition: none !important; }
  .hz, .hz .eb, .hz h1, .hz .deck, .hz .row, .scrollcue, #main-nav{
    animation: none !important; transform: none !important; }
  .mp-art, .mp-art > *{ animation: none !important; transform: none !important; }
  .navpanel, .navpanel li, .navtoggle span{ transition: none !important; }
  .fr-hover:hover img, .card:hover{ transform: none !important; }
  .px > img{ transform: none !important; }
}


/* ---- 19. Reveal transitions, LAST in the file ---------------------------
   The transition shorthand REPLACES rather than merges, so a component
   declaring its own `transition:` later in the file would silently kill
   these (§5.3). */
.rv, .rv-l, .rv-r, .rv-s{ transition: transform var(--dur-rv) var(--ease); }
.rv.in, .rv-l.in, .rv-r.in, .rv-s.in,
.card.rv.in, .card.rv-l.in, .card.rv-r.in, .card.rv-s.in,
.plate.rv-l.in, .plate.rv-r.in, .plate.rv-s.in{ transform: translate3d(0,0,0); }
