/* ============================================================================
   CMD Constructions — homepage mockup, built to the measured spec
   Vanilla CSS. No dependencies, no CDN, no build step.

   THE SPEC (brief/BENCHMARK.md, measured off seven premium builder sites):
     page height  under ~5,000px at 1440      (category median 4,273px)
     words        under ~400                  (category median 312)
     images       8-12                        (Futureflip runs on 4)
     body         14-16px at weight 300
     headings     42-74px at weight ~500, set slightly narrow
     one typeface, square corners, projects named
     absence is the design: no services grid, no testimonial carousel,
     no stats bar, no material index, no "why choose us" row

   GROUND. Warm white dominant with deep olive bands at the hero and the
   close. Priestley (the keeper) runs roughly this way; the wider field is
   light-dominant; and the last three attempts were all dark and all wrong.

   PALETTE, muted from Cade's own logo (#27AAE0 / #ACD58A / #E25829).
   ========================================================================== */

/* Montserrat is now the brand's typeface: Dan's settled mark sets CONSTRUCTIONS
   in it, light and widely tracked, and the site takes the same face so the page
   and the logo are one system rather than two.

   ⚠️ Montserrat has a WEIGHT AXIS ONLY — no wdth. Every `font-stretch` in this
   file was doing real work under Archivo (headings ran at 88%) and would now be
   silently ignored, so the display sizes and tracking below are re-cut for a
   geometric face that sets considerably wider at the same pixel size. Do not
   reintroduce font-stretch here; change the size or the tracking instead. */
@font-face {
  font-family: 'Montserrat';
  src: url('../fonts/montserrat-var-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  --paper:     #F6F1E9;   /* the ground */
  --paper-2:   #EDE7DC;   /* inset surface */
  --ground:    #2B3630;   /* deep olive, the hero and the close */
  --ground-2:  #333F38;
  --deep:      #1E2723;

  --ink:       #232B26;   /* body text on paper */
  --muted:     #5C685F;   /* secondary on paper */
  --on-dark:   #F6F1E9;
  --muted-dark:#AFB9AE;

  --clay:      #9C5339;   /* text-safe clay on paper */
  --clay-dark: #D5926F;   /* text-safe clay on the olive ground: 4.88:1 */
  --clay-photo:#F3DFD0;   /* the same accent over a PHOTOGRAPH, where the
                             backdrop is foliage rather than a flat ground and
                             --clay-dark measures 2.45:1. Warm off-white keeps
                             the accent legible without veiling the picture. */
  --sage:      #6B7C5C;
  --slate:     #46697B;

  --rule:      #D2CABB;   /* hairline on paper */
  --rule-dark: rgba(246,241,233,.18);
  --edge:      #A99E8C;   /* control borders on paper */

  --sans: 'Montserrat', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;

  /* four text sizes, three display steps, nothing between */
  --t-xs:   0.6875rem;  /* 11px  labels */
  --t-sm:   0.8125rem;  /* 13px  meta */
  --t-md:   0.9375rem;  /* 15px  body — inside the measured 14-16 band */
  --t-lg:   1.0625rem;  /* 17px  standfirst only */

  /* Re-cut for Montserrat. MEASURED, not guessed: the same string set in
     Montserrat runs 32.7% wider than it did in Archivo at font-stretch 88%,
     and 16.2% wider at body weight. Display therefore comes down ~20% — less
     than the full 25% the width says, because Montserrat's cap height is the
     smaller of the two per pixel, so a straight width match would leave the
     headings optically undersized. 54px at the top of d1 sits inside the
     benchmark's measured 36–54px band for the category. */
  --d1: clamp(2.05rem, 4.15vw, 3.35rem);  /* 33 → 54px */
  --d2: clamp(1.4rem, 2.4vw, 2rem);       /* 22 → 32px */
  --d3: clamp(1.1rem, 1.5vw, 1.38rem);    /* 18 → 22px */

  --track: 0.15em;

  --s1:.25rem; --s2:.5rem; --s3:.75rem; --s4:1rem; --s5:1.5rem; --s6:2rem; --s7:3rem;
  --band: clamp(2.75rem, 5.4vw, 4.75rem);
  --wrap: 1200px;
  --pad: clamp(1.25rem, 4vw, 3.25rem);
  --headh: 4.5rem;
  /* ⚠️ THE BAND AND THE HERO ARE ONE MEASUREMENT, NOT TWO. Cade's line sits in a
     full-width band at the bottom of the first screen, and "the first screen" only
     works if hero + band == exactly 100svh. This token is the band; the hero takes
     whatever is left (see .hero__in). Defining it once is what stops a floor on one
     of them silently pushing the other past the fold — which it did at 1280x620,
     where 8svh is 49.6px and the 3.25rem floor made the pair 622px against a 620px
     window. */
  --byline-h: max(8svh, 3.25rem);
}

*,*::before,*::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
section[id] { scroll-margin-top: var(--headh); }

body {
  margin: 0; background: var(--paper); color: var(--ink);
  font-family: var(--sans); font-size: var(--t-md); font-weight: 300;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased; overflow-x: hidden;
}
/* Montserrat is a wide geometric face and reads dense at display weight, so the
   headings run LIGHTER than the Archivo build did (500 → 400) and take less
   negative tracking (-.028 → -.02); -.028em on a round geometric closes the
   counters. The light register is also the argument the settled mark makes:
   CONSTRUCTIONS is set light and widely tracked, and the page echoes it. */
h1,h2,h3 { margin: 0; font-weight: 400; line-height: 1.08; letter-spacing: -.02em; }
p { margin: 0; }
ul,ol { margin: 0; padding: 0; } li { list-style: none; }
img,video { display: block; max-width: 100%; height: auto; }
figure { margin: 0; }
a { color: inherit; }
button,input,textarea,select { font: inherit; color: inherit; }
button { cursor: pointer; }
a,button,input,textarea,select,label { touch-action: manipulation; }
:focus-visible { outline: 2px solid var(--clay); outline-offset: 3px; }
/* ⚠ --clay is a PAPER colour. On the olive ground it measures 2.22:1, which
   fails even the 3:1 non-text threshold, so the focus ring was effectively
   invisible on every dark band — the form, the footer, the close and the
   awards strip. --clay-dark is 4.88:1 there. Measured with scripts/contrast.py,
   not eyeballed. */
.hero :focus-visible, .ask :focus-visible, .awards :focus-visible,
.close :focus-visible, .foot :focus-visible { outline-color: var(--clay-dark); }

.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--pad); }
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden;
  clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0; }

.skip { position:absolute; left:var(--s3); top:-6rem; z-index:90; background:var(--ink);
  color:var(--paper); padding:var(--s3) var(--s5); font-size:var(--t-md); font-weight:500;
  text-decoration:none; transition:top .18s ease; }
.skip:focus { top: var(--s3); }

.lab { font-size:var(--t-xs); font-weight:600; letter-spacing:var(--track);
  text-transform:uppercase; color:var(--clay); }
.lab--dark { color: var(--clay-dark); }

/* ── header ─────────────────────────────────────────────────────
   Sits over the olive hero, so it starts transparent and only gains a
   ground once the hero has gone by. */
.head { position:fixed; inset:0 0 auto 0; z-index:50; height:var(--headh);
  display:flex; align-items:center; transition:background .3s ease, border-color .3s ease;
  border-bottom:1px solid transparent; }
/* HEADER CHROME IS THE HEADER'S PROBLEM. Thinning the hero scrim so the house
   reads dropped the nav to 1.72:1 over bright sky — measured at four of five
   widths by scripts/hero-contrast.py. The wrong fix is putting the veil back
   over the whole photograph; the right one is a band the height of the header
   itself, which reads as UI rather than as a wash over the picture.
   The alpha is derived, not eyeballed: .78 of #1E2723 takes the brightest tenth
   of the sky from ~225 to ~108 in sRGB, which is the value that clears 4.5:1
   for #F6F1E9. Negative z-index puts it behind the header's own content but
   still above the hero, and it has to switch off once .solid paints a real
   background — an element's own background paints BELOW its negative-z children. */
.head::before { content:""; position:absolute; inset:0 0 auto 0; z-index:-1;
  height:calc(var(--headh) * 1.9); pointer-events:none;
  background:linear-gradient(to bottom, rgba(30,39,35,.78) 0, rgba(30,39,35,.62) 38%,
    rgba(30,39,35,.26) 72%, rgba(30,39,35,0) 100%);
  transition:opacity .3s ease; }
.head.solid::before { opacity:0; }
.head.solid { background:var(--paper); border-bottom-color:var(--rule); }
.head__in { display:flex; align-items:center; gap:var(--s5); width:100%;
  max-width:var(--wrap); margin-inline:auto; padding-inline:var(--pad); }
.mark { display:inline-flex; align-items:center; min-height:44px; }
.mark img { width:132px; }
.head .mark img.on-dark { display:block; }
.head .mark img.on-light { display:none; }
.head.solid .mark img.on-dark { display:none; }
.head.solid .mark img.on-light { display:block; }
.nav { margin-left:auto; display:flex; align-items:center; gap:clamp(.85rem,2vw,1.9rem); }
.nav a { font-size:var(--t-sm); font-weight:500; text-decoration:none; color:var(--on-dark);
  min-height:44px; display:inline-flex; align-items:center; transition:color .18s ease; }
.nav a:hover { color:var(--clay-dark); }
.head.solid .nav a:hover { color:var(--clay); }
.head__tel { font-size:var(--t-sm); font-weight:600; color:var(--on-dark); text-decoration:none;
  min-height:44px; display:inline-flex; align-items:center; border-bottom:1px solid var(--clay-dark); }
.head.solid .nav a, .head.solid .head__tel { color:var(--ink); }
.head.solid .head__tel { border-bottom-color:var(--clay); }
@media (max-width: 640px) { .nav a.nav--x { display:none; } .head__in { gap:var(--s4); } }

/* ── hero ───────────────────────────────────────────────────────
   FULL LOADING HEIGHT. 100svh, not 100vh or 100dvh: svh is the SMALL
   viewport height, which is exactly what is visible at load on a phone
   while the browser chrome is still expanded. 100vh overflows under that
   chrome and clips the standfirst; 100dvh starts at the same place but
   then grows as the chrome retracts, which resizes the hero mid-scroll
   and drags the whole page with it. svh is the one that means "the height
   it loads at". 100vh stays first as the fallback for anything old.

   THE SCRIM IS THE PICTURE'S PROBLEM, NOT THE TYPE'S. The previous version
   washed the frame to 94% of its height to carry an 11px label, and buried
   the house — which is the only reason the photograph is there. Two things
   pay for taking that away: the hero is now half again as tall, so the type
   block occupies proportionally much less of the frame, and the label keeps
   its own small local ground (below) instead of taxing the whole image.
   Everything here was re-measured after the change; see scripts/contrast.py. */
.hero { position:relative; background:var(--ground); color:var(--on-dark); }
.hero__frame { position:absolute; inset:0; }
.hero__frame img, .hero__frame video { width:100%; height:100%; object-fit:cover; }

/* PHONE first: the type spans the full width there, so a full-width band is
   the only thing that can carry it — but it now starts at 62% rather than 94%,
   so the top two-thirds of the frame are clean. */
.hero::after { content:""; position:absolute; inset:0; pointer-events:none;
  background:
    linear-gradient(to top, rgba(30,39,35,.92) 0, rgba(30,39,35,.86) 18%,
      rgba(30,39,35,.68) 34%, rgba(30,39,35,.38) 48%, rgba(30,39,35,.12) 57%,
      rgba(30,39,35,0) 62%); }

/* DESKTOP: the type only ever occupies the bottom-left — h1 is 16ch, the
   standfirst 46ch — so ground THAT CORNER and let the frame open up across
   the soffit, the cladding and the glazing to the right. The corner radial is
   tighter than it was (105%/125% against 130%/175%) because a taller hero
   puts more picture between the top of the type and the top of the frame. */
@media (min-width: 820px) {
  .hero::after {
    background:
      radial-gradient(112% 145% at 0% 100%, rgba(30,39,35,.93) 0%,
        rgba(30,39,35,.85) 28%, rgba(30,39,35,.66) 46%,
        rgba(30,39,35,.32) 62%, rgba(30,39,35,.07) 76%, rgba(30,39,35,0) 86%),
      linear-gradient(to top, rgba(30,39,35,.42) 0, rgba(30,39,35,.14) 16%,
        rgba(30,39,35,0) 34%);
  }

  /* The label is 11px type at the TOP of the content block, where the corner
     scrim has already fallen away, and it lands on sunlit fronds. A scrim
     strong enough to carry it on its own measured 6.6:1 but visibly veiled the
     house. So the label carries a soft local ground instead: it costs the
     picture a patch the width of two words rather than the whole lower half.

     align-self, NOT display:inline-block — .lab is a flex item of .hero__in,
     and flex items blockify, so inline-block computes to block and the ground
     stretches the full column as a visible band. */
  .hero .lab { position:relative; align-self:flex-start; }
  .hero .lab::before {
    content:""; position:absolute; z-index:-1;
    inset:-.85em -2.4em -.85em -1.8em;
    background:radial-gradient(58% 100% at 42% 50%, rgba(30,39,35,.88) 0%,
      rgba(30,39,35,.62) 52%, rgba(30,39,35,0) 100%);
  }
}
/* belt and braces: the contrast script measures type against its CSS
   background, so it cannot see a photograph. This guarantees the hero
   reads over whatever frame ends up in the slot. */
.hero .lab, .hero h1, .hero__sub, .hero__meta { text-shadow: 0 1px 20px rgba(30,39,35,.85); }
.hero .lab { color: var(--clay-photo); }

/* ⚠️ NOT A FIXED 92svh — 100svh MINUS THE BAND. The band has a floor (a short
   landscape window would otherwise pinch it to nothing), and a hardcoded 92 does
   not know about that floor, so the pair overshot the fold by 2px at 1280x620.
   Subtracting keeps hero + band at exactly one small viewport at every height,
   which is the whole point: Cade's line at the bottom of the first screen, above
   the fold, always. The 92vh above is the fallback for anything without svh.
   (svh, not vh or dvh, for the reason set out at the top of this section: it is
   the height the page LOADS at, with the browser chrome expanded.) */
.hero__in { position:relative; z-index:2;
  min-height:92vh; min-height:calc(100svh - var(--byline-h));
  display:flex; flex-direction:column; justify-content:flex-end;
  padding-block:var(--headh) clamp(2.25rem,4vw,3.5rem); }

/* Light, and tracked a little tighter than the body — the register the
   settled mark sets with its own wordmark. */
.hero h1 { margin-top:var(--s4); font-size:var(--d1); font-weight:300; max-width:17ch;
  text-wrap:balance; letter-spacing:-.028em; }
.hero__sub { margin-top:var(--s5); max-width:46ch; font-size:var(--t-lg); color:var(--on-dark);
  opacity:.92; }

/* A quiet cue that there is more below, which a full-height hero needs and a
   three-quarter-height one does not: at 74vh the next section was already
   peeking. Hidden for reduced motion and on short landscape windows where it
   would collide with the standfirst. */
.hero__more { position:absolute; left:50%; bottom:1.15rem; z-index:2;
  translate:-50% 0; width:1px; height:clamp(1.6rem,3vh,2.4rem);
  background:linear-gradient(to bottom, rgba(246,241,233,0), rgba(246,241,233,.75));
  animation:heroMore 2.6s ease-in-out infinite; }
@keyframes heroMore { 0%,100%{opacity:.25;transform:scaleY(.55);transform-origin:top}
  50%{opacity:1;transform:scaleY(1);transform-origin:top} }
@media (prefers-reduced-motion:reduce) { .hero__more{animation:none;opacity:.6} }
@media (max-height: 620px) { .hero__more{display:none} }
/* On a phone the type block fills most of the frame and the cue lands on top of
   the standfirst's last line. It is also redundant there — a hero that fills the
   screen with a paragraph running to the bottom edge already reads as "there is
   more". Desktop keeps it, where the bottom centre is genuinely empty. */
@media (max-width: 819.98px) { .hero__more{display:none} }

/* ── the statement ──────────────────────────────────────────── */
.intro { padding-block:var(--band); }
.intro p { font-size:var(--d3); font-weight:400; line-height:1.45;
  letter-spacing:-.012em; max-width:34ch; }
.intro p + p { margin-top:var(--s5); font-size:var(--t-md); font-weight:300; line-height:1.6;
  letter-spacing:0; color:var(--muted); max-width:52ch; }
@media (min-width: 900px) {
  .intro .wrap { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:var(--s7); align-items:start; }
  .intro p + p { margin-top:.4rem; }
}

/* ── the work, as a reel ────────────────────────────────────────
   One project at a time on a scroll-snap track. Native swipe on touch,
   arrows and arrow-keys on desktop, and it still scrolls with JS off —
   the controls are the only thing that needs script, so they are hidden
   until .js is present rather than sitting there doing nothing.

   No dots. Dots are the tell of a carousel component; a counter and two
   arrows say the same thing and take a quarter of the room. Showing one
   project at a time instead of three stacked took about 1,700px off the
   page, which is where the space for a fourth and fifth project comes
   from once the August photography lands. */
/* ⚠️ padding-BLOCK, not padding-bottom. This section had no top padding at all
   for a month and looked right, because it used to follow .intro and was sitting
   in the bottom half of .intro's own --band. Moving the film in between the two
   on 9 Sep 2026 took that away and left "SELECTED WORK" flush against the bottom
   edge of the dark film band — measured at 0px, at 1440 AND 390.

   The lesson is the general one: a section that gets its top space from its
   NEIGHBOUR'S bottom padding is only correct while that neighbour stays put.
   Every band on this page now owns its own padding-block. */
.work { padding-block:var(--band); }
.work__head { display:flex; align-items:center; justify-content:space-between;
  gap:var(--s5); flex-wrap:wrap; padding-bottom:var(--s4); border-bottom:1px solid var(--rule); }

.reel__nav { display:none; align-items:center; gap:var(--s4); }
.js .reel__nav { display:flex; }
.reel__count { font-size:var(--t-xs); font-weight:600; letter-spacing:var(--track);
  color:var(--muted); font-variant-numeric:tabular-nums; }
.reel__count b { color:var(--ink); font-weight:600; }
.reel__btn { width:44px; height:44px; display:grid; place-items:center;
  background:transparent; border:1px solid var(--rule); border-radius:0; color:var(--ink);
  transition:background .16s ease, border-color .16s ease, color .16s ease, opacity .16s ease; }
.reel__btn:hover:not(:disabled) { background:var(--ink); border-color:var(--ink); color:var(--paper); }
.reel__btn:disabled { opacity:.32; cursor:default; }
.reel__btn svg { width:15px; height:15px; fill:none; stroke:currentColor; stroke-width:1.5; }

.reel__track { display:flex; gap:var(--s5); overflow-x:auto; scroll-snap-type:x mandatory;
  overscroll-behavior-x:contain; scrollbar-width:none; -ms-overflow-style:none;
  margin-top:clamp(1.75rem,3.4vw,2.6rem); }
.reel__track::-webkit-scrollbar { display:none; }
@media (prefers-reduced-motion: no-preference) { .reel__track { scroll-behavior:smooth; } }
.reel__slide { flex:0 0 100%; scroll-snap-align:start; min-width:0; }
/* Below the arrows' comfortable size, let the next slide peek. On a phone
   the controls sit at the top of the section and are easy to miss once you
   are looking at the photograph; a sliver of the next frame is the cue. */
@media (max-width: 819.98px) { .reel__slide { flex-basis:86%; } }

.proj__lead img { width:100%; aspect-ratio:16/9; object-fit:cover; background:var(--paper-2); }
.proj__cap { margin-top:var(--s5); display:grid; gap:var(--s5); }
.proj__n { font-size:var(--d3); }
.proj__meta { margin-top:var(--s2); font-size:var(--t-xs); font-weight:600;
  letter-spacing:var(--track); text-transform:uppercase; color:var(--muted); }
.proj__d { margin-top:var(--s4); color:var(--muted); max-width:46ch; }
.proj__detail img { width:100%; aspect-ratio:1/1; object-fit:cover; background:var(--paper-2); }
@media (min-width: 820px) {
  .proj__cap { grid-template-columns:minmax(0,1fr) 15rem; gap:var(--s7); align-items:start; }
  .proj__detail { margin-top:-4.5rem; }
}

/* ── Cade ───────────────────────────────────────────────────────
   The portrait slot was composed in August to hold a photograph that did
   not exist yet, and took the real one from the 21 Aug shoot without a
   single change to the layout. That was the point of building it empty.
   Never a silhouette, an avatar or an illustration. */
/* ⚠️ A DARK BAND, AND THAT IS THE POINT. The middle of this page was four
   consecutive sections on the same paper — intro, work, who, proof — one
   undifferentiated pale run about 60% of the page long, and it read as flat.
   This is the break.

   ⚠️ --deep, NOT the olive --ground. The olive is reserved for the two ENDS of
   the page (hero and close) and a third olive band starts to fuse them into a
   theme rather than a frame. --deep is a distinct near-black, it is already the
   footer's ground so it is not a new colour, and it gives the film something to
   sit in: the black of the player runs into the black of the band instead of
   being a rectangle stranded on cream.

   Measured on --deep #1E2723, not eyeballed: --on-dark 13.64:1, --muted-dark
   7.57:1, --clay-dark 5.97:1. */
.who { background:var(--deep); color:var(--on-dark); padding-block:var(--band); }
.who .lab { color:var(--clay-dark); }
.who h2 { margin-top:var(--s4); font-size:var(--d2); max-width:15ch; text-wrap:balance; }
.who p { margin-top:var(--s4); color:var(--muted-dark); max-width:46ch; }
.who__tel { margin-top:var(--s6); display:inline-flex; align-items:center; min-height:44px;
  font-size:var(--d3); font-weight:500; letter-spacing:-.015em; color:var(--on-dark);
  text-decoration:none; border-bottom:1px solid var(--clay-dark); }
.who__tel:hover { color:var(--clay-dark); }

/* Portrait left, words right.

   ⚠️ REBUILT 3 SEPTEMBER 2026 AROUND FRAME 4851. The previous version ran a 2:5
   sliver of frame 4703 and every number in it was cut to that sliver. 4851 is a
   standing three-quarter portrait at a native 2:3 and will not survive being made
   thin — a 2:5 crop takes the shoulder and the hand on the hip with it. Julian's
   call, and the frame is the reason: cut the column to the picture.

   THE COLUMN IS DERIVED, NOT PICKED. At 1440 the content box is 1096 and the gap
   64, leaving 1032 across both columns. At .72fr the frame lands at 432px and a 2:3
   crop stands 648px, which sits just inside the ~683px the copy runs to — so the
   two columns finish within a line of each other and the row reads as one block.
   ⚠️ Re-derive it if the copy length or the measure changes; the old 2:5 geometry
   is gone and none of its numbers apply. */
.who__grid { display:grid; gap:var(--s6); }
/* ⚠️ THE CAP IS A SHARE OF THE COLUMN, NOT A FIXED 20rem. Julian, 3 Sep: "Cade's
   portrait on mobile is odd" — and it was, though the fault was worst on a tablet
   rather than a phone. A flat 20rem cap put a 320px picture in the top-left of a
   768px-wide dark band with 417px of nothing beside it, which reads as a broken
   thumbnail rather than a portrait.

   28rem instead, and it resolves two different ways on purpose: on a phone
   min() takes the 100% branch, so the picture runs the full content width and the
   dead strip disappears; from about 500px up it takes the 28rem branch and the
   remaining space reads as a deliberate asymmetric margin rather than a gap.
   Left-aligned throughout — the whole section is on a hard left grid and a centred
   picture over left-aligned type would fight it. */
.who__fig { margin:0; min-height:0; max-width:min(28rem, 100%); }
.who__fig img { width:100%; aspect-ratio:2/3; object-fit:cover; background:var(--ground-2); }
/* ⚠️ 760px, NOT 900. Between the two the section was single-column and WIDE, which
   left a 448px picture sitting in the top-left of an 827px dark band. Two columns
   from 760 keeps the text at a 45ch measure there — checked, not assumed — and the
   picture stops being stranded. Below 760 the picture runs full width instead.

   ⚠️ align-items:start, NOT center. It was centred while the image was a 2:5 column
   cut to exactly the height of the text; a 2:3 picture is shorter than the copy at
   every width under about 1400, and centring a short thing against a long thing
   floats it with dead column above AND below. Aligned to the top, the picture and
   the "THE BUILDER" label start on the same line, the columns simply run to
   different depths, and it reads as ordinary editorial two-column rather than as a
   photograph that has come loose. At 1440 the two finish 11px apart, so this costs
   nothing where they do balance. */
@media (min-width: 760px) {
  .who__grid { grid-template-columns:minmax(0,.72fr) minmax(0,1fr);
    gap:clamp(2rem,5vw,4rem); align-items:start; }
  .who__fig { max-width:none; }
  /* ⚠️ 50ch, not the 46ch the rest of the page uses. A wider frame leaves a wider
     text column beside it, and 46ch left ~200px of dead paper to the right of every
     paragraph. 50ch is still inside the benchmark's measure band and it is the
     measure the ROW needs, not a general loosening — hence scoped to this breakpoint
     and this section. (56ch was tried on 27 Aug under the old geometry and reverted
     for a reason that no longer applies: it broke a height match that is gone.) */
  .who__body p { max-width:50ch; }
}

/* ── the film band ──────────────────────────────────────────────
   Moved out of .who and up under the statement on 9 Sep 2026 (Julian).
   Its own dark band, for the reason the film was on --deep inside .who in
   the first place: the black of the player runs into the black of the
   ground instead of sitting as a rectangle on cream. Full measure, no cap
   — it is the piece the page is built around.

   ⚠️ Tighter padding than --band. This band exists to carry one 16:9 frame
   and nothing else, so a full --band top and bottom made it read as an
   empty section with a video parked in it. */
.filmband { background:var(--deep); color:var(--on-dark);
  padding-block:clamp(2rem,4vw,3.25rem); }
.filmband .lab { display:block; margin-bottom:var(--s4); }
.filmband__film { margin:0; }

/* The Brand Story film, full measure. ⚠️ .who__film is KEPT even though the
   film has moved out of .who: the rule is two lines, the section may want a
   film back beside the portrait, and nothing is served by deleting it and
   re-deriving it later. */
.who__film { margin-top:clamp(2.5rem,5vw,4rem); }
.who__filmnote { margin-top:var(--s3); font-size:var(--t-xs); font-weight:600;
  letter-spacing:var(--track); text-transform:uppercase; color:var(--muted-dark); }

/* ── the proof ──────────────────────────────────────────────────
   Work, then the man, then the client.

   ⚠️ NOT olive. The first cut of this section grounded it in --ground, and
   it fused with the olive enquiry band directly beneath it into one
   undifferentiated dark run about 40% of the page long — two sections
   reading as one, and the "deep olive bands at the hero and the close"
   rule in the header of this file quietly broken by adding a third. The
   inset paper is a tonal step rather than a new colour, the olive stays
   reserved for the two ends of the page, and the film supplies its own
   darkness: a 16:9 black rectangle on warm paper is a strong enough object
   without a band built around it. */
.proof { background:var(--paper-2); padding-block:var(--band);
  border-top:1px solid var(--rule); }
.proof__grid { display:grid; gap:var(--s6); align-items:center; }
.proof h2 { margin-top:var(--s4); font-size:var(--d2); max-width:14ch; text-wrap:balance; }
.proof__p { margin-top:var(--s5); color:var(--muted); max-width:40ch; }
.proof__q { margin:var(--s5) 0 0; }
.proof__q p { font-size:var(--d3); line-height:1.25; letter-spacing:-.01em;
  color:var(--ink); max-width:22ch; text-wrap:balance; }
.proof__by { margin-top:var(--s4); font-size:var(--t-sm); color:var(--ink); }
/* ⚠️ .proof__note carried "Preview cut — the finished film follows" while the
   testimonial was a 40-second demo snippet. The finished cut landed on 3 September
   and the label came off with it, so the rule has no user. Kept, not deleted: this
   section is the natural home for a status line and one will be wanted again if a
   second testimonial is ever cut. If it is still unused a year from now, delete it. */
/* ⚠️ PROVISIONAL COPY MARKER. The reel already carries plain "Name and year to
   confirm" text, but a role line buried in a spec table needs to be visibly
   unfinished or it reads as fact. Clay on paper is --clay, which is text-safe there
   (the olive-ground --clay-dark is the wrong token on this background).
   Delete the class AND the rule together once the copy is settled — a leftover
   .tbc on finished copy is worse than none. */
.tbc { color:var(--clay); font-weight:500; }
dd.tbc { font-style:normal; }

.proof__note { margin-top:var(--s2); font-size:var(--t-xs); font-weight:600;
  letter-spacing:var(--track); text-transform:uppercase; color:var(--muted); }

/* aspect-ratio on the WRAPPER, so the block holds its space before the
   player loads and the page does not jump when it does. */
.film__box { position:relative; aspect-ratio:16/9; background:var(--deep); }
.film__box iframe { position:absolute; inset:0; width:100%; height:100%; border:0; }
@media (min-width: 900px) {
  .proof__grid { grid-template-columns:minmax(0,.62fr) minmax(0,1fr); gap:clamp(2rem,5vw,4rem); }
}

/* ── enquire ────────────────────────────────────────────────── */
.ask { background:var(--ground); color:var(--on-dark); padding-block:var(--band); }
.ask h2 { margin-top:var(--s4); font-size:var(--d2); max-width:15ch; text-wrap:balance; }
.ask__p { margin-top:var(--s5); color:var(--muted-dark); max-width:42ch; }
.ask__grid { display:grid; gap:var(--s7); }
/* ⚠️ THE FORM COLUMN WIDENED AND THE GAP CAME IN, 9 Sep 2026, because the form
   became a CARD. At .8fr/1fr with a 5rem gap the card landed about 480px wide at
   1440 and its own padding then ate ~80px of that, which put the three project
   radio cards at ~120px each and wrapped every one of their labels. The panel
   needs to be paid for out of the gap and the left column, not out of the fields.
   .58fr/1fr with a 3.5rem cap puts the card at ~620px and the radios at ~180px,
   which holds "Large renovation" on one line. Re-derive if the card padding
   changes. */
@media (min-width: 900px) { .ask__grid { grid-template-columns:minmax(0,.58fr) minmax(0,1fr); gap:clamp(2rem,4vw,3.5rem); } }

.form { display:grid; gap:var(--s4); }
.f2 { display:grid; gap:var(--s4); }
@media (min-width: 560px) { .f2 { grid-template-columns:1fr 1fr; } }
.field label { display:block; font-size:var(--t-xs); font-weight:600;
  letter-spacing:var(--track); text-transform:uppercase; color:var(--muted-dark); }
.field input, .field select, .field textarea {
  width:100%; margin-top:var(--s2); background:transparent; color:var(--on-dark);
  border:0; border-bottom:1px solid var(--rule-dark); border-radius:0;
  padding:.7rem 0; min-height:44px; transition:border-color .16s ease; }
.field select { appearance:none;
  background-image:linear-gradient(45deg,transparent 50%,var(--muted-dark) 50%),linear-gradient(135deg,var(--muted-dark) 50%,transparent 50%);
  background-position:calc(100% - 11px) 22px, calc(100% - 6px) 22px;
  background-size:5px 5px,5px 5px; background-repeat:no-repeat; padding-right:var(--s6); }
.field select option { background:var(--ground); color:var(--on-dark); }
.field textarea { min-height:5.5rem; resize:vertical; }
.field input:hover, .field select:hover, .field textarea:hover { border-bottom-color:var(--muted-dark); }
.field input:focus, .field select:focus, .field textarea:focus { border-bottom-color:var(--clay-dark); }
.pot { position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden; }
.btn { margin-top:var(--s3); justify-self:start; background:var(--paper); color:var(--ink);
  border:1px solid var(--paper); border-radius:0; min-height:50px; padding:0 var(--s6);
  font-size:var(--t-sm); font-weight:600; letter-spacing:.02em;
  transition:background .16s ease, border-color .16s ease; }
.btn:hover { background:var(--clay-dark); border-color:var(--clay-dark); }
.done { border-top:1px solid var(--clay-dark); padding-top:var(--s5); }
.done h3 { font-size:var(--d3); }
.done p { margin-top:var(--s3); color:var(--muted-dark); max-width:38ch; }

/* ── the enquiry form ───────────────────────────────────────────
   Three steps, gated on `.js`. With JS off every .step renders and the
   single submit works, which is why the step chrome is the thing that is
   hidden by default rather than the content.

   ⚠ Every component below that sets its own `display` ALSO carries its own
   `[hidden]` rule. An author `display` beats the UA's `[hidden]{display:none}`,
   so without these the `hidden` attribute silently does nothing. */
.ask__form { min-width: 0; }
.tel { border-bottom:1px solid var(--rule-dark); }
.tel:hover { border-bottom-color:var(--clay-dark); }

.field__opt { font-weight:400; letter-spacing:0; color:var(--muted-dark);
  text-transform:none; font-size:var(--t-xs); }
.field__help { margin-top:var(--s2); font-size:var(--t-sm); color:var(--muted-dark);
  line-height:1.45; max-width:54ch; }
.field__help[hidden] { display:none; }
/* #F0A882 on --ground #2B3630 measures 6.35:1 — see scripts/contrast.py. */
.field__err { margin-top:var(--s2); font-size:var(--t-sm); color:#F0A882; }
.field__err[hidden] { display:none; }
.field.is-bad input, .field.is-bad select, .field.is-bad textarea { border-bottom-color:#F0A882; }
.choices.is-bad .choice__b { border-color:#F0A882; }
.field[hidden] { display:none; }

/* step chrome: only ever present when JS is driving the form */
.steps { display:none; gap:var(--s2); }
.js .steps { display:flex; }
.steps__i { flex:1 1 0; height:2px; background:var(--rule-dark); transition:background .2s ease; }
.steps__i.is-on { background:var(--clay-dark); }

.step { display:grid; gap:var(--s5); }
.step[hidden] { display:none; }
.step__h { display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--s3);
  font-size:var(--d3); }
.step__h:focus-visible { outline:2px solid var(--clay-dark); outline-offset:6px; }
.step__n { font-size:var(--t-xs); font-weight:600; letter-spacing:var(--track);
  color:var(--clay-dark); }
.step__p { margin-top:calc(var(--s5) * -1 + var(--s2)); font-size:var(--t-sm);
  color:var(--muted-dark); max-width:46ch; }

.step__nav { display:none; gap:var(--s4); align-items:center; flex-wrap:wrap; }
.js .step__nav { display:flex; }
.step__nav .btn { margin-top:0; }

/* the submit lives outside the steps so a no-JS visitor always has one */
.js .form__send[hidden] { display:none; }

/* Radio cards. ⚠ The selected state is a clay EDGE, never a clay fill:
   --clay-solid measures 3.77:1 with dark text and 3.78:1 with light, so it
   fails AA as a ground either way. State is also carried by weight and by a
   printed square, so it does not rest on colour alone. */
.choices { border:0; margin:0; padding:0; min-inline-size:0; }
.choices legend { padding:0; font-size:var(--t-xs); font-weight:600;
  letter-spacing:var(--track); text-transform:uppercase; color:var(--muted-dark); }
.choices__g { display:grid; gap:var(--s3); margin-top:var(--s3); }
@media (min-width: 560px) { .choices__g { grid-template-columns:repeat(3,1fr); } }
.choice { position:relative; display:block; }
.choice input { position:absolute; inset:0; width:100%; height:100%; margin:0;
  opacity:0; cursor:pointer; }
.choice__b { display:flex; align-items:center; gap:var(--s3); min-height:52px;
  padding:var(--s3) var(--s4); border:1px solid var(--rule-dark);
  font-size:var(--t-sm); font-weight:500; color:var(--on-dark);
  transition:border-color .16s ease; }
.choice__b::before { content:""; flex:none; width:9px; height:9px;
  border:1px solid var(--muted-dark); transition:background .16s ease, border-color .16s ease; }
.choice input:hover + .choice__b { border-color:var(--muted-dark); }
.choice input:focus-visible + .choice__b { outline:2px solid var(--clay-dark); outline-offset:3px; }
.choice input:checked + .choice__b { border-color:var(--clay-dark); font-weight:600; }
.choice input:checked + .choice__b::before { background:var(--clay-dark); border-color:var(--clay-dark); }

.btn--ghost { background:transparent; color:var(--on-dark); border-color:var(--edge); }
.btn--ghost:hover { background:transparent; border-color:var(--paper); }

.form__alert { padding:var(--s4); border:1px solid #F0A882; color:var(--on-dark);
  font-size:var(--t-sm); line-height:1.5; }
.form__alert a { border-bottom:1px solid var(--rule-dark); }
.form__note { font-size:var(--t-sm); color:var(--muted-dark); display:flex;
  flex-wrap:wrap; gap:var(--s3); align-items:baseline; }
.form__note[hidden] { display:none; }
.form__note button { background:none; border:0; padding:0; color:var(--on-dark);
  font-size:var(--t-sm); font-weight:600; border-bottom:1px solid var(--rule-dark); }
.noscript { font-size:var(--t-sm); color:var(--muted-dark); line-height:1.5; }
.noscript a { border-bottom:1px solid var(--rule-dark); }
/* files: only ever shown when JS is up, since there is nothing to upload with */
.files[hidden] { display:none; }
.files input[type=file] { width:100%; margin-top:var(--s2); padding:.7rem 0; min-height:44px;
  color:var(--muted-dark); font-size:var(--t-sm); }
.files input[type=file]::file-selector-button { margin-right:var(--s4); min-height:38px;
  padding:0 var(--s4); background:transparent; color:var(--on-dark); border:1px solid var(--edge);
  border-radius:0; font-size:var(--t-xs); font-weight:600; letter-spacing:var(--track);
  text-transform:uppercase; cursor:pointer; }
.files input[type=file]::file-selector-button:hover { border-color:var(--paper); }
.files__list { margin-top:var(--s3); display:grid; gap:var(--s2); }
.files__i { display:flex; align-items:center; justify-content:space-between; gap:var(--s4);
  padding:var(--s3) var(--s4); border:1px solid var(--rule-dark); font-size:var(--t-sm); }
.files__n { min-width:0; overflow-wrap:anywhere; color:var(--on-dark); }
.files__x { flex:none; min-height:44px; background:none; border:0; padding:0 0 0 var(--s3);
  color:var(--muted-dark); font-size:var(--t-xs); font-weight:600; letter-spacing:var(--track);
  text-transform:uppercase; }
.files__x:hover { color:var(--clay-dark); }

.done__sum { margin-top:var(--s4); font-size:var(--t-xs); font-weight:600;
  letter-spacing:var(--track); text-transform:uppercase; color:var(--clay-dark);
  line-height:1.8; max-width:38ch; }

/* ══════════════════════════════════════════════════════════════════════════
   THE ENQUIRY FORM ON A LIGHT CARD
   Added 9 September 2026. Julian: "Make the form stand out more - maybe put
   it in a container with a white background if you can or something like
   that. It's easy to miss."

   He is right that it was easy to miss: the form was dark-on-dark inside the
   olive .ask band, so a page that is otherwise photographs ended at a section
   whose only interactive thing was invisible until you looked for it. It is
   now a paper panel on the olive ground, which is the largest contrast step
   available anywhere on this site.

   ⚠️ --paper, NOT #fff. Paper IS the site's light ground (the body, the
   scrolled header), so a pure-white card would be a fourth surface colour
   introduced for one component. Against --ground the paper panel already
   reads as white and it stays inside the palette.

   ⚠️ EVERY VALUE BELOW IS MEASURED, via scripts/contrast.py, because this is
   a wholesale palette inversion of a form that was tuned for the dark ground
   and every single pair had to be re-derived:
       --ink   #232B26 on paper  12.93:1   body text
       --muted #5C685F on paper   5.19:1   labels, help, optional
       --clay  #9C5339 on paper   5.03:1   accents, step numbers
       paper on --clay             5.03:1   the primary button
       #A6321A on paper            6.04:1   errors  (the dark ground's #F0A882
                                            is 1.9:1 here and unusable)
       #8A8375 on paper            3.34:1   CONTROL BORDERS
   ⚠️ --edge #A99E8C is the kit's control border for paper and it measures
   2.35:1, which FAILS the 3:1 non-text minimum for a UI boundary. An input
   whose edge you cannot see is not a styling nicety. #8A8375 is the darkened
   value and it is used for every control boundary in here. Do not "tidy" it
   back to --edge.

   Scoped entirely under .ask__form, so the dark form styling above is intact
   and reverting is deleting this block.
   ══════════════════════════════════════════════════════════════════════════ */
.ask__form {
  background:var(--paper); color:var(--ink);
  border-top:3px solid var(--clay);
  padding:clamp(1.25rem,3.5vw,2.5rem);
  /* the olive band is flat, so the card needs an edge of its own to read as a
     raised surface rather than a colour change */
  box-shadow:0 1px 0 rgba(35,43,38,.06), 0 18px 40px -24px rgba(30,39,35,.55);
}
@media (prefers-reduced-motion:no-preference) { .ask__form { transition:none; } }

.ask__form .noscript,
.ask__form .field__opt,
.ask__form .field__help,
.ask__form .step__p,
.ask__form .form__note,
.ask__form .files input[type=file] { color:var(--muted); }

.ask__form .field label,
.ask__form .choices legend { color:var(--muted); }

.ask__form .field input,
.ask__form .field select,
.ask__form .field textarea {
  color:var(--ink); border-bottom-color:#8A8375;
}
.ask__form .field select {
  background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
}
.ask__form .field select option { background:var(--paper); color:var(--ink); }
.ask__form .field input:hover,
.ask__form .field select:hover,
.ask__form .field textarea:hover { border-bottom-color:var(--ink); }
.ask__form .field input:focus,
.ask__form .field select:focus,
.ask__form .field textarea:focus { border-bottom-color:var(--clay); }
.ask__form :focus-visible { outline-color:var(--clay); }

/* errors. #A6321A, not the dark ground's #F0A882 (1.9:1 on paper). */
.ask__form .field__err { color:#A6321A; }
.ask__form .field.is-bad input,
.ask__form .field.is-bad select,
.ask__form .field.is-bad textarea { border-bottom-color:#A6321A; }
.ask__form .choices.is-bad .choice__b { border-color:#A6321A; }
.ask__form .form__alert { border-color:#A6321A; color:var(--ink); }
.ask__form .form__alert a { border-bottom-color:#8A8375; }

/* step chrome */
.ask__form .steps__i { background:var(--rule); }
.ask__form .steps__i.is-on { background:var(--clay); }
.ask__form .step__n { color:var(--clay); }
.ask__form .step__h:focus-visible { outline-color:var(--clay); }
.ask__form .done { border-top-color:var(--clay); }
.ask__form .done p { color:var(--muted); }
.ask__form .done__sum { color:var(--clay); }

/* radio cards. Still a clay EDGE and never a clay fill — same reason as the
   dark version: --clay as a ground fails AA with text of either polarity. */
.ask__form .choice__b { border-color:#8A8375; color:var(--ink); }
.ask__form .choice__b::before { border-color:#8A8375; }
.ask__form .choice input:hover + .choice__b { border-color:var(--ink); }
.ask__form .choice input:focus-visible + .choice__b { outline-color:var(--clay); }
.ask__form .choice input:checked + .choice__b { border-color:var(--clay); }
.ask__form .choice input:checked + .choice__b::before { background:var(--clay); border-color:var(--clay); }

/* buttons. The primary is now clay-on-paper text rather than paper-on-dark:
   on a paper card the old .btn (paper ground, ink text) was invisible. */
.ask__form .btn { background:var(--clay); color:var(--paper); border-color:var(--clay); }
.ask__form .btn:hover { background:#8A4830; border-color:#8A4830; }
.ask__form .btn--ghost { background:transparent; color:var(--ink); border-color:#8A8375; }
.ask__form .btn--ghost:hover { background:transparent; border-color:var(--ink); }

/* links and the small text controls */
.ask__form a { color:var(--ink); }
.ask__form .tel,
.ask__form .noscript a,
.ask__form .form__note button { color:var(--ink); border-bottom-color:#8A8375; }
.ask__form .tel:hover,
.ask__form .form__note button:hover { color:var(--clay); border-bottom-color:var(--clay); }

/* uploads */
.ask__form .files input[type=file]::file-selector-button {
  color:var(--ink); border-color:#8A8375;
}
.ask__form .files input[type=file]::file-selector-button:hover { border-color:var(--ink); }
.ask__form .files__i { border-color:var(--rule); }
.ask__form .files__n { color:var(--ink); }
.ask__form .files__x { color:var(--muted); }
.ask__form .files__x:hover { color:var(--clay); }

/* ⚠️ The LEFT column of .ask__grid stays dark-on-olive. Only the form card
   flips. That is the contrast that makes the card read as a thing you fill in
   rather than as another band of copy. */

/* ── footer ─────────────────────────────────────────────────── */
.foot { background:var(--deep); color:var(--muted-dark); padding-block:var(--s7);
  border-top:1px solid var(--rule-dark); font-size:var(--t-sm); }
.foot__in { display:flex; flex-wrap:wrap; gap:var(--s4) var(--s7); justify-content:space-between; align-items:center; }
.foot a { display:inline-flex; align-items:center; min-height:44px;
  text-decoration:none; border-bottom:1px solid var(--rule-dark); }
.foot a:hover { border-bottom-color:var(--clay-dark); }
.foot img { width:124px; opacity:.9; }
/* mark over line, as one lockup. The line is tracked to sit under the width of
   the wordmark rather than being centred on it — the artwork's own supporting
   line does the same thing, so the two agree. */
.foot__mark { display:grid; gap:.55rem; justify-items:start; }
.foot__sig { font-size:var(--t-xs); font-weight:600; letter-spacing:var(--track);
  text-transform:uppercase; color:var(--muted-dark); opacity:.8; }

/* ── the footer page list ─────────────────────────────────────────────
   Added 9 Sep 2026, and the only change to this page since it was frozen.
   The six content pages behind the homepage need a route in from it, both
   for a reader and because a page nothing links to is a page a crawler
   reaches only via the sitemap. It sits ABOVE .foot__in so the existing
   mark / credentials / phone row is untouched.
   ⚠️ It wraps rather than scrolls: six items at 13px fit two lines at
   390px, and a horizontally scrolling row in a footer is a thing nobody
   discovers. The 44px target comes from .foot a, which already sets it. */
.foot__nav { display:flex; flex-wrap:wrap; gap:var(--s2) var(--s5);
  padding-bottom:var(--s5); margin-bottom:var(--s5);
  border-bottom:1px solid var(--rule-dark); }
.foot__nav a { font-size:var(--t-sm); border-bottom-color:transparent; }
.foot__nav a:hover { border-bottom-color:var(--clay-dark); }

/* ── reveal, JS-gated so a no-JS visitor sees everything ─────── */
.js .rv { opacity:0; transform:translateY(14px); }
.js .rv.in { opacity:1; transform:none; transition:opacity .6s ease, transform .6s cubic-bezier(.16,1,.3,1); }
@media (prefers-reduced-motion: reduce) { .js .rv, .js .rv.in { opacity:1; transform:none; transition:none; } }

/* ============================================================================
   PROJECT PAGE — Myocum Magic
   Same system, one job: show the house. The homepage is held to the measured
   category spec because a homepage is a filter; a project page is the thing
   the filter sends you to, so it runs longer and is almost entirely
   photographs. Words stay in caption register throughout.
   ========================================================================== */

/* the awards, and the designer credit, on the reel slide ─────────
   Cade's condition on using this project is that EcoSphereDesign is
   credited wherever the photographs appear, so the credit rides with the
   slide and is not held back for the project page. */
.proj__awards { margin-top:var(--s4); display:flex; flex-wrap:wrap; gap:var(--s2) var(--s4); }
.proj__awards li { font-size:var(--t-xs); font-weight:600; letter-spacing:var(--track);
  text-transform:uppercase; color:var(--clay); }
/* ⚠️ CMD's own scope on a slide, and it is INK where .proj__by is --muted, on
   purpose. The designer credit below is an attribution; this is the claim, and it
   is the one line on the slide a client is actually buying. It sits between the
   description and the awards so the reading order is: the house, what we did, and
   then the awards and the credit that belong to the designer. */
/* ⚠️ --ink, NOT --on-dark. The work reel is on PAPER — .proj__d beside it uses
   --muted, which is the paper secondary — so a near-white --on-dark here would
   have been invisible cream-on-cream. Caught before it shipped by checking the
   section's ground rather than assuming a slide is dark. */
.proj__role { margin-top:var(--s4); font-size:var(--t-sm); font-weight:500;
  color:var(--ink); max-width:46ch; }
.proj__by { margin-top:var(--s3); font-size:var(--t-sm); color:var(--muted); }
.proj__by a { text-decoration:none; border-bottom:1px solid var(--edge); }
.proj__by a:hover { border-bottom-color:var(--clay); }
.proj__more { margin-top:var(--s5); display:inline-flex; align-items:center; min-height:44px;
  font-size:var(--t-sm); font-weight:600; letter-spacing:.02em; text-decoration:none;
  border-bottom:1px solid var(--clay); }
.proj__more:hover { color:var(--clay); }

/* hero meta line ───────────────────────────────────────────────
   Credits the hero photograph and links to that build. ⚠️ Colours are measured
   against the text-shadow scrim the hero applies to this selector (see the
   .hero .lab rule above), not against flat paper: --muted-dark is 7.17:1 there
   and --clay-photo 11.25:1. --clay-dark would be the obvious accent and is the
   wrong one — over a PHOTOGRAPH it drops to 2.45:1, which is why --clay-photo
   exists at all. */
/* ⚠️ align-self:flex-start, NOT the default stretch. .hero__in is a column flex
   container, so a bare <p> stretches to the full 1096px measure — and this line is
   only ~390px of text sitting in the bottom-LEFT corner, which is the one part of
   the frame the desktop scrim actually grounds. A stretched box reaches across the
   sunlit right-hand side of the photograph, so hero-contrast.py samples brightness
   the glyphs never touch and reports ~2.0:1 for type that measures 6.0.

   It was shrink-to-fit for a fortnight only because it happened to be a flex ITEM
   of a row that also held the slogan. When the slogan moved to .byline the row went
   with it and the box silently went full-width. Same family as the .lab rule below —
   see [[flex-item-blockify-trap]]. */
.hero__meta { margin-top:var(--s4); align-self:flex-start;
  font-size:var(--t-xs); font-weight:600;
  letter-spacing:var(--track); text-transform:uppercase; color:var(--muted-dark); }
.hero__meta a { color:var(--clay-photo); text-decoration:none;
  border-bottom:1px solid rgba(243,223,208,.45); padding-bottom:.15em;
  white-space:nowrap; margin-left:.35em; }
.hero__meta a:hover { border-bottom-color:var(--clay-photo); }

/* ── the byline band ───────────────────────────────────────────
   Cade's own line, and the only place it appears above the fold. It had a
   fortnight bottom-right INSIDE the hero, over the photograph, which needed a
   local radial scrim because the desktop hero scrim is anchored to the
   bottom-LEFT corner and deliberately leaves the right-hand side of the frame
   open. On a flat band none of that is needed and the line stops competing
   with the picture.

   8svh against the hero's 92 — see .hero__in. The floor matters: at a 620px
   landscape window 8svh is 50px, which is still a comfortable single line, but
   below that the band would pinch, so it never goes under 3.25rem. */
.byline { background:var(--clay); color:var(--on-dark);
  min-height:var(--byline-h); display:flex; align-items:center;
  justify-content:center; padding:var(--s3) var(--pad); }
/* ⚠️ THIS IS THE ONE PIECE OF TYPE ON THE PAGE THAT IS NOT IN THE FOUR-SIZE SCALE,
   and deliberately: it is the only line Cade wrote himself and he asked for it on
   the site. At --t-xs (11px) it read as a caption under the photograph rather than
   as a statement, so it runs 14 -> 22px instead. Julian, 3 Sep: make it bigger.

   It still does not out-rank the h1 above it — 22px against the hero's 54px — so
   the headline keeps the selling job and this stays a byline.

   WEIGHT 300, NOT 600. Julian, 3 Sep: "thin the text - too bold". It is also the
   right register for this line — light and widely tracked is exactly what the
   settled wordmark does with CONSTRUCTIONS, so the byline and the mark now agree.

   ⚠️ line-height 1.15, NOT the body's 1.6, AND THAT IS WHAT KEEPS IT ABOVE THE
   FOLD. min-height is a floor, so content that exceeds it grows the band — but
   .hero__in subtracts the TOKEN, not the grown height. At 1280x620 the inherited
   1.6 made a 22px line 35px tall, the band 59px against a 52px token, and the pair
   overshot the window by 7px. A tight line-height on a single line of caps costs
   nothing and holds the geometry. ⚠️ If this ever wraps to two lines the same
   arithmetic breaks again — keep the line short. */
.byline p { font-size:clamp(0.875rem, 1.9vw, 1.35rem); font-weight:300;
  letter-spacing:.2em; text-transform:uppercase; text-align:center;
  text-wrap:balance; line-height:1.15; }
/* Tracking comes off as the screen narrows — .2em on 25 characters overflows a
   320px phone, and the tracking is the part that can go without the line losing
   its character. Measured at 320/360/390, not guessed. */
@media (max-width: 420px) { .byline p { letter-spacing:.13em; } }
@media (max-width: 360px) { .byline p { letter-spacing:.09em; } }

/* the awards band ────────────────────────────────────────────────
   Three awards is the whole reason this project leads the site, so it is
   the first thing under the photograph rather than a footnote. */
/* --ground, not --ground-2: the year is --clay-dark, which is 4.88:1 on the
   olive ground but only 4.28:1 on the lighter one. */
.awards { background:var(--ground); color:var(--on-dark); padding-block:var(--band); }
.awards__grid { display:grid; gap:var(--s6); }
.awards h2 { font-size:var(--d2); max-width:14ch; text-wrap:balance; margin-top:var(--s4); }
.awards__by { margin-top:var(--s5); color:var(--muted-dark); max-width:34ch; }
.awards__by a { color:var(--on-dark); text-decoration:none; border-bottom:1px solid var(--clay-dark); }
.awards ol { display:grid; gap:0; }
.awards li { display:grid; gap:var(--s1); padding-block:var(--s5);
  border-top:1px solid var(--rule-dark); }
.awards li:last-child { border-bottom:1px solid var(--rule-dark); }
.awards .yr { font-size:var(--t-xs); font-weight:600; letter-spacing:var(--track);
  color:var(--clay-dark); }
.awards .nm { font-size:var(--d3); font-weight:500; letter-spacing:-.015em; }
.awards .of { font-size:var(--t-sm); color:var(--muted-dark); }
@media (min-width: 900px) {
  .awards__grid { grid-template-columns:minmax(0,.72fr) minmax(0,1fr); gap:var(--s7); align-items:start; }
}

/* the facts band ─────────────────────────────────────────────── */
.facts { padding-block:var(--band); }
.facts__grid { display:grid; gap:var(--s6); }
.facts__lede { font-size:var(--d3); font-weight:400; line-height:1.45;
  letter-spacing:-.012em; max-width:32ch; }
.facts__body { margin-top:var(--s5); color:var(--muted); max-width:50ch; }
.facts__body + .facts__body { margin-top:var(--s4); }
.facts dl { display:grid; grid-template-columns:auto minmax(0,1fr); gap:0 var(--s5);
  border-top:1px solid var(--rule); }
.facts dt { padding-block:var(--s3); font-size:var(--t-xs); font-weight:600;
  letter-spacing:var(--track); text-transform:uppercase; color:var(--muted);
  border-bottom:1px solid var(--rule); }
.facts dd { margin:0; padding-block:var(--s3); font-size:var(--t-sm);
  border-bottom:1px solid var(--rule); }
.facts dd a { text-decoration:none; border-bottom:1px solid var(--edge); }
.facts dd a:hover { border-bottom-color:var(--clay); }
@media (min-width: 900px) {
  .facts__grid { grid-template-columns:minmax(0,1fr) minmax(0,.78fr); gap:var(--s7); align-items:start; }
}

/* the photograph sequence ────────────────────────────────────────
   Full-bleed frames carry the house, wrapped pairs carry the rooms and the
   details. The alternation is what stops fifteen photographs reading as a
   contact sheet. */
.seq { padding-bottom:var(--band); }
.seq > * + * { margin-top:clamp(1.75rem,3.6vw,3rem); }
.seq__full img { width:100%; aspect-ratio:16/9; object-fit:cover; background:var(--paper-2); }
.seq__full--32 img { aspect-ratio:3/2; }
.seq__pair { display:grid; gap:clamp(1rem,2.4vw,var(--s5)); }
.seq__pair img { width:100%; object-fit:cover; background:var(--paper-2); }
.seq__pair--tall img { aspect-ratio:4/5; }
.seq__pair--wide img { aspect-ratio:3/2; }
.seq__pair--sq img { aspect-ratio:1/1; }
@media (min-width: 720px) { .seq__pair { grid-template-columns:1fr 1fr; } }
.cap { margin-top:var(--s3); font-size:var(--t-sm); color:var(--muted); max-width:46ch; }
/* a full-bleed frame's caption still has to line up with the text column */
.seq__full .cap { width:100%; max-width:var(--wrap); margin-inline:auto;
  padding-inline:var(--pad); }

/* the close: credit, and back into the funnel ────────────────── */
.close { background:var(--ground); color:var(--on-dark); padding-block:var(--band); }
.close h2 { font-size:var(--d2); max-width:16ch; text-wrap:balance; margin-top:var(--s4); }
.close p { margin-top:var(--s5); color:var(--muted-dark); max-width:46ch; }
.close a.lnk { color:var(--on-dark); text-decoration:none; border-bottom:1px solid var(--clay-dark); }
.close__row { margin-top:var(--s6); display:flex; flex-wrap:wrap; gap:var(--s4) var(--s6); align-items:center; }
/* .btn is a <button> on the homepage and an <a> here, so it has to carry its
   own centring and lose the underline an anchor brings with it. */
.close .btn { margin-top:0; display:inline-flex; align-items:center; justify-content:center;
  text-decoration:none; }
.back { display:inline-flex; align-items:center; min-height:44px; font-size:var(--t-sm);
  font-weight:600; letter-spacing:.02em; text-decoration:none; color:var(--on-dark);
  border-bottom:1px solid var(--rule-dark); }
.back:hover { border-bottom-color:var(--clay-dark); }
