/* ============================================================
   Components — the brand book system.
   Night grounds behind photography, Paper for reading, Sunset on the
   one primary action per screen, squared surfaces, pills on nav only.
   ============================================================ */

/* ---------------- header ----------------
   Thin, typographic, no fill, no shadow. Phone number prominent. */

.masthead {
  position: sticky;
  top: 0;
  z-index: 100;
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-fast) var(--ease);
}
.masthead[data-stuck="true"] { border-bottom-color: var(--line); }

/* ---------------- the home header ----------------

   ⛔ TWO NUMBERS THAT HAD TO AGREE, AND NOTHING MADE THEM. The page used
   to be pulled up by `margin-top: calc(var(--masthead-h) * -1)` so the
   hero slid under a sticky, in-flow bar. --masthead-h is 72px. The bar
   measured 105px, because its height is not the token at all: it is the
   LOGO (clamp 38-58px) plus the padding-block that was added to float it
   over the photograph. 105 - 72 left a 33px strip of Paper above the hero
   at 1440, and a different strip at every other width, because both the
   logo and the padding are viewport-relative.

   Raising the token would only move the bug. The fix is to delete the
   second number: the home bar is `position: fixed`, so it is out of flow
   and `main` starts at y=0 on its own. There is no pull to keep in sync,
   and no window shape can reopen the gap. It behaves identically to the
   sticky version, which was pinned at top:0 from the first pixel anyway.

   --masthead-h then only has to describe the bar to things that clear it
   (the hero stack's top padding, the mobile drawer, scroll-margin), so
   the bar is given that exact height rather than being left to size
   itself off its contents. 76px also brings it under the 80px ceiling a
   nav should keep; the old 105px bar was eating 12% of a 900px viewport.

   ⛔ ONE DESIGN IN BOTH STATES. Daniel: "looks really ugly with two
   complete different styles." It used to be transparent-with-white-type
   over the hero and Paper-wash-with-Ink-type once scrolled, which is two
   headers, not two states of one. It is now Night glass throughout, so
   the logo, the nav type, the pill and the CTA never change. The only
   thing that moves is the background alpha, 0.30 over the photograph to
   0.94 over Paper, plus the hairline. That is the whole delta.

   Night glass is also the brand book's own material: Night is listed for
   "cards, header, footer", and the bar, the nav pill and the hero card
   are now the same surface at three depths (0.30 / 0.28 / 0.80). */
/* ⚠ 78px, NOT 84px. A marketing nav over 80px eats viewport for nothing;
   measured at 85px including its border, which is over the cap. The logo
   clamp and the pill both still fit. */
.page-home {
  --masthead-h: 78px;
}
/* ⛔ NO BAR OVER THE HERO. The approved comp has no header ground at all:
   the sky reads straight through and the lockup, the pill and the CTA sit
   on the photograph. A Night wash was tried and it stacked on top of the
   hero scrim, which is what turned the sunset brown. The bar only appears
   once the page has scrolled past the picture, where white type would
   otherwise be sitting on Paper. */
.page-home .masthead {
  position: fixed;
  inset-inline: 0;
  top: 0;
  color: var(--paper);
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0);
  transition: background-color var(--dur-fast) var(--ease-ui),
              border-bottom-color var(--dur-fast) var(--ease-ui);
}
.page-home .masthead[data-stuck="true"] {
  background: rgba(11, 10, 7, 0.94);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom-color: rgba(255, 255, 255, 0.10);
}
.page-home .masthead__inner {
  height: var(--masthead-h);
  min-height: var(--masthead-h);
  padding-block: 0;
  /* ⛔ NOT the 1280px reading container. The comp runs the lockup out to
     the page edge and the menu button to the other one; inside .container
     the lockup started at 13% of the frame instead of 3% and the bar read
     as a narrow strip floating in the middle of a wide window. */
  max-width: none;
  padding-inline: clamp(1.25rem, 3.2vw, 3.5rem);
}
.page-home .nav__link { color: var(--white); }
.page-home .nav__link:hover { color: var(--white); }
.page-home .masthead__tel { color: var(--paper); }
.page-home .masthead__tel:hover { color: var(--gold); }
/* While the bar is transparent over the film, the flank text needs its
   own ground: mid-film frames run bright (the cream kitchen), and only
   the top scrim sits under them. Same layered-shadow recipe as the
   wordmark; gone once the bar goes solid. */
.page-home .masthead:not([data-stuck="true"]) .masthead__tel--bar,
.page-home .masthead:not([data-stuck="true"]) .masthead__cta {
  text-shadow: 0 1px 3px rgba(11, 10, 7, 0.85), 0 2px 12px rgba(11, 10, 7, 0.5);
}
/* Constant across both bar states on purpose: the identity gate compares
   the logo's computed filter stuck vs unstuck, and a shadow is invisible
   on the solid bar anyway. */
.page-home .masthead__logo,
.page-home .nav__toggle {
  filter: drop-shadow(0 2px 10px rgba(11, 10, 7, 0.6));
}
/* ⛔ The lockup PNG is WHITE glyphs. On every light bar — all the
   non-home pages — it was white on Paper and invisible (Daniel,
   2026-08-23, the About report). Ink it with a filter so there is one
   source asset. */
body:not(.page-home) .masthead__logo { filter: brightness(0); }
@media (max-width: 62rem) {
  .page-home .nav {
    background: var(--night);
    border-bottom-color: rgba(255, 255, 255, 0.12);
  }
  .page-home .nav .nav__link { border-bottom-color: rgba(255, 255, 255, 0.12); }
}

/* The bar itself. Flex at every width; the desktop grid at the end of this
   file replaces the display so the pill can be centred on the BAR rather
   than between the flanks. */
.masthead__inner {
  /* ⛔ ONE bar geometry on every page (Daniel, 2026-08-24). The markup
     carries .container, but the bar is a viewport fixture: the logo and
     the CTA sit the same distance from the screen corners on all eleven
     pages. Before this, subpages inherited .container's 1280px cap and
     the CTA drifted 368px inboard at 1920 while home hugged the corner,
     so the whole bar visibly jumped when navigating. */
  max-width: none;
  padding-inline: max(clamp(1.25rem, 3.2vw, 3.5rem), env(safe-area-inset-left, 0px));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-24);
  min-height: var(--masthead-h);
}

/* ---- the lockup ----
   The horizontal mark-plus-wordmark, sized so the name is readable. It ran
   at 77px wide before, because the asset in use was the SQUARE lockup at
   the same height. */
/* The link is sized by the image, and the image is 34px tall at the small
   end of its clamp, so the home link was a 34px target. The image keeps its
   size; the LINK gets the 44px. */
.masthead__mark { display: inline-flex; align-items: center; min-height: 44px; }
.masthead__logo { height: clamp(34px, 3.2vw, 46px); width: auto; display: block; }
.page-home .masthead__logo { filter: none; }

/* ---- the right-hand group: text CTA then the menu button ---- */
.masthead__left {
  display: inline-flex;
  align-items: center;
  gap: clamp(16px, 1.6vw, 26px);
  flex-shrink: 0;
}
.masthead__right {
  display: inline-flex;
  align-items: center;
  gap: clamp(14px, 1.6vw, 26px);
  flex-shrink: 0;
}
/* ---- the phone in the bar ----
   ⛔ Only from 87.5rem up, and the threshold is measured rather than
   guessed. It sits on the LEFT flank beside the lockup, which at 1280 has
   156px of clearance to the centred pill while the number plus its divider
   wants about 165. 1400 is the first width where it clears comfortably.
   Below that the number is one tap away in the drawer, and the hero card
   and every CTA block still carry it. qa-home.mjs fails if a flank ever
   reaches the pill, at twelve window shapes. */
/* ⛔ .masthead, not a bare class. `.masthead__tel { display: inline-flex }`
   is declared LATER in this file at the same specificity, so a bare
   `.masthead__tel--bar { display: none }` lost the cascade and the number
   showed at EVERY width: it pushed the header 89px past a 320px viewport
   and put /404.html and /contact/ into horizontal scroll. Same trap the
   note at the end of this file describes. */
.masthead .masthead__tel--bar {
  display: none;
  /* inline-FLEX, not inline-block: as an inline box it sat on the text
     baseline and rode a few pixels above the lockup's centre line. */
  align-items: center;
  font-size: var(--fs-15);
  font-weight: var(--w-med);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  padding-left: clamp(16px, 1.6vw, 26px);
  border-left: 1px solid var(--line);
}
.page-home .masthead__tel--bar { border-left-color: var(--line-on-dark); }
.page-home .masthead__tel--bar { color: var(--white); }
.masthead__tel--bar:hover { color: var(--sunset-hi); }
.page-home .masthead__tel--bar:hover { color: var(--gold); }
/* Always visible from the width it fits at (Daniel, 2026-08-23) — the
   old reveal-once-the-hero-card-left gating is gone. */
@media (min-width: 87.5rem) {
  .masthead .masthead__tel--bar { display: inline-flex; }
}

.masthead__cta {
  position: relative;             /* anchors the finished-clip rule below */
  font-size: var(--fs-15);
  font-weight: var(--w-med);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  padding-bottom: 4px;
  border-bottom: 2px solid var(--sunset-btn);
  transition: border-color var(--dur-fast) var(--ease-ui), color var(--dur-fast) var(--ease-ui);
}
.masthead__cta:hover { border-bottom-color: var(--sunset-hi); }
.page-home .masthead__cta { color: var(--white); }
@media (max-width: 62rem) { .masthead__cta { display: none; } }

/* ---- the one nudge, for when the clip has finished ----
   The hero runs one golden hour into one night and then holds on the night
   frame. Nothing else on the page is moving by then, so "what now" is a
   real question and it deserves an answer.

   The answer is one quiet mark: the Sunset rule under the header CTA draws
   itself in, left to right, once, and stops.

   ⛔ NOT a hand drawn arrow. The brand book is a heavy grotesque over
   squared surfaces with no decorative flourishes, so a doodle would be the
   single hand made mark anywhere on the site and would read as a different
   designer's work.

   The gesture is not new either. It is the same accent wipe the service
   cards run on hover, slowed from 240ms to 400ms so it reads as a
   statement rather than as a hover, and eased with --ease-soft so it
   arrives rather than snaps. --sunset is the brightest of the three and
   its token is documented graphic-only on dark grounds, which is what the
   header is in BOTH of its states (Night glass at 0.30 over the plate,
   0.94 over Paper).

   ⛔ ONCE, and never again: site.js adds the attribute on `ended` with
   { once: true }, and that listener is only ever attached inside the branch
   that already decided the clip may play. A phone, Save-Data, 2g/3g and
   reduced motion cannot reach it structurally, rather than by a second
   check that could drift out of step with the first. */
/* The CTA is 29px tall because it is a text link with a rule under it, and
   the rule has to sit on the text. Growing the box would drag the rule away
   from the words, so the HIT AREA grows instead and the rule stays put.
   It is only shown at 62rem and up, but an iPad in landscape is both. */
.masthead__cta::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
.masthead__cta::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -2px;   /* -2px sits it on the border, not under it */
  height: 2px;
  background: var(--sunset);
  transform: scaleX(0);
  transform-origin: left center;
}
@media (prefers-reduced-motion: no-preference) {
  .masthead[data-hero-ended] .masthead__cta::after {
    transform: scaleX(1);
    transition: transform 400ms var(--ease-soft);
  }
}

/* ---- the menu button: three bars, not the word "Menu" ---- */
.nav__toggle {
  /* ⛔ Hidden on desktop (Daniel, 2026-08-23): the pill carries the
     primary set and the drawer content (.nav__rest/.nav__foot) is
     display:none above 62rem, so on a desktop the button opened nothing.
     The ≤62rem media block below turns it back on for the drawer. */
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
  flex-shrink: 0;
}
.page-home .nav__toggle { color: var(--white); }

/* The three bars. One element plus its two pseudo-elements, so there is no
   icon font, no SVG and nothing to download. Crosses into an X when open. */
.nav__bars, .nav__bars::before, .nav__bars::after {
  display: block;
  width: 26px;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  transition: transform var(--dur-fast) var(--ease-ui), background-color var(--dur-fast) var(--ease-ui);
}
.nav__bars { position: relative; }
.nav__bars::before, .nav__bars::after { content: ""; position: absolute; left: 0; }
.nav__bars::before { top: -8px; }
.nav__bars::after  { top: 8px; }
[aria-expanded="true"] .nav__bars { background: transparent; }
[aria-expanded="true"] .nav__bars::before { transform: translateY(8px) rotate(45deg); }
[aria-expanded="true"] .nav__bars::after  { transform: translateY(-8px) rotate(-45deg); }

.nav { display: flex; align-items: center; gap: clamp(12px, 1.4vw, 22px); }
.nav__link {
  font-size: var(--fs-15);
  color: var(--ink);
  font-weight: var(--w-med);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
  white-space: nowrap;
}
.nav__link:hover { color: var(--sunset-hi); }
.nav__link[aria-current="page"] { color: var(--sunset-hi); }
/* Measured 43px, one pixel under the thumb target, at every width. */
.nav__link { display: inline-flex; align-items: center; min-height: 44px; }


@media (max-width: 62rem) {
  .nav__toggle { display: flex; }
  .nav {
    position: fixed;
    inset: var(--masthead-h) 0 auto 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    padding-inline: max(var(--gutter), env(safe-area-inset-left, 0px));
    padding-top: var(--s-16);
    transform: translateY(-115%);
    transition: transform var(--dur) var(--ease);
    max-height: calc(100dvh - var(--masthead-h));
    overflow-y: auto;
    /* Scrolling the open drawer must not chain to the page behind it. */
    overscroll-behavior: contain;
    padding-bottom: calc(var(--s-36) + env(safe-area-inset-bottom, 0px));
  }
  .nav[data-open="true"] { transform: translateY(0); }
  .nav__link {
    padding-block: var(--s-16);
    border-bottom: 1px solid var(--line);
    font-size: var(--fs-18);
  }
  .nav .btn { margin-top: var(--s-24); justify-content: center; }
}

/* ---------------- buttons ----------------
   Brand book: ONE primary and ONE secondary, and always the same pair.
   "Ask Max for a quote" in a Sunset fill, and the phone number as a
   ruled ghost. Never a third variant.

   ⛔ The fill is Sunset BUTTON #C25520, not Sunset #D9662E. White on
   #D9662E measures 3.57:1 and fails AA at body size; on #C25520 it is
   4.55:1. That split is an accessibility fix, not a preference, so
   nothing below may reach for --sunset as a fill behind text.

   ORDER MATTERS HERE. `.on-walnut .btn` and `.on-walnut .btn--secondary`
   have identical specificity (0,2,0), so whichever is written last wins.
   With the base block last, the secondary once inherited the primary's
   fill and the two CTAs became indistinguishable. The order is:
     base -> modifiers -> modifiers on dark
   so a modifier is never overridden by the thing it modifies.
   ---------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-12);
  font-family: var(--sans);
  font-size: var(--fs-15);
  font-weight: var(--w-label);
  letter-spacing: -0.006em;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  /* 48px minimum, both variants, because the phone link is a tap target
     and not a run of text. */
  min-height: 48px;
  padding: var(--s-14) var(--s-24);
  border-radius: var(--r-btn);
  border: 1px solid var(--sunset-btn);
  background: var(--sunset-btn);
  color: var(--white);
  box-shadow: none;
  transition: background var(--dur-fast) var(--ease-ui), color var(--dur-fast) var(--ease-ui),
              border-color var(--dur-fast) var(--ease-ui), transform var(--dur-fast) var(--ease-ui);
}
.btn:hover { background: var(--sunset-hi); border-color: var(--sunset-hi); color: var(--white); }
.btn:active { transform: translateY(1px); }
.on-night .btn, .on-walnut .btn { background: var(--sunset-btn); border-color: var(--sunset-btn); color: var(--white); }
.on-night .btn:hover, .on-walnut .btn:hover { background: var(--sunset-hi); border-color: var(--sunset-hi); }

/* The arrow box inside the primary. Decorative, so it is aria-hidden in
   the markup and inherits the button's own colour here. */
.btn__arw {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px; height: 24px;
  flex: 0 0 auto;
  border: 1.5px solid rgba(255, 255, 255, 0.55);
}

/* --- secondary: the phone. Same height so a pair sits level, but a rule
       under the text rather than a fill, because there is only ever one
       primary action on a screen. --- */
.btn--secondary {
  background: transparent;
  border-color: transparent;
  color: var(--ink);
  box-shadow: inset 0 -1px 0 var(--ink);
  padding-inline: var(--s-5);
}
.btn--secondary:hover {
  background: transparent;
  border-color: transparent;
  color: var(--sunset-hi);
  box-shadow: inset 0 -2px 0 var(--sunset-hi);
}
.on-night .btn--secondary, .on-walnut .btn--secondary {
  color: var(--paper);
  background: transparent;
  border-color: transparent;
  box-shadow: inset 0 -1px 0 rgba(246, 243, 240, 0.5);
}
.on-night .btn--secondary:hover, .on-walnut .btn--secondary:hover {
  color: var(--gold);
  background: transparent;
  box-shadow: inset 0 -2px 0 var(--gold);
}

/* --- ghost and outline: kept because the nine pages this run does not
       touch still use them. Both are quiet, neither is a second primary. --- */
/* .btn--ghost and .btn--outline removed 2026-08-24 (handoff audit): the
   brand system is exactly two button variants, .btn and .btn--secondary.
   Ghost rendered once (home proof band) and became a secondary; outline
   was never referenced by any page. */

/* The brand book's pair is "Ask Max for a quote" AND the phone, always
   together. The header used to carry only the phone, as the filled button;
   the primary action belongs there and the phone stays beside it as a tap
   target rather than disappearing into the drawer. */
.masthead__tel {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: var(--fs-15);
  font-weight: var(--w-h1);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
}
.masthead__tel:hover { color: var(--sunset-hi); }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--s-18); align-items: center; }

/* ---------------- hero banner ----------------
   The SERVICE-PAGE banner. Night ground, Paper text, one primary CTA.
   The home page no longer uses this; it uses .vhero below. */

.hero {
  background: var(--night);
  color: var(--paper);
  padding-block: clamp(3.5rem, 7vw, 7rem) clamp(3rem, 6vw, 6rem);
  position: relative;
}
.hero__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}
@media (max-width: 62rem) { .hero__grid { grid-template-columns: 1fr; gap: var(--s-36); } }

.hero__title {
  /* ⛔ NOT --fs-display. That is the home page's brand size, and a service
     page H1 has a job to do instead: it carries the primary term and the
     town, so it is a longer line by design. At display size
     "Deck builder in Loveland and Northern Colorado" wrapped to four lines
     at 1440 and the gate failed it on all four service pages. */
  font-size: var(--fs-h-lg);
  line-height: var(--lh-display);
  letter-spacing: var(--track-h-lg);
  color: var(--paper);
  font-weight: var(--w-h1);
  text-wrap: balance;
}
.hero__sub { max-width: 40ch; color: var(--on-dark-soft); }

.hero__media {
  border-radius: var(--r-specialty);
  overflow: hidden;
  aspect-ratio: 4 / 3;
  background: rgba(0, 0, 0, 0.2);
}
.hero__media img { width: 100%; height: 100%; object-fit: cover; }
/* A portrait image keeps its own shape instead of being beheaded by the
   4:3 default (Daniel, 2026-08-23: "why are we cutting off Maxes head?!"). */
.hero__media--portrait {
  aspect-ratio: 3 / 4;
  max-width: min(26rem, 100%);
  margin-left: auto;
  border: 1px solid var(--line-on-dark);
  /* Framed artwork: a Sunset rule offset behind the top-right corner.
     The one graphic accent, doing the "designed, not placed" work. */
  position: relative;
}
/* ⚠ Doubled selector on purpose: a later `.hero__media { overflow:
   hidden }` rule wins a same-specificity cascade and silently clips the
   offset frame (it did, and the corner shipped bare once). object-fit
   crops the img by itself, so visible overflow is safe here. */
.hero__media.hero__media--portrait { overflow: visible; }
.hero__media--portrait::after {
  content: "";
  position: absolute;
  top: -14px;
  right: -14px;
  width: 100%;
  height: 100%;
  border: 2px solid var(--sunset);
  pointer-events: none;
  z-index: -1;
}
@media (max-width: 62rem) { .hero__media--portrait { margin-left: 0; } }

/* ================================================================
   ⛔ ONE RENDERING STYLE PER SURFACE (Daniel, 2026-08-23, after the
   sketch-on-photograph collage). Photographs sit on Night or Paper.
   The film sits under its own UI. An ILLUSTRATION sits on a GRAPHIC
   ground: Night, a tonal wash echoing the artwork's own light, the
   site grain, and brand geometry. When two media want one hero, the
   second becomes graphic. Never image-on-image.
   .hero--stage is that graphic ground. */
.hero--stage {
  background:
    radial-gradient(46rem 34rem at 72% 42%,
      rgba(232, 185, 143, 0.13), rgba(217, 102, 46, 0.05) 48%, transparent 72%),
    var(--night);
}
.hero--stage::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.1;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/></filter><rect width='160' height='160' filter='url(%23n)'/></svg>");
}
.hero--stage .container { position: relative; z-index: 1; }

/* ---- the contact hero, recomposed (taste audit, 2026-08-23) ----
   A contact page's conversion content is the phone, the email and the
   service area, so they sit IN the hero as hairlined fact rows. 7/5
   split (no central void), content-tall section (no 90vh of dark), and
   the cutout figure is bottom-anchored: Max STANDS on the section edge
   inside the glow instead of floating mid-air. */
.hero--contact { padding-block: 0; }
/* Utility-page scale: the headline shares the hero with a person, so the
   type calms down and the figure carries the weight (taste + UX review,
   2026-08-23: "he is small and the text is massive"). */
.hero--contact .hero__title { font-size: clamp(2.25rem, 3.4vw, 3.25rem); }
.hero--contact.hero--stage {
  /* Anchored at torso height, not the head: a centred glow behind a small
     distant figure read as an icon, not a carpenter. */
  background:
    radial-gradient(38rem 32rem at 76% 74%,
      rgba(232, 185, 143, 0.20), rgba(217, 102, 46, 0.08) 46%, transparent 68%),
    var(--night);
}
.hero-contact {
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
  gap: clamp(1.5rem, 3.5vw, 3.5rem);
  align-items: end;
}
.hero-contact__copy {
  align-self: center;
  padding-block: clamp(2.25rem, 5.5vh, 3.75rem);
}
.hero-contact__facts {
  margin: var(--s-36) 0 0;
  display: grid;
  grid-template-columns: 1fr;
  max-width: 34rem;
}
.hero-contact__fact {
  display: flex;
  align-items: baseline;
  gap: var(--s-14);
  padding-block: var(--s-12);
  border-top: 1px solid var(--line-on-dark);
  margin: 0;
}
.hero-contact__fact dt {
  flex: none;
  min-width: 3.75rem;
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--gold);
}
.hero-contact__fact dd {
  margin: 0;
  font-size: var(--fs-15);
  color: var(--on-dark-soft);
}
.hero-contact__fact dd a {
  color: var(--paper);
  font-weight: var(--w-med);
  text-decoration: none;
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.35);
  transition: color var(--dur-fast) var(--ease-ui), box-shadow var(--dur-fast) var(--ease-ui);
}
.hero-contact__fact dd a:hover { color: var(--gold); box-shadow: inset 0 -2px 0 var(--gold); }
/* Bottom-anchored: the column is end-aligned in a section with no bottom
   padding, so the masked feet of the figure melt exactly at the section
   boundary. He stands on the edge; nothing floats. */
.hero-contact__figure { align-self: end; }
.hero-contact__figure img {
  display: block;
  width: 100%;
  max-width: clamp(25rem, 30vw, 31rem);
  height: auto;
  margin-inline: auto;
  -webkit-mask-image: linear-gradient(to bottom, black 88%, transparent 100%);
  mask-image: linear-gradient(to bottom, black 88%, transparent 100%);
}
@media (max-width: 62rem) {
  .hero-contact { grid-template-columns: 1fr; gap: var(--s-24); }
  .hero-contact__copy { padding-block: clamp(2.5rem, 6vh, 4rem) 0; }
  .hero-contact__facts { grid-template-columns: 1fr; }
  .hero-contact__figure img { max-width: 19rem; }
}
@media (max-width: 26rem) {
  .hero-contact__fact { flex-direction: column; gap: var(--s-5); }
  .hero--contact.hero--stage {
    background:
      radial-gradient(30rem 26rem at 50% 78%,
        rgba(232, 185, 143, 0.15), rgba(217, 102, 46, 0.06) 46%, transparent 70%),
      var(--night);
  }
}
/* The CUTOUT treatment: the illustrated subject floats directly on the
   stage - no box, no border, no frame, nothing to read as image-on-image
   (Daniel, 2026-08-23, third pass on this banner). The bottom of the
   figure melts into the ground with a mask so the crop line never shows. */
.hero__media.hero__media--cutout {
  aspect-ratio: auto;
  background: none;
  border: 0;
  border-radius: 0;
  overflow: visible;
  max-width: min(26rem, 100%);
  margin-left: auto;
}
@media (max-width: 62rem) { .hero__media.hero__media--cutout { margin-left: 0; } }
.hero__media--cutout img {
  height: auto;
  object-fit: contain;
  -webkit-mask-image: linear-gradient(to bottom, black 84%, transparent 99%);
  mask-image: linear-gradient(to bottom, black 84%, transparent 99%);
}

/* One of Max's REAL photographs as a barely-there material texture under
   a banner: his tongue-and-groove ceiling reads as warm wood planks.
   Darkened hardest over the text column so every word keeps its ground. */
.hero--textured { overflow: hidden; }
.hero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.hero__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.55;
}
.hero__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
    rgba(11, 10, 7, 0.94) 0%, rgba(11, 10, 7, 0.8) 42%,
    rgba(11, 10, 7, 0.38) 100%);
}
.hero--textured .container { position: relative; z-index: 1; }
/* The purpose-built (generated) banner image ships at full presence: it
   is authored with its own near-black left third, so the overlay only
   guarantees the text column rather than muddying the whole frame. */
.hero__bg--full img { opacity: 1; }
.hero__bg--full::after {
  background: linear-gradient(100deg,
    rgba(11, 10, 7, 0.8) 0%, rgba(11, 10, 7, 0.45) 42%,
    rgba(11, 10, 7, 0.08) 100%);
}

.hero__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-12) var(--s-36);
  margin-top: var(--s-36);
  padding-top: var(--s-24);
  border-top: 1px solid var(--line-on-dark);
}

/* ---------------- section opener ---------------- */

/* ⛔ THIS COMPONENT USED TO REQUIRE A CHILD THAT MAY NOT EXIST. It was a
   two-column grid, 0.36fr | 1fr, designed as eyebrow | body. The brand
   book allows one eyebrow per three sections, so four of the six openers
   on this page have none, and their single .opener__body child fell into
   column ONE: 299px of text with 829px of empty page beside it. That is
   the "What will it cost?" screenshot, and it is most of why the page read
   as flat.

   Single column is now the DEFAULT and the two-column form is opt-in on
   :has(), wrapped in @supports so a browser without :has() stacks the
   eyebrow rather than inheriting the bug. Children are placed explicitly
   too, so the layout no longer depends on source order either. */
.opener {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.25rem, 4vw, 3.5rem);
  align-items: start;
}
@supports selector(:has(*)) {
  @media (min-width: 62.01rem) {
    .opener:has(.eyebrow) { grid-template-columns: minmax(0, 0.36fr) minmax(0, 1fr); }
    .opener:has(.eyebrow) > .eyebrow { grid-column: 1; }
    .opener:has(.eyebrow) > .opener__body { grid-column: 2; }
  }
}
@media (max-width: 62rem) { .opener { gap: var(--s-16); } }

/* ONE PROSE MEASURE ON THE PAGE. The openers were capped at 46ch, about
   420px, while .lede-answer directly above them runs to 46rem, about
   740px. Two measures on one page, and the narrower one used five times
   out of six, is what still made the openers read as a leftover column
   rather than a chosen one even after the empty grid track was fixed.
   42rem is close enough to the lede to look like one system and stays
   inside a sane line length; the wide right margin that remains is white
   space, which is the point.

   A heading takes a shorter line than body copy, not a longer one: it is
   set large, so 22ch is already a wide block of type. Capping both at the
   same number is the other half of why these read as a strip. */
.opener__body { max-width: 42rem; }
/* MEASURE (--measure, and the arithmetic behind it, is in tokens.css).
   Set per PARAGRAPH rather than on the block.
   Before this the opener ran 42rem for everything, which is 102 characters
   at the 16px body size and 108 at 834 where the FAQ folds to one column.
   The target is 45 to 75 and the outer tolerance is 80, so it was half as
   long again as a line should be, at every width above the phone. That is
   what the page read as a wall.
   ⚠ On the PARAGRAPHS, not on .opener__body, deliberately. The block keeps
   its 42rem so the column and everything aligned to it stay exactly where
   they were: this is a typography fix, not a layout change, and nothing
   moves except where the lines break. It also puts the lede, the lead and
   the body on ONE measure for the first time, which is the thing the 42rem
   was reaching for when it replaced the old 46ch. */
.opener__body > p,
.opener__body > .lead,
.feature > div > p,
.feature > div > .lead { max-width: calc(var(--measure-ch) * 1ch); }
.opener__body > .h,
.opener__body > .h-lg,
.opener__body > h2 { max-width: 22ch; }
.opener:has(.eyebrow) .opener__body > .h { max-width: none; }

/* ---------------- cards ---------------- */

.card {
  background: var(--warm-sand);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-subtle);
  overflow: hidden;
}

.card--feature {
  border-radius: var(--r-specialty);
  padding: var(--s-36);
}

.card__media { aspect-ratio: 4 / 3; overflow: hidden; background: var(--driftwood); }
.card__media img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--dur) var(--ease); }
.card:hover .card__media img { transform: scale(1.03); }
.card__body { padding: var(--s-24); }
.card__body .eyebrow { margin-bottom: var(--s-10); }

/* Service card, links out */
a.card { display: block; text-decoration: none; transition: box-shadow var(--dur-fast) var(--ease); }
a.card:hover { box-shadow: var(--shadow-md); }

/* ---------------- .card--service ----------------
   Daniel: the six service cards "lack styling" next to the rest of the
   page. They were a photograph, a warm-sand panel, a title and one line,
   and warm-sand against Paper is a four-percent step, so the panel never
   read as a surface at all. A drop shadow was standing in for an edge and
   doing it badly.

   Built from what the page already owns rather than from new decoration:
   the squared surface, the hairline rules that carry the warmth, the
   Sunset accent that marks ONE thing at a time, and the arrow box the
   primary button already uses. Three changes, each with a job:

   1. A real edge. 1px --line at rest, --line-strong on hover, no shadow.
   2. A keyed rule under the photograph. Neutral hairline at rest; the
      accent wipes across it from the left on hover, so the accent marks
      the card you are pointing at instead of all six at once. "If
      everything is Sunset, nothing is."
   3. An arrow box that earns the card being a link. Outlined at rest,
      filled on hover. It is the same 24px box as the primary CTA, so the
      affordance is the site's, not a new one.

   ⛔ NO NUMBERED INDEX, though it was the obvious third element and it
   was tried. Numbering six services 01 to 06 states an order that does
   not exist: decks are not step one of bathrooms. The process list below
   is numbered because that IS a sequence.

   ⛔ Accent is --sunset-hi (#ae4718), never --sunset. On warm sand it
   measures 4.70:1 and white on it measures 5.66:1; plain Sunset is the
   dark-ground graphic colour and fails both. */
.card--service {
  display: flex;
  flex-direction: column;
  background: var(--warm-sand);
  border: 1px solid var(--line);
  box-shadow: none;
  transition: border-color var(--dur-fast) var(--ease-ui);
}
a.card--service:hover { box-shadow: none; }
.card--service .card__media { flex: 0 0 auto; }
/* THREE BANDS, NOT TWO. The arrow first sat absolutely in the bottom
   right corner, which left a different amount of dead space in every card
   because the captions are different lengths. As a real footer row with a
   rule above it, the card has a structure (photograph, what it is, where
   it goes) and the text block simply flexes to fill whatever is left. */
.card--service .card__body {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  border-top: 1px solid var(--line);
}
/* the accent, wiping in over the hairline above */
.card--service .card__body::before {
  content: "";
  position: absolute;
  top: -1px; left: 0; right: 0;
  height: 2px;
  background: var(--sunset-hi);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-fast) var(--ease-ui);
}
.card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-16);
}
/* The heading has to be allowed to wrap, or a long title pushes the arrow
   out of the card instead of taking a second line. */
.card__head > .h-sm { min-width: 0; }
.card__arw {
  margin-top: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px; height: 28px;
  flex: 0 0 auto;
  border: 1.5px solid var(--line-strong);
  color: var(--cedar);
  font-size: var(--fs-14);
  line-height: 1;
  transition: background-color var(--dur-fast) var(--ease-ui),
              border-color var(--dur-fast) var(--ease-ui),
              color var(--dur-fast) var(--ease-ui),
              transform var(--dur-fast) var(--ease-ui);
}

/* Hover is gated: a touch device fires it on tap and leaves the card stuck
   in its hover state. focus-visible carries the same reveal for a keyboard,
   which is the whole point of doing this with state rather than a shadow. */
.card--service:focus-within { border-color: var(--line-strong); }
.card--service:focus-within .card__body::before { transform: scaleX(1); }
.card--service:focus-within .card__arw {
  background: var(--sunset-hi); border-color: var(--sunset-hi); color: var(--white);
}
@media (hover: hover) and (pointer: fine) {
  .card--service:hover { border-color: var(--line-strong); }
  .card--service:hover .card__body::before { transform: scaleX(1); }
  .card--service:hover .card__arw {
    background: var(--sunset-hi);
    border-color: var(--sunset-hi);
    color: var(--white);
    transform: translateX(2px);
  }
}
.card--service:active { transform: scale(0.995); }

/* ---------------- grids ---------------- */

.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-24); }
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-24); }
@media (max-width: 62rem) { .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 46rem) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }

/* ---------------- media ----------------
   Contained in 12px-radius containers unless the image genuinely
   earns full bleed. Judged per image, not by rule. */

.media {
  border-radius: var(--r-media);
  overflow: hidden;
  background: var(--driftwood);
}
.media img { width: 100%; height: auto; }
.media--4x3 { aspect-ratio: 4 / 3; }
.media--3x2 { aspect-ratio: 3 / 2; }
.media--16x9 { aspect-ratio: 16 / 9; }
.media--square { aspect-ratio: 1 / 1; }
.media[class*="media--"] img { height: 100%; object-fit: cover; }

.media-caption {
  margin-top: var(--s-12);
  font-size: var(--fs-14);
  color: var(--cedar);
}
.on-night .media-caption, .on-walnut .media-caption { color: var(--on-dark-soft); }

/* Full bleed, used sparingly and only where the image earns it. */
.bleed {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  border-radius: 0;
  overflow: hidden;
  background: var(--driftwood);
  height: clamp(18rem, 44vw, 34rem);
}
.bleed img { width: 100%; height: 100%; object-fit: cover; }

/* ---------------- feature row ---------------- */

.feature {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: center;
}
.feature--flip > :first-child { order: 2; }
@media (max-width: 62rem) {
  .feature { grid-template-columns: 1fr; gap: var(--s-24); }
  .feature--flip > :first-child { order: 0; }
}

/* ---------------- stat row ---------------- */

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr)); gap: var(--s-30); }
.stat__value {
  display: block;
  font-family: var(--display);
  font-size: var(--fs-h);
  font-weight: 900;
  line-height: 1;
  letter-spacing: var(--track-display);
  color: var(--ink);
}
.on-night .stat__value, .on-walnut .stat__value { color: var(--paper); }
.stat__label { display: block; margin-top: var(--s-12); font-size: var(--fs-14); color: var(--cedar); max-width: 24ch; }
.on-night .stat__label, .on-walnut .stat__label { color: var(--on-dark-soft); }

/* ---------------- quotes ---------------- */

.quote {
  background: var(--warm-sand);
  border-radius: var(--r-specialty);
  padding: var(--s-36);
  box-shadow: var(--shadow-subtle);
}
.stars {
  display: inline-flex;
  gap: 3px;
  color: var(--sunset-hi);
  margin-bottom: var(--s-16);
}
.on-night .stars, .on-walnut .stars { color: var(--gold); }
.stars svg { display: block; }

.quote blockquote {
  margin: 0;
  font-size: var(--fs-lead);
  line-height: 1.4;
  letter-spacing: -0.008em;
  color: var(--ink);
}
.quote figcaption { margin-top: var(--s-24); }
.quote__name { display: block; font-size: var(--fs-14); font-weight: var(--w-h1); color: var(--ink); }
.quote__meta { display: block; margin-top: 2px; font-size: var(--fs-12); color: var(--cedar); }

/* ---------------- note ---------------- */

.note {
  border-left: 3px solid var(--sunset-btn);
  padding-left: var(--s-18);
  font-size: var(--fs-16);
  color: var(--cedar);
}

/* ---------------- list ---------------- */

.list { display: grid; gap: 0; }
.list__item {
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 1fr);
  gap: var(--s-16);
  padding-block: var(--s-24);
  border-bottom: 1px solid var(--line);
}
.list__item:first-child { border-top: 1px solid var(--line); }
.list__num { font-size: var(--fs-14); font-weight: var(--w-label); color: var(--sunset-hi); }

/* ---------------- form ---------------- */

.form { display: grid; gap: var(--s-18); }
.field { display: grid; gap: var(--s-8); }
.field label { font-size: var(--fs-14); font-weight: var(--w-med); color: var(--ink); }
.field input,
.field textarea,
.field select {
  font-family: var(--sans);
  font-size: var(--fs-15);
  color: var(--ink);
  background: var(--wash-input);
  border: 1px solid var(--wash-border);
  border-radius: var(--r-input);
  padding: var(--s-14) var(--s-16);
  width: 100%;
  transition: border-color var(--dur-fast) var(--ease);
}
.field input:focus, .field textarea:focus, .field select:focus { border-color: var(--ink); }
.field textarea { min-height: 9rem; resize: vertical; }
.field__hint { font-size: var(--fs-12); color: var(--cedar); }
.form__row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-18); }
@media (max-width: 46rem) { .form__row { grid-template-columns: 1fr; } }

.hp {
  position: absolute !important;
  width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* ---------------- contact rail ---------------- */

.rail {
  display: grid;
  gap: 0;
  /* Queried by the email address below, which has to size itself off the
     column it actually got rather than off the viewport: the same rail is
     353px wide in the footer at 1440 and 480px on /contact/. */
  container-type: inline-size;
}
.rail__item { padding-block: var(--s-18); border-bottom: 1px solid var(--line); }
.rail__item:first-child { border-top: 1px solid var(--line); }
.rail__value {
  display: block;
  margin-top: var(--s-6);
  font-size: var(--fs-h-sm);
  letter-spacing: var(--track-h-sm);
  font-weight: var(--w-h1);
  color: var(--ink);
  text-decoration: none;
}
a.rail__value:hover { color: var(--sunset-hi); }

/* elevatedcarpentry101@gmail.com IS THE PROBLEM CHILD OF THIS RAIL, and the
   fix is to make it fit rather than to choose where it breaks.
   Measured: in Archivo at the rail's weight the address renders at a
   constant 16.05x its font size, so it wants 385px at the 24px top of
   --fs-h-sm. The footer's brand column is 353px at 1280 and above and
   280px at 320, so it broke at BOTH ends of the range and only fitted in
   the 390-1024 middle. `overflow-wrap: anywhere` did not cause that, but it
   is what made the break land inside the address ("...@gmai / l.com").
   1cqi is one hundredth of the rail, so 5.8cqi holds the address at 93% of
   whatever width it was given, and the clamp stops it ever growing past the
   phone number beside it. The phone is 169px and keeps the full heading
   size, which is the right hierarchy anyway: you ring a builder.
   ⚠ Specificity, not order: a.rail__value--email (0,1,1) beats
   .rail__value (0,1,0) wherever either lands in this file. */
a.rail__value--email {
  font-size: clamp(0.9375rem, 5.8cqi, var(--fs-h-sm));
  letter-spacing: -0.01em;
}

/* ---------------- video ---------------- */

/* The only real clip is a phone video: 960x1706, so roughly 9:16. It used to
   sit in a 16/9 frame with object-fit:cover, which threw away about two thirds
   of every frame. These ratios are chosen to match the source, not the layout:
   3/4 on wide screens where a 9:16 block would be absurdly tall, and the true
   9/16 on phones, where it crops nothing at all. */
.reel__video {
  position: relative;
  border-radius: var(--r-media);
  overflow: hidden;
  background: var(--driftwood);
  aspect-ratio: 3 / 4;
  max-width: 34rem;
}
@media (max-width: 40rem) {
  .reel__video { aspect-ratio: 9 / 16; max-width: none; }
}
.reel__video video { width: 100%; height: 100%; object-fit: cover; }
.reel__badge {
  position: absolute;
  left: var(--s-12); top: var(--s-12);
  padding: var(--s-6) var(--s-12);
  border-radius: var(--r-pill);
  /* Solid, not translucent. At 0.86 the badge measured 4.30:1 against a
     bright frame of the poster, and the poster's brightness shifts with
     load timing, so the ratio was drifting either side of the line. */
  background: var(--night);
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--paper);
}

/* A 9 second autoplaying loop counts as motion the visitor must be able
   to stop. Sits opposite the badge, keyboard reachable like any button. */
.reel__toggle {
  position: absolute;
  right: var(--s-12); top: var(--s-12);
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  padding: var(--s-6) var(--s-16);
  border: 0;
  border-radius: var(--r-pill);
  background: var(--night);
  color: var(--paper);
  font-family: var(--sans);
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}
.reel__toggle:hover { background: var(--sunset-hi); }

/* ---------------- footer ---------------- */

.footer {
  background: var(--night);
  color: var(--paper);
  padding-block: var(--s-96) var(--s-42);
  /* No top margin: it showed a linen strip between the walnut CTA
     band and the walnut footer, which read as a stray white bar. */
}
.footer h2, .footer h3 { color: var(--paper); }
.footer .eyebrow { color: var(--gold); }

.footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr));
  gap: clamp(2rem, 4vw, 3.5rem);
}
/* The brand block keeps the full width when the columns fold, so the
   contact details never end up in a narrow half-column. */
@media (max-width: 74rem) {
  .footer__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .footer__brand { grid-column: 1 / -1; margin-bottom: var(--s-24); }
}
@media (max-width: 52rem) { .footer__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
@media (max-width: 34rem) { .footer__grid { grid-template-columns: minmax(0, 1fr); } }

.footer__blurb {
  margin-top: var(--s-18);
  max-width: 34ch;
  font-size: var(--fs-lead);
  line-height: var(--lh-lead);
  color: var(--on-dark-soft);
}
.footer__brand .rail { margin-top: var(--s-30); max-width: 30rem; }
.footer__area {
  margin-top: var(--s-16);
  font-size: var(--fs-15);
  line-height: 1.7;
  color: var(--on-dark-soft);
}
.footer__find { margin-top: var(--s-30); }
.footer__grid .eyebrow + .footer__list { margin-top: var(--s-16); }

/* The name is the logo now. Height-driven, because the artwork is fixed
   aspect and the old text block it replaces was sized by its type; width
   auto keeps it honest if the asset is ever re-cut. Capped so it cannot
   outgrow the column on a phone. */
.footer__logo {
  display: block;
  height: clamp(64px, 7vw, 92px);
  width: auto;
  max-width: 100%;
}
/* .footer__name is gone: the footer partial is shared by all ten pages, so
   swapping the text for the logo removed its only use. */
.footer__list { display: grid; gap: var(--s-5); }
.footer__list a {
  font-size: var(--fs-15);
  color: var(--on-dark-soft);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
  /* Measured 23px tall. The row is full-width so it is easy to hit
     horizontally, but vertically it was under the 44px thumb target. */
  display: block;
  padding-block: var(--s-12);
  margin-block: calc(var(--s-12) * -0.5);
}
.footer__list a:hover { color: var(--gold); }

.footer .rail__item { border-color: var(--line-on-dark); }
/* 43px for the phone and 35px for the address at 320, both under the thumb
   target, on the two links in the footer that exist to be tapped. */
.footer a.rail__value {
  display: inline-flex; align-items: center;
  min-height: 44px; padding-block: var(--s-5);
}
.footer .rail__value { color: var(--paper); }
/* Kept as the safety valve, not as the fix. 30 unbreakable characters set
   the min-content width of whatever they land in and once pushed the
   document 37px wide at 320px in the footer and 17px on /contact/. The
   minmax(0,...) above lets the grid track shrink and this lets the address
   give way rather than push. With the container-query size above it no
   longer fires at any width we test, but a longer address one day would
   overflow the page without it. */
.rail__value { overflow-wrap: anywhere; }
.footer a.rail__value:hover { color: var(--gold); }

.footer__base {
  padding-bottom: env(safe-area-inset-bottom, 0px);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-12) var(--s-30);
  justify-content: space-between;
  margin-top: var(--s-72);
  padding-top: var(--s-24);
  border-top: 1px solid var(--line-on-dark);
  font-size: var(--fs-12);
  color: var(--on-dark-soft);
}

/* ---------------- reveal: calm ---------------- */

[data-reveal] {
  opacity: 0;
  transform: translateY(var(--rise));
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
[data-reveal].is-in { opacity: 1; transform: none; }
.no-js [data-reveal] { opacity: 1; transform: none; }

/* ================================================================
   MOTION
   Every effect below is transform/opacity only, so it stays on the
   compositor. Anything scroll-linked is wrapped in @supports and
   degrades to the plain static layout, never to a broken one.
   base.css kills the lot under prefers-reduced-motion.
   ================================================================ */

/* ---------------- hero ---------------- */

.hero__media { position: relative; overflow: hidden; }

/* The hero copy settles in once, on load. Not scroll-linked: the first
   thing a visitor sees should not depend on them scrolling. */
@media (prefers-reduced-motion: no-preference) {
  .hero__content > * {
    animation: rise var(--dur-slow) var(--ease) both;
  }
  .hero__content > *:nth-child(2) { animation-delay: 90ms; }
  .hero__content > *:nth-child(3) { animation-delay: 180ms; }
  .hero__content > *:nth-child(4) { animation-delay: 270ms; }
}

@keyframes rise {
  from { opacity: 0; transform: translate3d(0, calc(var(--rise) * 2), 0); }
  to   { opacity: 1; transform: none; }
}

/* ---------------- parallax on full-bleed bands ----------------
   Native scroll-driven animation where the browser has it. The image is
   scaled up first so the drift never exposes an edge. Text never moves:
   parallax on type is what makes a page feel cheap and it hurts reading. */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .bleed img {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
      will-change: transform;
    }
  }
}

@keyframes drift {
  from { transform: translate3d(0, calc(var(--parallax) * -1), 0) scale(1.18); }
  to   { transform: translate3d(0, var(--parallax), 0) scale(1.18); }
}

/* ---------------- before / after slider ----------------
   The one interaction that genuinely belongs on a builder's site: it is
   the proof, not decoration. Works from a range input, so it is keyboard
   accessible and needs no pointer events to be usable. */

.ba {
  position: relative;
  border-radius: var(--r-media);
  overflow: hidden;
  background: var(--driftwood);
  aspect-ratio: 4 / 3;
  touch-action: pan-y;
}
.ba img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  user-select: none;
  -webkit-user-drag: none;
}
/* the "after" frame is revealed from the left as the handle travels */
.ba__after { clip-path: inset(0 0 0 var(--ba-pos, 50%)); }

.ba__range {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  margin: 0; padding: 0;
  opacity: 0;
  cursor: ew-resize;
  z-index: 3;
}
.ba__range:focus-visible + .ba__handle {
  outline: 3px solid var(--sunset);
  outline-offset: 2px;
}
.ba__handle {
  position: absolute; top: 0; bottom: 0;
  left: var(--ba-pos, 50%);
  width: 2px;
  background: var(--paper);
  transform: translateX(-1px);
  pointer-events: none;
  z-index: 2;
  box-shadow: var(--shadow-sm);
}
.ba__handle::after {
  content: "";
  position: absolute; top: 50%; left: 50%;
  width: 40px; height: 40px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--paper);
  box-shadow: var(--shadow-md);
}
.ba__tag {
  position: absolute; bottom: var(--s-12);
  padding: var(--s-6) var(--s-12);
  border-radius: var(--r-pill);
  background: rgba(11, 10, 7, 0.82);
  color: var(--paper);
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  pointer-events: none;
  z-index: 2;
}
.ba__tag--before { left: var(--s-12); }
.ba__tag--after  { right: var(--s-12); }

/* ---------------- stat count-up ----------------
   Tabular figures so the number does not jitter while it counts. */
.stat__value { font-variant-numeric: tabular-nums; }

/* ================================================================
   THE HERO
   ================================================================
   A single photographic plate, a scrim, the decorative wordmark and the
   card. The wordmark reads over the photograph in full: an earlier build
   layered a silhouette cut from the plate on top of it so the ridge and
   the roofline passed in front of the letters, and that is deliberately
   gone. Nothing hides any part of the type.

   ⛔ The plate is GENERATED and depicts work Max did not build. It is a
   hero background only: alt="", aria-hidden, never captioned, never in a
   gallery. See docs/GENERATED-ASSETS.md.
   ---------------------------------------------------------------- */

.vhero {
  position: relative;
  background: var(--night);
  isolation: isolate;
}

.vhero__frame {
  position: relative;
  container-type: inline-size;
  /* `width: 100%` is NOT optional and `max-width` is not a substitute.
     With only `aspect-ratio` set, the width is auto, so whenever a height
     constraint binds Chromium derives the width back FROM the height:
     `min-height` won between about 780 and 860px and pushed the frame
     wider than the viewport, and `max-height: 88svh` won on any short wide
     window and pulled it NARROWER, which is the black band down the right
     of the hero on a 1890x940 screen. Fixing the width fixes both: height
     then comes from the ratio and is clamped, and the width never moves. */
  width: 100%;
  aspect-ratio: 16 / 9;
  /* The floor is what stops the composition being crushed on a short
     window, and it is itself capped by the window so it can never force a
     hero taller than the screen. 16:9 at 834px wide is a 469px frame, and
     the lockup plus the card do not fit in that. */
  min-height: min(88svh, 44rem);
  /* 100svh, not 92. The frame is width-driven, so on a wide short window
     the 16:9 height cannot be met and the picture is cropped to fit: at
     1920x912 it was a 2.29:1 letterbox showing only 78% of the plate, 11%
     cut off the top and 11% off the bottom. A 1440 laptop sees 99% of it.
     Giving the frame the whole viewport height takes the wide case to 85%
     and makes the hero read as full screen, which is what the comp does. */
  max-height: 100svh;
  overflow: hidden;
  background: var(--night);
  /* `size`, not `inline-size`: the wordmark has to be able to clamp itself
     against the frame's HEIGHT as well as its width, which needs cqh. Safe
     here because nothing inside contributes to the frame's own size. */
  container-type: size;
}

/* <picture> would otherwise emit an inline box of its own around an
   already absolutely positioned child. */
.vhero picture { display: contents; }

.vhero__plate {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 58% 50%;
  display: block;
  z-index: 0;
}

/* The video sits in the plate's exact box, with the plate's crop, so the
   two are in register and the fade between them reads as the picture
   coming to life rather than as a swap. It starts hidden and is revealed
   by site.js on `playing`; if the file stalls, never loads, or is refused
   on a thin pipe, what stays on screen is the still. */


/* ⛔ The clip runs eight seconds and starts itself, which is exactly the
   case the guidelines require a control for. It is a real <button>, it is
   visible rather than hover-revealed, and it reports its state. It is
   only ever shown once a video has actually been given a source, so the
   reduced-motion and Save-Data paths do not offer a control over a still. */


/* The clip sits in the plate's exact box, with the plate's crop, so the
   two are in register and the fade between them reads as the picture
   coming to life rather than as a swap. Revealed on `playing`; if it
   stalls, never loads, or is refused on a thin pipe, what stays on screen
   is the still. */
.vhero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 58% 50%;
  display: block;
  z-index: 0;
  opacity: 0;
  transition: opacity 620ms var(--ease);
  pointer-events: none;
}
.vhero__video[data-playing="true"] { opacity: 1; }

/* ---------------- the hero picker (review tool, build-flag only) ----------------
   HERO_PICKER=1 stacks all four heroes in the frame and shows one. The
   plates are already position:absolute inset:0, so the wrapper only has to
   give them a box and carry the on/off state.
   ⛔ Not in the default build. See src/pages/home.mjs and the gate. */
.vhero__opt {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 420ms var(--ease);
}
.vhero__opt[data-on] { opacity: 1; visibility: visible; }

/* Option 2's sky is a bright gold exactly where the wordmark sits, and it
   measured 2.78:1 behind ELEVATED at 1440 against a 3.0 floor for large
   text. The other three sit between 3.2 and 4.0, so deepening the shared
   scrim would darken three good heroes to rescue one. The scrim is a
   sibling AFTER the options, so it can be deepened for that option alone.
   Re-measured after: see the note in QUESTIONS.md. */
.vhero__opt[data-opt="2"][data-on] ~ .vhero__scrim {
  background:
    linear-gradient(to bottom, rgba(11, 10, 7, 0.76) 0%, rgba(11, 10, 7, 0.30) 26%, rgba(11, 10, 7, 0) 44%),
    linear-gradient(to top right, rgba(11, 10, 7, 0.38) 0%, rgba(11, 10, 7, 0) 46%);
}
@media (prefers-reduced-motion: reduce) { .vhero__opt { transition: none; } }

.heropick { padding-block: var(--s-36) var(--s-42); background: var(--paper); }
.heropick__lede {
  margin: var(--s-10) 0 var(--s-24);
  font-size: var(--fs-lead);
  color: var(--cedar);
}
.heropick__row { display: flex; align-items: center; gap: var(--s-16); }
.heropick__list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-16);
  flex: 1; min-width: 0;
}
@media (max-width: 46rem) {
  .heropick__list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The arrows are a nicety on a phone; the thumbnails are the control. */
  .heropick__arw { display: none; }
}
.heropick__item {
  position: relative; display: block; width: 100%; padding: 0;
  background: none; border: 1px solid var(--line); cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-ui);
}
.heropick__item img { display: block; width: 100%; height: auto; }
.heropick__item[aria-pressed="true"] { border-color: var(--sunset-btn); }
/* The ring is drawn OUTSIDE the box so turning it on cannot move the grid. */
.heropick__item[aria-pressed="true"]::after {
  content: ""; position: absolute; inset: -4px;
  border: 2px solid var(--sunset-btn); pointer-events: none;
}
.heropick__n {
  position: absolute; top: 0; left: 0;
  min-width: 30px; padding: var(--s-5) var(--s-8);
  background: var(--night); color: var(--white);
  font-size: var(--fs-14); font-weight: var(--w-h1); line-height: 1;
}
.heropick__name {
  display: block; padding: var(--s-10) var(--s-12);
  font-size: var(--fs-14); line-height: 1.35; color: var(--ink); text-align: left;
}
.heropick__arw {
  flex: none; width: 44px; height: 44px; padding: 0;
  background: none; border: 1px solid var(--line); color: var(--ink);
  font-size: 18px; line-height: 1; cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-ui), color var(--dur-fast) var(--ease-ui);
}
.heropick__arw:hover { border-color: var(--line-strong); color: var(--sunset-hi); }
.heropick__now {
  margin-top: var(--s-18);
  font-size: var(--fs-15); font-weight: var(--w-med); color: var(--cedar);
}

/* Top band only, so the header and the top of the wordmark have
   something to sit on without dulling the deck lower down. */
.vhero__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    /* The top stop carries the header now that the header has no ground of
       its own. At 0.46 the nav links measured 3.36:1 against the sunset
       behind them and the menu button 3.22:1. */
    linear-gradient(to bottom, rgba(11, 10, 7, 0.66) 0%, rgba(11, 10, 7, 0.24) 26%, rgba(11, 10, 7, 0) 44%),
    linear-gradient(to top right, rgba(11, 10, 7, 0.38) 0%, rgba(11, 10, 7, 0) 46%);
}

/* Lockup at the top, card at the bottom, and the gap between them is
   whatever is left. They cannot overlap, because they are siblings in a
   flow rather than two things anchored to opposite edges. */
.vhero__stack {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(1rem, 4cqh, 3rem);
  padding: calc(var(--masthead-h) + clamp(0.75rem, 3cqh, 2.25rem))
           clamp(1rem, 2.9cqw, 3rem)
           clamp(1rem, 2.9cqw, 3rem);
  /* the photograph stays selectable/clickable between the two blocks */
  pointer-events: none;
}
.vhero__stack > * { pointer-events: auto; }
.vhero__lockup { text-align: center; }

/* The decorative brand wordmark. NOT the H1: it carries no keyword and
   it is aria-hidden, because a screen reader announcing "ELEVATED" ahead
   of the real headline is noise. */
.vhero__word {
  margin: 0;
  text-align: center;
  font-family: var(--display);
  font-weight: 900;
  /* Clamped on BOTH axes. Sizing purely off the width is what let the
     type keep growing on a wide, short window while the frame it sits in
     was being clamped by the viewport height. */
  /* Pulled back from 10.6cqw. The type was crowding the picture. */
  font-size: var(--word-size, min(10.1cqw, 28cqh));
  line-height: 0.9;
  letter-spacing: -0.014em;
  color: var(--white);
  /* Tight and low-opacity. The previous stack ran to a 60px spread at 52%
     and pooled into a grey halo around every letter, which is what made
     the type look pasted on rather than lit. */
  text-shadow:
    0 1px 2px rgba(11, 10, 7, 0.3),
    0 4px 16px rgba(11, 10, 7, 0.3);
}

/* Above the cutout, not behind it: the strip carries information and it
   sits low enough that the ridge would swallow it.

   It therefore sits ON the photograph, and measured against a sunset sky
   white type came out at 2.77:1. A text-shadow does not fix that, it just
   makes it look fixed: the shadow spreads outward, so the pixels actually
   behind the strokes stay bright. The band below is a real ground, faded
   out at both ends so it reads as light rather than as a box. */
.vhero__strip {
  /* inline-block, so the box HUGS the text. As a block it spanned the whole
     lockup while its text was only 30cqw of it, which meant the vignette had
     to be sized against a box four times wider than the thing it was
     shading, and the contrast gate was sampling bright hillside from parts
     of the box that hold no text at all. Both problems are the same problem. */
  display: inline-block;
  position: relative;      /* the vignette below is positioned against it */
  margin: clamp(0.5rem, 1.4cqh, 1.25rem) 0 0;
  text-align: center;
  color: var(--white);
  font-size: max(0.75rem, 1.02cqw);   /* 12px floor, per the brand book */
  font-weight: var(--w-label);
  letter-spacing: var(--track-strip);
  text-indent: var(--track-strip);   /* tracking pushes the block left */
  text-transform: uppercase;
  text-shadow: 0 1px 3px rgba(11, 10, 7, 0.85);
}
/* ⛔ NO BOX ON THE STRIP. It had a chip, and before that a blurred
   ellipse, because on its own it measured 2.7:1 against the sunlit
   horizon. Both read as a patch stuck on the photograph. The ground it
   needs belongs to the LOCKUP, not to one line inside it: one soft shade
   sitting behind the wordmark and the strip together, which is what the
   approved comp has. It darkens the sky the type sits on and stops at the
   type, so it reads as the light in the photograph rather than as a box.
   Sized in percentages of the lockup and pulled down past the strip,
   because an ellipse centred on the lockup is weakest exactly where the
   strip sits. */
.vhero__lockup {
  position: relative;
  isolation: isolate;
}
.vhero__lockup::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -16% -8% -26% -8%;
  background: radial-gradient(ellipse 62% 58% at 50% 46%,
    rgba(11, 10, 7, 0.66) 0%, rgba(11, 10, 7, 0.58) 46%,
    rgba(11, 10, 7, 0.26) 72%, rgba(11, 10, 7, 0) 100%);
}

/* One grain over everything, so the type and the photograph share a
   texture instead of looking pasted together. */
.vhero__grain {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  opacity: 0.12;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/></filter><rect width='160' height='160' filter='url(%23n)'/></svg>");
}

/* Lower left, and it is the anchor of the composition: rating, H1, lede,
   the one primary action and the phone. */
.vhero__card {
  align-self: flex-start;
  /* Wider than it was. At min(38cqw, 33rem) the 33rem cap bound on every
     desktop and forced the H1 to three lines in a narrow column. */
  /* Proportional, and the rem cap pulled in. This is the whole reason the
     page looked better zoomed out: the card was pinned at 576px, so on a
     1920 window it took 30% of the frame and on a 2374 one only 24%. Sized
     against the frame it keeps the same share at every width. */
  width: min(36cqw, 32rem);
  max-width: 100%;
  padding: clamp(1.1rem, 1.9cqw, 2rem);
  background: rgba(11, 10, 7, 0.8);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--r-card);
  color: var(--white);
}
.vhero__rating {
  margin: 0 0 var(--s-12);
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
}
.vhero__title {
  margin: 0 0 var(--s-10);
  font-size: clamp(1.25rem, 2.05cqw, 1.9rem);
  font-weight: var(--w-h1);
  line-height: 1.18;
  letter-spacing: -0.014em;
  color: var(--white);
  text-wrap: balance;
}
.vhero__lede {
  margin: 0 0 var(--s-18);
  max-width: 34ch;
  font-size: clamp(1rem, 1.14cqw, 1.125rem);
  line-height: 1.5;
  color: #dfe1db;                    /* 12.4:1 on the card ground */
}
.vhero__card .btn-row { gap: var(--s-16); }
.vhero__card .btn { font-size: clamp(0.875rem, 1.04cqw, 0.9375rem); }

/* The card is a Night surface. Without this the secondary CTA inherits the
   light-ground rule and the phone number renders Ink on Night at 1.2:1. */
.vhero__card .btn--secondary {
  color: var(--white);
  background: transparent;
  border-color: transparent;
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.5);
}
.vhero__card .btn--secondary:hover {
  color: var(--gold);
  background: transparent;
  box-shadow: inset 0 -2px 0 var(--gold);
}

@media (prefers-reduced-transparency: reduce) {
  .vhero__card { background: rgba(11, 10, 7, 0.97); backdrop-filter: none; -webkit-backdrop-filter: none; }
}

/* Portrait. The frame goes taller, the crop pulls toward the house so the
   roof is still there to cut the wordmark, and the card takes the full
   width because a 38cqw card on a phone is a column of two-word lines. */
@media (max-width: 46rem) {
  /* Height first, ratio second. The portrait crop is cut at 0.6 to match a
     phone at 78svh, so cover has almost nothing left to trim and
     sizes="100vw" is true again. */
  .vhero__frame {
    aspect-ratio: auto;
    height: 78svh;
    /* 30rem was not enough for the lockup plus the card on a 360x640
       phone: 78svh is 499px there and the card was being clipped off the
       bottom of the frame, which loses the only CTA above the fold. */
    min-height: 34rem;
    max-height: none;
  }
  /* Centred and set low. On the portrait crop the roof is barely in frame,
     so the ridge is the occluder and the word has to come down to meet it.
     Measured against the shipped portrait cutout: 26% of the letters
     hidden across the middle, 21% on the tail, 38% at worst. */
  .vhero__plate { object-position: 50% 50%; }
  .vhero__stack { justify-content: flex-start; }
  .vhero__lockup { margin-bottom: auto; }
  /* The portrait crop puts the strip lower against brighter ground and the
     lockup is a smaller target, so the same shade measured 4.26:1 here
     against 9.10:1 at desktop. Deeper and pulled further down. */
  .vhero__lockup::before {
    inset: -18% -10% -36% -10%;
    background: radial-gradient(ellipse 68% 60% at 50% 48%,
      rgba(11, 10, 7, 0.78) 0%, rgba(11, 10, 7, 0.7) 48%,
      rgba(11, 10, 7, 0.3) 74%, rgba(11, 10, 7, 0) 100%);
  }
  .vhero__word  { --word-size: min(15.2cqw, 22cqh); }
  .vhero__strip {
    font-size: max(0.6875rem, 2.6cqw);
    letter-spacing: 0.28em;
    text-indent: 0.28em;
  }
  .vhero__card {
    align-self: stretch;
    width: auto;
    padding: 4.4cqw;
  }
  /* Floors, not raw cqw. On a 390px phone 3.4cqw is 13px, and the brand
     book's rule is that body text never goes below 16px on this site. */
  .vhero__rating { font-size: max(0.75rem, 2.9cqw); margin-bottom: var(--s-8); }
  .vhero__title { font-size: max(1.25rem, 5.1cqw); }
  .vhero__lede  { font-size: max(1rem, 3.4cqw); max-width: none; margin-bottom: var(--s-14); }
  .vhero__card .btn-row { gap: var(--s-10); }
  .vhero__card .btn { font-size: max(0.875rem, 3.5cqw); padding: var(--s-12) var(--s-16); }
  .vhero__strip { font-size: max(0.75rem, 2.6cqw); }
}

/* ================================================================
   THE PHOTO HERO — .vhero--photo
   ================================================================
   Arnold, 2026-08-24, on the delivered site: "better to have a standard
   beautiful photo as the frontpage and then have the deck automation
   further down". So the home page opens on one of Max's own photographs
   and the film is a feature below the service cards.

   ⛔ IT IS THE SAME COMPONENT, DELIBERATELY. The frame, the plate, the
   scrim, the stack, the card and the grain above are reused whole; only
   the deltas a real daylight photograph needs are written here. A second
   parallel hero component is how a brand starts drifting, and this file
   already carries that warning above .tour.

   THE THREE DELTAS, and each one is a measurement rather than a taste
   call:

   1. NO LOCKUP. The giant ELEVATED wordmark needs a dark radial ground
      under it to reach AA, and it was tuned over a generated dusk sky.
      This photograph is bright, with pale sky exactly where the letters
      would sit, so the type would either fail or need a dark smudge
      painted across the middle of the picture Arnold asked to lead with.
      The wordmark keeps its home on the film below, where the frames are
      Night. With one child, the stack's space-between put the card at the
      top, so the stack is told to end-align here.

   2. A LIGHTER SCRIM, WEIGHTED DIFFERENTLY. The shared scrim darkens the
      top for the header and the lower-left for the card. Over a night
      plate that reads as atmosphere; over a bright photograph the same
      wash reads as a dirty picture. The top band stays, because the
      masthead still has no ground of its own and the logo is white. The
      diagonal is replaced by a shallow bottom lift that only reaches the
      band the card sits in, so three quarters of the frame is the
      photograph undarkened.

   3. A CREDIT LINE. This is the one picture on the site big enough to be
      taken for stock, and the film below it carries a provenance line
      saying it is generated. Without the counterpart the visitor has no
      way to tell which is which. It says whose work it is, on the work.
   ---------------------------------------------------------------- */

.vhero--photo .vhero__stack { justify-content: flex-end; }

.vhero--photo .vhero__scrim {
  background:
    /* Header band. 0.58 at the very top, gone by 32%: the white lockup and
       the nav pill sit in the first 84px and nothing below them needs it. */
    linear-gradient(to bottom, rgba(11, 10, 7, 0.58) 0%, rgba(11, 10, 7, 0.2) 18%, rgba(11, 10, 7, 0) 32%),
    /* Card band. A lift under the bottom third only, so the card's own
       Night glass has something to sit against and the pergola, the deck
       and the sky are left alone. */
    linear-gradient(to top, rgba(11, 10, 7, 0.52) 0%, rgba(11, 10, 7, 0.18) 18%, rgba(11, 10, 7, 0) 38%);
}

/* ⛔ THE PHONE NEEDS A DEEPER HEADER BAND, AND THE GATE IS WHY THIS EXISTS.
   The portrait crop puts pale sky directly behind the bar, and on a 390px
   phone the menu button is a 13px white glyph rather than a 15px link on a
   glass pill, so it has the least ground of anything on the page: it
   measured 3.84:1 against a 4.5 floor while the desktop composition passed
   at 5.41:1. The band is deepened over the masthead's own height only and
   is gone by 30% of the frame, so the picture below the bar is untouched.
   ⚠ Re-measure after ANY change to the portrait crop window. Whatever that
   window's top edge lands in is what this band has to cover. */
@media (max-width: 46rem) {
  .vhero--photo .vhero__scrim {
    background:
      linear-gradient(to bottom, rgba(11, 10, 7, 0.78) 0%, rgba(11, 10, 7, 0.6) 12%, rgba(11, 10, 7, 0.22) 22%, rgba(11, 10, 7, 0) 34%),
      linear-gradient(to top, rgba(11, 10, 7, 0.52) 0%, rgba(11, 10, 7, 0.18) 18%, rgba(11, 10, 7, 0) 38%);
  }
}

/* Bottom right, opposite the card, small and quiet. It is a real sentence
   rather than a caption chip because it is making a factual claim about
   provenance, which is the same job the footer note does for the film. */
.vhero--photo .vhero__credit {
  position: absolute;
  /* ⚠ vw, not cqw. The credit is a child of .vhero, and the container that
     carries `container-type: size` is .vhero__frame INSIDE it, so cqw here
     would silently resolve against the viewport anyway. Saying vw means
     the value is what it looks like. The inset matches the stack's own
     padding so the credit and the card share one margin. */
  right: clamp(1rem, 2.9vw, 3rem);
  bottom: clamp(1rem, 2.9vw, 3rem);
  z-index: 6;
  margin: 0;
  max-width: min(46vw, 30rem);
  text-align: right;
  font-size: var(--fs-12);
  line-height: 1.45;
  letter-spacing: 0.01em;
  /* Paper rather than pure white, and shadowed rather than boxed: a chip
     here would be a third surface in the corner of a photograph. Measured
     on the darkest and the lightest ground this line can land on. */
  color: rgba(246, 243, 240, 0.92);
  text-shadow: 0 1px 3px rgba(11, 10, 7, 0.92), 0 2px 14px rgba(11, 10, 7, 0.7);
  pointer-events: none;
}

/* On a phone the card is full width at the bottom of the frame, so there
   is no corner left for the credit to sit in. It becomes an ordinary line
   of text on the section's Night ground under the picture instead of
   overlapping the CTAs.
   ⛔ This is why the credit is a child of .vhero and NOT of .vhero__frame:
   the frame is overflow:hidden and every other child of it is absolutely
   positioned, so a static credit inside it would paint at the top of the
   photograph rather than below it. */
@media (max-width: 46rem) {
  .vhero--photo .vhero__credit {
    position: static;
    max-width: none;
    text-align: left;
    padding: var(--s-12) clamp(1rem, 4.4vw, 2rem) var(--s-16);
    color: var(--on-dark-soft);
    text-shadow: none;
    font-size: max(0.75rem, 2.7vw);
  }
}

/* ================================================================
   THE FILM'S SECTION LEAD
   ================================================================
   The film stopped being the hero on 2026-08-24, which means it needs
   what every other section on the page has: a name, a sentence saying
   what it is, and — because it is the one piece of generated imagery on
   the page — the provenance line, in the reading path, immediately above
   it.

   ⛔ THE NOTE IS NOT FINE PRINT AND IT IS NOT STYLED LIKE FINE PRINT. It
   is set at body size on a ruled ground so it reads as part of the copy.
   The footer keeps its own site-wide version; this one exists because a
   visitor who never reaches the footer still has to be told. */
.film-lead__note {
  margin: var(--s-24) 0 0;
  padding-left: var(--s-18);
  border-left: 2px solid var(--sunset);
  max-width: calc(var(--measure-ch) * 1ch);
  font-size: var(--fs-15);
  line-height: 1.6;
  color: var(--paper);
}

/* The tour's own title card carries the same fact in one line, for anyone
   who scrolled into the film past the section lead. */
.vhero__note {
  margin: var(--s-14) 0 0;
  font-size: var(--fs-12);
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: #c9cbc4;                    /* 8.9:1 on the card ground */
}

/* ---------------- hero motion ----------------
   The plate drifts, the wordmark travels against it. Two planes rather
   than the three the cutout allowed, but the parallax between type and
   photograph is what gave the hero its depth and it survives. Native
   scroll-driven animation only, wrapped in @supports, so a browser
   without it gets the static composition rather than a broken one.
   transform only, so it stays on the compositor. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .vhero__frame { view-timeline-name: --hero; }
    .vhero__plate,
    .vhero__video,
    .vhero__lockup {
      animation: linear both;
      animation-timeline: --hero;
      animation-range: contain 0% cover 100%;
      will-change: transform;
    }
    /* scaled first, so the drift can never expose an edge. The video
       takes the SAME keyframes as the plate: they are stacked and
       cross-fading, so if only one of them drifts the fade tears. */
    .vhero__plate,
    .vhero__video { animation-name: hero-far; }
    .vhero__lockup { animation-name: hero-type; }
  }
}
/* 1.02, not 1.06. The scale is what was cropping the landscape away;
   a smaller push shows more of the picture and still parallaxes. */
@keyframes hero-far  { from { transform: translate3d(0, -1.1%, 0) scale(1.02); } to { transform: translate3d(0, 1.1%, 0) scale(1.02); } }
@keyframes hero-type { from { transform: translate3d(0, 0, 0); }                to { transform: translate3d(0, -9%, 0); } }

/* The card settles in once on load. Not scroll-linked: the first thing a
   visitor sees should not depend on them scrolling. */
@media (prefers-reduced-motion: no-preference) {
  .vhero__card > * { animation: rise var(--dur-slow) var(--ease) both; }
  .vhero__card > *:nth-child(2) { animation-delay: 80ms; }
  .vhero__card > *:nth-child(3) { animation-delay: 160ms; }
  .vhero__card > *:nth-child(4) { animation-delay: 240ms; }
}

/* ================================================================
   THE BUILD TIMELINE — sticky, scroll-scrubbed
   ================================================================
   A tall track with a sticky viewport inside it. Scroll position maps to
   currentTime, eased so the seek is not jittery.

   ⛔ ON MOBILE IT AUTOPLAYS INSTEAD. Scrubbing needs the whole file
   buffered before a seek does anything, and on a phone that is both a
   large download and visibly janky. The two paths are chosen in
   src/js/site.js, and the file served differs with them: the desktop cut
   carries a keyframe every half second so seeking lands, the mobile cut
   carries a normal GOP and is a quarter of the size.
   ---------------------------------------------------------------- */

/* The scrub layout is OPT IN. The default is a plain 16:9 block, and
   src/js/site.js sets data-mode="scrub" only when it is actually going to
   drive currentTime. Defaulting the other way round left a page with JS
   off showing four blank viewports of sticky nothing. */
.track { position: relative; background: var(--night); }
.track[data-mode="scrub"] { height: 420vh; }

.stick {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--night);
}
.track[data-mode="scrub"] .stick {
  position: sticky;
  top: 0;
  height: 100svh;
  aspect-ratio: auto;
}

.stick video,
.stick__poster {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.stick__poster { position: absolute; inset: 0; }

/* Bottom band, so the stage labels and the progress bar always have a
   ground. The frame brightens as the deck is finished, and a label that
   passed on the first frame failed on the last. */
/* The frame brightens as the deck is finished, so this is sized against
   the LAST frame, not the first. On a phone the section is a 16:9 block
   about 220px tall and the same copy stack fills most of it, so the band
   has to run higher and hold its opacity further up: at 58% with a linear
   ramp the caption measured 2.96:1 there against 11.5:1 on desktop. */
.stick__scrim {
  position: absolute;
  inset: auto 0 0 0;
  height: 62%;
  pointer-events: none;
  background: linear-gradient(to top,
    rgba(11, 10, 7, 0.94) 0%, rgba(11, 10, 7, 0.88) 42%,
    rgba(11, 10, 7, 0.6) 74%, rgba(11, 10, 7, 0) 100%);
}
@media (max-width: 46rem) {
  .stick__scrim {
    height: 88%;
    background: linear-gradient(to top,
      rgba(11, 10, 7, 0.94) 0%, rgba(11, 10, 7, 0.9) 55%,
      rgba(11, 10, 7, 0.66) 82%, rgba(11, 10, 7, 0) 100%);
  }
}

/* Caption, note and stage row are ONE bottom-anchored stack. They were
   three separately positioned blocks and the caption sat on top of the
   note at 900px tall, because a clamp() tuned at one viewport height
   silently overlaps at another. A flow layout cannot do that. */
.stick__copy {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0 var(--gutter) calc(var(--s-24) + 4px);
  display: flex;
  flex-direction: column;
  gap: var(--s-12);
}

.stick__cap {
  margin: 0;
  max-width: 16ch;
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(1.75rem, 4.2vw, 3.25rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--white);
}
.stick__note {
  margin: 0;
  max-width: 46ch;
  font-size: var(--fs-15);
  line-height: 1.45;
  color: var(--on-dark-soft);
}

.stages {
  display: flex;
  gap: var(--s-16);
  margin: var(--s-12) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--white);
  border-top: 1px solid var(--line-on-dark);
  padding-top: var(--s-14);
}
.stages li { flex: 1; opacity: 0.42; transition: opacity var(--dur-fast) var(--ease-ui); }
.stages li[data-on="true"] { opacity: 1; }
/* On a phone four labels at a readable size do not fit across 350px, and
   shrinking them to fit puts the type at 10px, under the brand book's own
   12px floor for micro labels. Showing only the live stage keeps the size
   and loses nothing: the progress bar already carries the position. */
@media (max-width: 46rem) {
  .stages { gap: 0; font-size: var(--fs-12); letter-spacing: 0.14em; }
  .stages li { display: none; }
  .stages li[data-on="true"] { display: block; flex: 0 0 auto; }
  .stick__cap { max-width: 13ch; font-size: 1.625rem; }
  .stick__note { display: none; }
}

/* ---- the scrub progress bar ----
   It was a 4px sliver of Sunset on the very bottom edge with nothing behind
   it, so there was no unfilled state to read against and it disappeared
   into the photograph. It is a real track now: a visible unfilled rail, a
   tick at each of the four stage boundaries so the position means
   something, and a Sunset fill with a soft glow so it separates from the
   picture whatever the frame behind it is doing. */
.bar-track {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 10px;
  background-color: rgba(11, 10, 7, 0.55);
  background-image:
    repeating-linear-gradient(to right,
      transparent 0, transparent calc(25% - 2px),
      rgba(255, 255, 255, 0.34) calc(25% - 2px), rgba(255, 255, 255, 0.34) 25%);
  border-top: 1px solid rgba(255, 255, 255, 0.22);
  overflow: hidden;
  z-index: 4;
}
.bar {
  height: 100%;
  width: 0;
  background: var(--sunset);
  box-shadow: 0 0 14px rgba(217, 102, 46, 0.75);
}

/* ---- the phone treatment of the timeline (Daniel, 2026-08-24) ----
   Play and poster modes only exist under 48em, where the film is a small
   16:9 box. The desktop overlay (scrim + caption ON the film) drowned it
   there, the same failure the tour panels had: "the entire thing is
   covered with a gradient and texts". Same cure as the tour band: the
   film shows CLEAN at its own aspect, the caption and stage sit under it
   on the Night ground, and the progress rail hugs the film's bottom
   edge. The Pause pill keeps its own ground on the film. Scrub mode
   (desktop, sticky full-screen) is untouched. */
@media (max-width: 48em) {
  .track[data-mode="play"] .stick,
  .track[data-mode="poster"] .stick {
    aspect-ratio: auto;
    overflow: visible;
  }
  .track[data-mode="play"] .stick video,
  .track[data-mode="poster"] .stick video {
    height: auto;
    aspect-ratio: 16 / 9;
  }
  .track[data-mode="play"] .stick__poster,
  .track[data-mode="poster"] .stick__poster {
    inset: 0 0 auto 0;
    height: auto;
    aspect-ratio: 16 / 9;
  }
  .track[data-mode="play"] .stick__scrim,
  .track[data-mode="poster"] .stick__scrim { display: none; }
  .track[data-mode="play"] .stick__copy,
  .track[data-mode="poster"] .stick__copy {
    position: static;
    padding: var(--s-18) var(--gutter) var(--s-24);
  }
  /* Below the film there is room again for the one-line stage note. */
  .track[data-mode="play"] .stick__note,
  .track[data-mode="poster"] .stick__note { display: block; }
  .track[data-mode="play"] .bar-track,
  .track[data-mode="poster"] .bar-track {
    bottom: auto;
    top: calc(100vw * 0.5625 - 10px);
  }
}

/* The scrub track holds the visitor in place while it plays, so the
   progress bar is the only thing telling them the page is still moving.
   With motion reduced there is no scrub, so there is nothing to report. */
/* Belt and braces. site.js already refuses to enter scrub mode under
   reduced motion, but if it ever did, this un-sticks it anyway. */
@media (prefers-reduced-motion: reduce) {
  .track, .track[data-mode="scrub"] { height: auto; }
  .track[data-mode="scrub"] .stick { position: relative; height: auto; aspect-ratio: 16 / 9; }
  .bar-track { display: none; }
}
/* Nothing to report when there is no scrub. */
.track:not([data-mode="scrub"]) .bar-track { display: none; }

.stick .reel__toggle { z-index: 3; }

/* ---------------- proof strip ---------------- */

.proof {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--s-30) var(--s-42);
}
.proof__item { padding-top: var(--s-18); border-top: 2px solid var(--sunset-btn); }
.on-night .proof__item { border-top-color: var(--sunset); }
.proof__value {
  display: block;
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.on-night .proof__value { color: var(--paper); }
.proof__label {
  display: block;
  margin-top: var(--s-12);
  max-width: 26ch;
  font-size: var(--fs-14);
  line-height: 1.5;
  color: var(--cedar);
}
.on-night .proof__label { color: var(--on-dark-soft); }

/* ---------------- nav pill ----------------
   Brand book: interactive pills get full radius, everything else is
   square. The nav is the one place on the site that gets a pill group. */
.nav__pill {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-6);
  border-radius: var(--r-pill);
  background: var(--wash);
  border: 1px solid transparent;
}
.nav__pill .nav__link {
  padding: var(--s-10) var(--s-16);
  border-radius: var(--r-pill);
  transition: background var(--dur-fast) var(--ease-ui), color var(--dur-fast) var(--ease-ui);
}
.nav__pill .nav__link[aria-current="page"] { background: var(--ink); color: var(--paper); }
.nav__pill .nav__link:hover { background: rgba(20, 17, 12, 0.08); color: var(--ink); }
.nav__pill .nav__link[aria-current="page"]:hover { background: var(--ink); color: var(--paper); }

/* Light glass, as the comp has it, not the dark chip that was here. It is
   10% white rather than the 18% it looks: at 18% white type measured 3.4:1
   against the sunset behind it. The hero's top scrim carries the rest. */
.page-home .nav__pill {
  /* A white tint OVER a dark base, not a white tint alone. Alone it took
     whatever the sunset behind it was doing and one link measured 3.54:1;
     the dark base gives every link the same ground whatever it sits on,
     and the white tint on top is what makes it read as light glass. */
  background: linear-gradient(rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.10)),
              rgba(11, 10, 7, 0.30);
  border-color: rgba(255, 255, 255, 0.22);
  backdrop-filter: blur(12px) saturate(115%);
  -webkit-backdrop-filter: blur(12px) saturate(115%);
}
@media (min-width: 62.01rem) {
  .page-home .nav__pill .nav__link[aria-current="page"] {
    background: var(--white);
    color: var(--ink);
  }
}
/* In the drawer there is no chip: a white pill inside a Night sheet reads
   as a button, and the ink on it measured 1.05:1 against the sheet. */
@media (max-width: 62rem) {
  .page-home .nav__link[aria-current="page"] { color: var(--gold); background: none; }
}
.page-home .nav__pill .nav__link:hover { background: rgba(255, 255, 255, 0.18); color: var(--white); }
.page-home .nav__pill .nav__link[aria-current="page"]:hover { background: var(--white); color: var(--ink); }

/* On a phone the pill becomes the stacked drawer, so it drops the group
   treatment entirely rather than trying to be both. */
@media (max-width: 62rem) {
  .nav__pill,
  .nav__rest,
  .nav__foot,
  .page-home .nav__pill {
    display: contents;
    background: none;
  }
  /* The label over the six service rows: the footer's "What we build"
     eyebrow, so the drawer's second group reads as a deliberate site map
     rather than an unexplained second menu (Daniel, 2026-08-24). */
  .nav__rest-label {
    display: block;
    margin: var(--s-24) 0 var(--s-6);
    font-size: var(--fs-12);
    font-weight: var(--w-label);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    /* The drawer ground differs by page: Paper on the nine light pages
       (Sunset Hover holds 5.11:1 there), Night on home (Gold, 11:1).
       Plain Gold on Paper measured 1.61:1 and failed the gate. */
    color: var(--sunset-hi);
  }
  .page-home .nav__rest-label { color: var(--gold); }
  .nav__foot .masthead__tel { padding-block: var(--s-16); font-size: var(--fs-18); }
}

/* ---------------- home page spec components ----------------
   The answer-first lede, the process list titles, the trust line, the FAQ
   and the recency stamp. All from the approved page spec. */

/* Carries the category term in the first 100 words, so it is set as a
   statement rather than as body copy. */
.lede-answer {
  max-width: 46rem;
  margin: 0;
  font-size: var(--fs-h-sm);
  line-height: 1.45;
  letter-spacing: var(--track-h-sm);
  font-weight: var(--w-med);
  color: var(--ink);
  text-wrap: pretty;
}
.on-night .lede-answer { color: var(--paper); }

.list__title {
  margin: 0;
  font-size: var(--fs-18);
  font-weight: var(--w-h1);
  line-height: 1.3;
  color: var(--ink);
}
.on-night .list__title { color: var(--paper); }
.on-night .list__item { border-bottom-color: var(--line-on-dark); }
.on-night .list__item:first-child { border-top-color: var(--line-on-dark); }
.on-night .list__num { color: var(--sunset); }

/* One line, three facts, all true today. It is the trust block every
   competitor fills with a licence and an insurer we cannot claim. */
.proof-line {
  margin: var(--s-42) 0 0;
  padding-top: var(--s-24);
  border-top: 2px solid var(--sunset-btn);
  font-size: var(--fs-lead);
  font-weight: var(--w-h1);
  letter-spacing: -0.01em;
  color: var(--ink);
}
/* ⛔ Cedar, not Board. Board #938581 is 3.21:1 on Paper, which the brand
   book records as borders and graphics only, never text. A separator IS
   text as far as a contrast checker is concerned, and the gate caught it. */
.proof-line span { color: var(--cedar); padding-inline: var(--s-6); }

.tags { display: flex; flex-wrap: wrap; gap: var(--s-8); margin: 0; padding: 0; list-style: none; }
.tags li {
  padding: var(--s-6) var(--s-14);
  border-radius: var(--r-pill);
  background: var(--wash);
  font-size: var(--fs-14);
  color: var(--cedar);
}

.faq { display: grid; gap: 0; }
.faq__item {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  gap: clamp(1.25rem, 4vw, 3.5rem);
  padding-block: var(--s-30);
  border-bottom: 1px solid var(--line);
}
.faq__item:first-child { border-top: 1px solid var(--line); }
@media (max-width: 62rem) {
  .faq__item { grid-template-columns: 1fr; gap: var(--s-10); }
}
.faq__q {
  margin: 0;
  font-size: var(--fs-18);
  font-weight: var(--w-h1);
  line-height: 1.3;
  letter-spacing: -0.012em;
  color: var(--ink);
  text-wrap: balance;
}
/* Same 54ch measure. The FAQ is worst at 834, where the two column grid
   folds to one and the answer inherits the whole 767px container: 106
   characters a line, on the block a visitor is most likely to actually
   read. */
.faq__a { margin: 0; color: var(--ink); max-width: calc(var(--measure-ch) * 1ch); }

.recency {
  margin: var(--s-42) 0 0;
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--on-dark-soft);
}

/* ---------------- masthead layout ----------------
   ⛔ MUST STAY AT THE END OF THIS FILE. Both rules below have the same
   specificity as the base .masthead__inner and .masthead__tel rules
   declared earlier, so at any earlier position the later declaration
   silently wins and the bar quietly falls back to flex. */
@media (min-width: 62.01rem) {
  /* A three column grid with the pill in the middle column. The two 1fr
     columns are equal by definition, so the pill is centred on the BAR
     rather than on whatever space the flanks happen to leave. */
  .masthead__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    column-gap: clamp(20px, 2.6vw, 48px);
  }
  .nav { display: contents; }
  .masthead__left { grid-column: 1; justify-self: start; }
  .nav__pill { justify-self: center; }
  /* Behind the menu button on a wide screen, in the drawer on a narrow one. */
  .nav__rest, .nav__foot, .nav__rest-label { display: none; }
  .masthead__right { grid-column: 3; justify-self: end; }
  .nav__pill .nav__link { font-size: var(--fs-15); padding: var(--s-10) var(--s-18); }
}
/* ⛔ The phone leaves the BAR on desktop, not the site. The brand book's
   pair is the quote and the phone together, and it is intact everywhere it
   matters: the hero card, every CTA block, the footer, and the mobile
   drawer, which is the only place a thumb goes looking for it. What it
   could not do was share the bar. Six nav labels make a 610px pill, and a
   pill centred on a 1280px container leaves each flank about 250px: enough
   for the CTA, not enough for the CTA and a phone number, so the two were
   touching at every desktop width. The comp centres five short labels. */
@media (min-width: 62.01rem) {
  .masthead__tel { display: none; }
}
/* Below 75rem the pill gives ground before the flanks do. */
@media (min-width: 62.01rem) and (max-width: 75rem) {
  .nav__pill .nav__link { font-size: var(--fs-13); padding: var(--s-8) var(--s-10); }
}

/* ================================================================
   THE HOUSE TOUR
   ================================================================
   The home page hero is a scroll-scrubbed drone tour of one house that
   stops at each service. Engine: src/js/tour.js. Frames: public/tour/,
   built by scripts/tour-frames.mjs.

   ⛔ IT REUSES THE HERO'S OWN FURNITURE ON PURPOSE. The scrim, the
   lockup, the wordmark, the micro strip, the card and the grain are the
   SAME .vhero__* components the still hero uses, nested inside .tour
   instead of inside .vhero__frame. They were tuned against measured
   contrast and a documented collision fix, and a second parallel set of
   hero components is exactly how a brand starts drifting. Only the stage
   and the canvas are new.

   ⛔ EVERY FRAME IS GENERATED IMAGERY. alt="", aria-hidden, never
   captioned, never a project, never in the photo library. The footer
   carries the provenance line. See docs/GENERATED-ASSETS.md.

   ⛔ THE TRACK'S HEIGHT IS THE FILM'S PACE, and it is set by the page,
   not here: home.mjs writes --tour-track-h from src/data/tour-stops.mjs,
   where the dwell mapping and the panel offsets are computed from the
   same number. 100svh of it is the sticky stage; everything above that
   is scroll distance. 680svh = the 30s film plus five ~half-screen
   reading holds, one per service stop. Shorten it and the panels race;
   lengthen it and the page feels like it will not end.
   ---------------------------------------------------------------- */

.tour-track {
  position: relative;
  height: var(--tour-track-h, 460svh);
  background: var(--night);
}

.tour {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  background: var(--night);
  /* `size`, not `inline-size`: the wordmark clamps against the stage's
     HEIGHT as well as its width, which needs cqh. Safe because nothing
     inside contributes to the stage's own size. */
  container-type: size;
  isolation: isolate;
}

/* The still and the canvas occupy one box and are in exact register, so
   the swap from one to the other is invisible.
   ⛔ THE STILL IS THE LCP ELEMENT AND IT IS NOT DECORATION. It paints
   first, carries fetchpriority="high", and is what stays on screen for
   anyone on reduced motion, Save-Data or a 2g pipe, and for anyone whose
   frames never arrive. */
.tour__still,
.tour__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
}
.tour__still { object-fit: cover; object-position: 50% 50%; }

/* Starts hidden. The canvas context is opaque, so before the first draw
   it is a black rectangle, and revealing it early would flash black over
   a correct still. tour.js sets data-tour-ready after the first frame
   has actually been painted. */
.tour__canvas {
  opacity: 0;
  transition: opacity 620ms var(--ease);
}
.tour[data-tour-ready] .tour__canvas { opacity: 1; }

/* Progress. A hairline, not a scrollbar: it tells you the hero is a
   thing you scroll through without asking to be read.
   ⛔ Sunset is the GRAPHIC accent on dark grounds, which is exactly what
   this is. It never sits on the photograph itself, only on this rule. */
.tour__bar-track {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  z-index: 7;
  background: rgba(246, 243, 240, 0.16);
}
/* ⛔ TRANSFORM, NOT WIDTH. This was `width: calc(var(--tour-p) * 100%)`,
   and --tour-p is rewritten on every animation frame of the scrub, so the
   rail forced a layout and a paint inside every single frame of the one
   interaction the section exists for. scaleX is compositor-only: the same
   rail, the same motion, no layout. `transform-origin: left` keeps it
   growing from the left edge, and scaleX(0) is a rail of nothing rather
   than a 1px sliver, which is what the width version gave at p=0. */
.tour__bar {
  height: 100%;
  width: 100%;
  transform: scaleX(var(--tour-p, 0));
  transform-origin: left center;
  will-change: transform;
  background: var(--sunset);
}

/* ---- what the copy does as the tour opens up ----
   The lockup and the card are the hero: they are fully present at the
   top, where the H1 has to be, and they clear out of the way once the
   film gets going. The closing panel comes back for the CTA.

   ⛔ SCRUB PATH ONLY. On the still path (reduced motion, Save-Data, a
   thin pipe, or no JS at all) --tour-p is never set, the default of 0
   applies, and the card stays fully visible for the whole section. That
   is why the fade is written as a function of the property rather than
   as an animation. */
/* ⛔ THE END PANEL MUST BE POSITIONED, NOT LEFT STATIC. As a static
   block it painted in the in-flow layer, which sits BELOW the
   absolutely-positioned canvas regardless of DOM order, so the closing
   CTA rendered invisibly behind the film from the day the tour shipped:
   every phase assertion passed because the attribute changed while the
   pixels never did. Overlay geometry mirrors .vhero__stack. */
.tour__end {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: calc(var(--masthead-h) + clamp(0.75rem, 3cqh, 2.25rem))
           clamp(1rem, 2.9cqw, 3rem)
           clamp(1rem, 2.9cqw, 3rem);
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
}
.tour__end .vhero__card { pointer-events: auto; }

/* The thresholds are custom properties written by home.mjs from the same
   table as the dwell mapping (src/data/tour-stops.mjs), so the card is
   always gone before the first service panel reaches the viewport and
   the closing panel waits for the camera to leave the house. The
   fallbacks match tour.js's fallbacks. */
/* ⛔ `.vhero__stack .vhero__card`, NEVER bare `.vhero__card`: the closing
   panel's card is ALSO a .vhero__card, and the bare selector faded it to
   zero and killed its pointer events exactly when the close arrived. That
   made the end CTA invisible AND unclickable while .tour__end's own
   computed style read visible - the second way this panel learned to
   fail while an assertion passed. qa-home now hit-tests the card itself. */
.tour[data-tour-mode="scrub"] .vhero__lockup,
.tour[data-tour-mode="scrub"] .vhero__stack .vhero__card {
  opacity: clamp(0, calc((var(--tour-open-end, 0.18) - var(--tour-p, 0)) * 14), 1);
}
.tour[data-tour-mode="scrub"][data-tour-phase="film"] .vhero__stack .vhero__card,
.tour[data-tour-mode="scrub"][data-tour-phase="close"] .vhero__stack .vhero__card { pointer-events: none; }

.tour[data-tour-mode="scrub"] .tour__end {
  opacity: clamp(0, calc((var(--tour-p, 0) - var(--tour-close-start, 0.84)) * 15), 1);
}
.tour[data-tour-mode="scrub"][data-tour-phase="close"] .tour__end { visibility: visible; }

/* The additions link inside the closing card: white with the same
   underline recipe as the card's secondary button, hover Gold. */
.tour__end-link {
  color: var(--white);
  text-decoration: none;
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.5);
  transition: color var(--dur-fast) var(--ease-ui), box-shadow var(--dur-fast) var(--ease-ui);
}
.tour__end-link:hover { color: var(--gold); box-shadow: inset 0 -2px 0 var(--gold); }

/* ---- the service stops ----
   Real DOM text, always present. ⛔ The panels APPEAR IN PLACE (Daniel,
   2026-08-23): while the dwell mapping holds a stop's frame, tour.js
   stamps data-tour-stop on the track and the matching panel fades in at
   the hero card's own lower-left position. They do not ride the scroll:
   a panel that slid up the screen while the film held read as page
   chrome, not as the film stopping to speak. Hidden panels are
   visibility:hidden, so they can neither swallow a click nor take tab
   focus, with no tabindex bookkeeping.

   DEFAULT (no JS) is the readable layout: the panels simply flow after
   the stage and scroll past like a page. The fixed overlay is opt-in via
   [data-tour-mode="scrub"], which only tour.js sets.

   The ground is the .vhero__card recipe on purpose (square, blur, 1px
   white at 10%): same family, no second dialect. */
.tour__stops {
  /* Positioned, or the no-JS stack paints invisibly BEHIND the sticky
     stage's layers: the exact paint-order trap that hid .tour__end. */
  position: relative;
  z-index: 6;
  display: grid;
  gap: var(--s-24);
  padding: var(--s-54) clamp(1rem, 2.9vw, 3rem);
}
.tour__panel {
  width: min(30rem, 100%);
  padding: clamp(1.1rem, 1.9vw, 1.75rem);
  background: rgba(11, 10, 7, 0.8);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--r-card);
  color: var(--white);
}

.tour-track[data-tour-mode="scrub"] .tour__stops { display: contents; }
.tour-track[data-tour-mode="scrub"] .tour__panel {
  position: fixed;
  left: clamp(1rem, 2.9vw, 3rem);
  bottom: clamp(1.25rem, 4.5vh, 3rem);
  z-index: 50;                        /* over the film, under the masthead */
  width: min(23rem, calc(100vw - 2 * clamp(1rem, 2.9vw, 3rem)));
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  translate: 0 8px;
  transition: opacity 320ms var(--ease), translate 320ms var(--ease),
              visibility 0s linear 320ms;
}
.tour-track[data-tour-mode="scrub"][data-tour-stop="deck"] .tour__panel[data-stop="deck"],
.tour-track[data-tour-mode="scrub"][data-tour-stop="kitchen"] .tour__panel[data-stop="kitchen"],
.tour-track[data-tour-mode="scrub"][data-tour-stop="bathroom"] .tour__panel[data-stop="bathroom"],
.tour-track[data-tour-mode="scrub"][data-tour-stop="carpentry"] .tour__panel[data-stop="carpentry"],
.tour-track[data-tour-mode="scrub"][data-tour-stop="basement"] .tour__panel[data-stop="basement"] {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  translate: 0 0;
  transition: opacity 320ms var(--ease), translate 320ms var(--ease);
}
/* The label is the .vhero__rating recipe: Gold, because the panel ground
   is translucent over photography and Gold holds 11:1 on Night where
   Sunset would sit near its floor. */
/* The panel's interior is an editorial composition rather than a caption
   (Daniel, 2026-08-23: "they look so boring"): glyph chip and route
   counter on the top row, the service name at display weight, one short
   Sunset rule (the brand's graphic accent, legal on dark), the copy, and
   the anchor carrying the same 24px arrow box the service cards use.
   Everything square, nothing shadowed, no new dialect. */
.tour__panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 0 0 var(--s-14);
}
/* The stop glyph in a square chip: inline stroke SVG like the stars — no
   icon font, no CDN. Gold, square caps per the square-surfaces rule. */
.tour__panel-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid rgba(255, 255, 255, 0.18);
}
.tour__panel-icon svg {
  display: block;
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.7;
  stroke-linecap: square;
}
/* Where you are on the route. The tour IS a sequence, so numbering it is
   legal where numbering the six services was not. */
.tour__panel-nav {
  display: inline-flex;
  align-items: center;
  gap: var(--s-10);
}
.tour__panel-count {
  font-size: var(--fs-13);
  font-weight: var(--w-label);
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
  color: var(--gold);
  margin-right: var(--s-6);
}
/* The route stepper: round pills (the one legal radius) with chevrons,
   deliberately a DIFFERENT shape, place and glyph from the square arrow
   box on the service link below - two controls, two purposes, and they
   no longer look like siblings (Daniel, 2026-08-23). Scrub-path only:
   in the still/no-JS stack a scroll-jump control is meaningless. */
.tour__panel-step {
  display: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  background: none;
  border: 1px solid var(--line-on-dark);
  border-radius: var(--r-pill);
  color: var(--on-dark-soft);
  cursor: pointer;
  flex: none;
  transition: color 160ms var(--ease-ui), border-color 160ms var(--ease-ui),
              transform 160ms var(--ease-ui);
}
.tour-track[data-tour-mode="scrub"] .tour__panel-step { display: inline-flex; }
.tour__panel-step svg {
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: square;
}
.tour__panel-step:hover { color: var(--gold); border-color: var(--gold); }
.tour__panel-step:active { transform: scale(0.94); }
.tour__panel-title {
  margin: 0 0 var(--s-12);
  font-size: clamp(1.3125rem, 1.6vw, 1.5rem);
  font-weight: var(--w-h2);
  letter-spacing: var(--track-h-sm);
  line-height: var(--lh-h-sm);
  color: var(--white);
  text-wrap: balance;
}
.tour__panel-rule {
  display: block;
  width: 44px;
  height: 2px;
  margin: 0 0 var(--s-14);
  background: var(--sunset);
}
.tour__panel-body {
  margin: 0 0 var(--s-16);
  font-size: var(--fs-15);
  line-height: 1.5;
  color: #dfe1db;                    /* 12.4:1 on the card ground */
}
.tour__panel-act {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-16);
}

.tour__panel-link {
  display: inline-flex;
  align-items: center;
  gap: var(--s-10);
  color: var(--white);
  font-weight: var(--w-med);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-ui);
}
/* The same 24px arrow box the service cards earned: outlined at rest,
   filled on hover, so the affordance is the site's rather than a bare
   underline. */
.tour__panel-arw {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  flex: none;
  border: 1px solid rgba(255, 255, 255, 0.35);
  font-size: var(--fs-14);
  line-height: 1;
  transition: background var(--dur-fast) var(--ease-ui),
              border-color var(--dur-fast) var(--ease-ui);
}
.tour__panel-link:hover { color: var(--gold); }
.tour__panel-link:hover .tour__panel-arw {
  background: var(--sunset-btn);
  border-color: var(--sunset-btn);
  color: var(--white);
}

@media (prefers-reduced-transparency: reduce) {
  .tour__panel { backdrop-filter: none; -webkit-backdrop-filter: none; background: rgba(11, 10, 7, 0.97); }
}

/* ---- the still path ----
   Reduced motion, Save-Data or a thin pipe (tour.js stamps the TRACK
   with data-tour-mode="still"), and reduced-motion users with no JS at
   all: the track collapses, the stage is one plain screen showing the
   still, and the five panels lay out in document order below it on the
   Night ground. That page is the accessibility page and the crawler
   page, and it reads top to bottom as hero, then the five services,
   then the rest of the page. */
.tour-track[data-tour-mode="still"] { height: auto; }
.tour-track[data-tour-mode="still"] .tour { position: relative; height: 100svh; }

@media (prefers-reduced-motion: reduce) {
  .tour__canvas { transition: none; }
  /* The same still-path layout as [data-tour-mode="still"] above, in
     pure CSS so it holds with JS off as well: the track collapses to
     the stage plus the panels in flow, because svh-loads of dead scroll
     with nothing moving is a trap. */
  .tour-track { height: auto; }
  .tour-track .tour { position: relative; height: 100svh; }
}

/* The stage is a fixed 100svh, so on a short window the lockup and the
   card have less room than the 16:9 hero gives them. Pull the wordmark
   back rather than letting it crowd the picture. */
@media (max-height: 40rem) {
  .tour .vhero__word { --word-size: min(9cqw, 20cqh); }
}

/* ---- THE PHONE BAND (Daniel, 2026-08-24) ----
   On a phone the film renders as a BAND at the frames' own aspect under
   the masthead, and everything written docks BELOW it on the Night
   ground: the hero card, the five stop panels and the closing card.
   ⛔ Never cover-crop the film full-screen on a portrait phone again:
   that stretched 600px frames ~6x in physical pixels (the 2026-08-24
   "mush" complaint) and put the panels ON the picture. Text and film do
   not stack here; the film is sharp because it renders near 1:1.
   Scrub-path only: the still path keeps its readable stacked page. */
@media (max-width: 46rem) {
  .tour-track[data-tour-mode="scrub"] {
    --tour-band-top: calc(var(--masthead-h) + 0.5rem);
    /* band bottom = top + width x 506/900, the mobile frames' own aspect */
    --tour-band-b: calc(var(--masthead-h) + 0.5rem + 100vw * 0.5622);
  }
  .tour[data-tour-mode="scrub"] .tour__still,
  .tour[data-tour-mode="scrub"] .tour__canvas {
    top: var(--tour-band-top);
    bottom: auto;
    height: auto;
    aspect-ratio: 900 / 506;
  }
  /* The scrim's two jobs (ground under the transparent bar, shade under
     the wordmark) both live in the band; the card zone below is plain
     Night and needs no shading. */
  .tour[data-tour-mode="scrub"] .vhero__scrim {
    bottom: auto;
    height: var(--tour-band-b);
  }
  /* Wordmark centred ON the band, strip riding under it inside the band. */
  .tour[data-tour-mode="scrub"] .vhero__stack {
    justify-content: flex-start;
    padding: 0 var(--gutter) calc(0.75rem + env(safe-area-inset-bottom, 0px));
  }
  .tour[data-tour-mode="scrub"] .vhero__lockup {
    position: absolute;
    left: 50%;
    top: calc(var(--tour-band-top) + 100vw * 0.2811);
    translate: -50% -50%;
    margin: 0;
    width: max-content;
    max-width: calc(100vw - 2 * var(--gutter));
  }
  .tour[data-tour-mode="scrub"] .vhero__word { --word-size: min(13.5cqw, 3.75rem); }
  /* The hero card docks below the band, full width. On a screen too short
     for all of it, the card scrolls inside itself rather than clipping
     its own CTAs. */
  .tour[data-tour-mode="scrub"] .vhero__stack .vhero__card {
    width: 100%;
    margin-top: calc(var(--tour-band-b) + 0.9rem);
    max-height: calc(100svh - var(--tour-band-b) - 1.9rem - env(safe-area-inset-bottom, 0px));
    overflow: auto;
  }
  /* The stop panels dock in the same slot, full width. */
  .tour-track[data-tour-mode="scrub"] .tour__panel {
    left: var(--gutter);
    right: var(--gutter);
    width: auto;
    top: calc(var(--tour-band-b) + 0.9rem);
    bottom: auto;
  }
  /* The closing card too. */
  .tour[data-tour-mode="scrub"] .tour__end {
    justify-content: flex-start;
    padding: calc(var(--tour-band-b) + 0.9rem) var(--gutter)
             calc(1rem + env(safe-area-inset-bottom, 0px));
  }
  .tour[data-tour-mode="scrub"] .tour__end .vhero__card { width: 100%; }
  /* Reading size and tap targets at phone scale. */
  .tour-track[data-tour-mode="scrub"] .tour__panel-body { font-size: 1rem; }
  .tour-track[data-tour-mode="scrub"] .tour__panel-step { width: 44px; height: 44px; }
}

/* The provenance line. Deliberately quiet, deliberately present.
   Sits below the copyright row, at caption size, on the dark footer. */
.footer__note {
  margin: var(--s-18) 0 0;
  max-width: 62ch;
  font-size: var(--fs-13);
  line-height: 1.55;
  color: var(--on-dark-soft);
}

/* The Telos credit. A quiet sign-off under the provenance line: logo link on
   its own row, the visible geography caption beneath it. The logo is white
   artwork for the Night ground, same reasoning as the footer logo above. */
.footer__credit {
  margin: var(--s-24) 0 0;
  padding-top: var(--s-18);
  border-top: 1px solid var(--line-on-dark);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-12) var(--s-18);
  font-size: var(--fs-12);
  color: var(--on-dark-soft);
}
.footer__credit a { display: inline-flex; }
.footer__credit img { display: block; opacity: .82; }
.footer__credit a:hover img { opacity: 1; }

/* ================================================================
   PORTED FROM THE SCROLL PROTOTYPE
   ================================================================
   The prototype at elevated-hero-scroll was built against a verbatim
   copy of this repo's tokens.css, so everything below is already inside
   the brand system rather than a second one bolted on. Only the pieces
   that earn their place came across: the site's own .feature, .note and
   .card already covered the rest.
   ---------------------------------------------------------------- */

/* The editorial numeral. A large Sunset figure above a heading, used to
   number a real sequence and nothing else.
   ⛔ Sunset #D9662E is the GRAPHIC accent and is only legal on a dark
   ground, where it measures 5.55:1. On Paper it fails, so the numeral
   uses Sunset Hover #AE4718 there, which measures 5.11:1.
   ⛔ Never number things that are not a sequence. Six services are not
   steps one to six; a build order is. */
.numeral {
  display: block;
  margin: 0 0 var(--s-12);
  font-family: var(--display);
  font-size: var(--fs-48);
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--sunset-hi);
}
.on-night .numeral { color: var(--sunset); }

/* Pill tabs. The one place full radius is legal, per the brand book:
   surfaces and buttons are square, interactive pills are round. */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-8);
  margin: 0 0 var(--s-30);
  padding: 0;
  list-style: none;
}
.tab {
  min-height: 44px;
  padding: var(--s-10) var(--s-18);
  background: transparent;
  color: var(--cedar);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font: inherit;
  font-size: var(--fs-14);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-ui),
              color var(--dur-fast) var(--ease-ui),
              border-color var(--dur-fast) var(--ease-ui);
}
.tab:hover { border-color: var(--line-strong); color: var(--ink); }
.tab[aria-selected="true"] {
  background: var(--ink); color: var(--paper);
  border-color: var(--ink); font-weight: var(--w-med);
}
.on-night .tab { color: var(--on-dark-soft); border-color: var(--line-on-dark); }
.on-night .tab:hover { color: var(--white); }
.on-night .tab[aria-selected="true"] {
  background: var(--paper); color: var(--ink); border-color: var(--paper);
}

/* The tabbed layer: one build stage at a time, a real photograph beside
   the words. Ported for /deck-building/, where the order of work is a
   buying argument. Without JS every layer simply stacks and reads. */
.layer {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: var(--s-42);
  align-items: center;
}
.layer[hidden] { display: none; }
.layer + .layer { margin-top: var(--s-42); }   /* the no-JS stack */
[data-tabs-ready] .layer + .layer { margin-top: 0; }
.layer__media { border: 1px solid var(--line); }
.on-night .layer__media { border-color: var(--line-on-dark); }
.layer__media img { width: 100%; height: auto; }
.layer__p {
  margin: var(--s-12) 0 var(--s-14);
  max-width: calc(var(--measure-ch) * 1ch);
}
.on-night .layer__p { color: var(--on-dark-soft); }
.layer__why {
  margin: 0;
  padding-left: var(--s-16);
  border-left: 2px solid var(--sunset-hi);
  color: var(--cedar);
}
.on-night .layer__why { border-left-color: var(--sunset); color: var(--on-dark-soft); }
@media (max-width: 52rem) { .layer { grid-template-columns: 1fr; gap: var(--s-24); } }

/* The seg buttons: the same pill grammar as .tab, but a picker that
   holds a choice rather than a view switch, so it keys off aria-pressed. */
.seg { display: flex; gap: var(--s-8); }
.seg--wrap { flex-wrap: wrap; }
.seg__b {
  min-height: 44px;
  padding: 0 var(--s-16);
  background: transparent;
  color: var(--cedar);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  font: inherit;
  font-size: var(--fs-14);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-ui),
              color var(--dur-fast) var(--ease-ui),
              border-color var(--dur-fast) var(--ease-ui);
}
.seg__b:hover { color: var(--ink); border-color: var(--ink); }
.seg__b[aria-pressed="true"] {
  background: var(--ink); color: var(--paper); border-color: var(--ink);
  font-weight: var(--w-med);
}
.on-night .seg__b { color: var(--on-dark-soft); border-color: var(--line-on-dark); }
.on-night .seg__b:hover { color: var(--white); border-color: var(--paper); }
.on-night .seg__b[aria-pressed="true"] {
  background: var(--paper); color: var(--ink); border-color: var(--paper);
}

/* The seg quote panel itself. */
.quote-seg { max-width: 44rem; }
.qfield { border: 0; padding: 0; margin: 0 0 var(--s-24); }
.qfield__label {
  display: block;
  margin-bottom: var(--s-10);
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--cedar);
}
.on-night .qfield__label { color: var(--on-dark-soft); }
.quote-seg__hint { margin: var(--s-14) 0 0; font-size: var(--fs-14); color: var(--cedar); }
.on-night .quote-seg__hint { color: var(--on-dark-soft); }
.quote-seg__hint.is-ok { color: var(--sunset-hi); }
.on-night .quote-seg__hint.is-ok { color: var(--gold); }
.quote-seg .btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.quote-seg .btn[disabled]:hover { background: var(--sunset-btn); }

/* ---- night-context fixes for components the band flip exposed ----
   The proof line and the quote-card stars were only ever used on Paper.
   On a Night band the line was Ink on Night, and the blanket
   `.on-night .stars` gold is wrong INSIDE a quote card, whose warm-sand
   surface is its own light ground. */
.on-night .proof-line { color: var(--paper); }
.on-night .proof-line span { color: var(--on-dark-soft); }
.on-night .quote { color: var(--ink); }
.on-night .quote .stars { color: var(--sunset-hi); }
.on-night .note { color: var(--on-dark-soft); border-left-color: var(--sunset); }
/* Gold links on night keep a visible underline: the Paper rule's dark
   red at 0.32 alpha disappears into the ground. */
.on-night .lead .link { border-bottom-color: rgba(232, 185, 143, 0.4); }
/* A card is its own LIGHT ground wherever the band puts it: the band's
   dark-context text rules must never reach inside the warm-sand surface,
   or the card titles render Paper on warm sand at 1.09:1. */
.on-night .card { color: var(--ink); }
.on-night .card h1, .on-night .card h2, .on-night .card h3,
.on-night .card .h-lg, .on-night .card .h, .on-night .card .h-sm { color: var(--ink); }
.on-night .card .caption, .on-night .card .dim { color: var(--cedar); }
.on-night .card .eyebrow { color: var(--sunset-hi); }

/* ================================================================
   THE FILM — a played video that stops at each service
   ================================================================
   Replaced the scroll-scrubbed canvas tour on 2026-08-24. Engine:
   src/js/film.js. Marks: src/data/tour-stops.mjs.

   ⛔ IT REUSES THE STOP PANELS' INTERIOR WHOLE. `.tour__panel-head`,
   `-icon`, `-count`, `-title`, `-rule`, `-body`, `-act`, `-link`, `-arw`
   are the SAME components the scrub used, and they stay. They were
   designed against measured contrast and a documented collision fix, and
   a second parallel set of panel components is how a brand starts
   drifting. Only the stage, the transport and the controls are new.

   ⛔ EVERY FRAME OF THE FILM IS GENERATED IMAGERY. The provenance line
   lives in .film-lead__note above it, on the closing panel, and in the
   footer. See docs/GENERATED-ASSETS.md.

   ⛔ THERE IS NO STICKY TRACK AND NO SCROLL HIJACK. That is the point of
   the rewrite. The section is about one screen tall, the film plays
   itself, and the visitor drives it with real buttons.
   ---------------------------------------------------------------- */

.film {
  position: relative;
  background: var(--night);
  isolation: isolate;
}
.film:focus { outline: none; }

.film__stage {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  /* Capped so the film can never be taller than the window: a 16:9 stage
     on a narrow tall window would otherwise push the controls off screen. */
  max-height: 92svh;
  /* ⛔ ROOM FOR BOTH, RATHER THAN HIDING ONE. At min(70svh, 34rem) a
     portrait tablet got a 16:9 stage of 432-576px, and a ~400px hero card
     plus the wordmark do not fit in that — qa-home caught them colliding
     at 1024x1366, 834x1112 and 768x1024. Raising the floor lets the stage
     be tall enough on a narrow window; object-fit: cover crops the film,
     which is what a hero is supposed to do.
*/
  min-height: min(78svh, 46rem);
  overflow: hidden;
  background: var(--night);
  container-type: size;
}

/* The poster is the video's own first frame, so the still and the moving
   picture are one image and the swap between them is invisible. */
.film__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  z-index: 0;
}

/* Ground for the panel and the controls, and nothing more. The film is
   the thing being looked at, so the middle of the frame is left alone. */
.film__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    /* ⛔ THE HEADER BAND, ADDED 2026-08-24 WHEN THE FILM BECAME THE HERO.
       It was not here before because the film sat below the fold and the
       masthead was never over it. With the film first, the bar has no
       ground of its own and the opening frame is a bright golden-hour
       aerial: the nav links measured 4.48:1 and the menu button 2.73:1
       against a 4.5 floor. Gone by 30%, so the picture is untouched. */
    linear-gradient(to bottom, rgba(11, 10, 7, 0.62) 0%, rgba(11, 10, 7, 0.24) 16%, rgba(11, 10, 7, 0) 30%),
    linear-gradient(to top, rgba(11, 10, 7, 0.72) 0%, rgba(11, 10, 7, 0.34) 22%, rgba(11, 10, 7, 0) 46%),
    linear-gradient(to right, rgba(11, 10, 7, 0.5) 0%, rgba(11, 10, 7, 0) 44%);
}

/* ---- the stops ----
   DEFAULT (no JS, reduced motion, Save-Data, a thin pipe) is the readable
   layout: the panels simply flow after the poster and the section reads as
   an ordinary page. The overlay positioning is opt-in from the play path,
   which is what keeps the still path correct rather than degraded. */
.film__panels {
  position: relative;
  z-index: 4;
  display: grid;
  gap: var(--s-18);
  padding: var(--s-24) clamp(1rem, 2.9vw, 3rem) var(--s-30);
}
.film__panel {
  font-size: var(--fs-15);
  /* The .vhero__card recipe: Night glass, square, one hairline, no
     shadow. Same surface as the hero card and the masthead, three depths
     of one material, per the brand book. */
  width: min(34cqw, 30rem);
  max-width: 100%;
  padding: clamp(0.95rem, 1.5cqw, 1.4rem);
  background: rgba(11, 10, 7, 0.82);
  backdrop-filter: blur(14px) saturate(120%);
  -webkit-backdrop-filter: blur(14px) saturate(120%);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--r-card);
  color: var(--white);
}
@media (prefers-reduced-transparency: reduce) {
  .film__panel { background: rgba(11, 10, 7, 0.97); backdrop-filter: none; -webkit-backdrop-filter: none; }
}

/* ---- the play path ----
   One panel at a time, lower left, over the film. `visibility` rather
   than opacity alone, so a hidden panel can neither swallow a click nor
   take tab focus and there is no tabindex bookkeeping to get wrong. */
/* ⛔ THE CONTAINER CARRIES THE WIDTH, NOT THE PANEL. Every panel inside it
   is absolutely positioned, so with only a max-width here the container
   had no in-flow content and collapsed to zero — and `max-width: 100%` on
   the panel then resolved against zero and rendered a 54px sliver of
   padding. The QA gate caught it by measuring the painted box rather than
   the attribute, which is the whole reason it measures pixels. */
/* ⛔ SMALLER (Daniel, 2026-08-24), AND WITH A FLOOR. 34cqw of a 768px
   stage is a 261px column, which does not make the box smaller — it makes
   it TALLER, because the copy wraps to twice as many lines. The floor is
   what actually keeps it compact on a narrow window.
*/
.film[data-film-mode="play"] .film__panels {
  position: absolute;
  inset: auto auto clamp(4.5rem, 9cqh, 7rem) clamp(1rem, 2.9cqw, 3rem);
  display: block;
  padding: 0;
  width: min(max(28cqw, 20rem), 26rem, 92cqw);
}
/* ⛔ NOT `--ease` HERE, AND THAT WAS THE WHOLE PROBLEM. The house easing
   token is cubic-bezier(0.19, 1, 0.22, 1) — an expo-out that covers ~90%
   of its distance in the first sixth of the duration. It is right for a
   reveal that should feel decisive; on a panel swapping every few seconds
   it reads as the box being thrown at you and then crawling the last few
   pixels. Daniel: "the box comes in violently and lacks a subtle
   animation". A gentle ease-out over a longer duration is the fix, plus a
   shorter travel — 6px, not 10.

   ⛔ AND THEY CROSS-FADE, THEY DO NOT COLLIDE. The outgoing panel leaves
   quickly (200ms) and the incoming waits for it (140ms delay) so there is
   never a moment with two panels legible on top of each other, which is
   the other half of what made it feel abrupt. */
.film[data-film-mode="play"] .film__panel {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  opacity: 0;
  visibility: hidden;
  transform: translateY(6px);
  transition: opacity 200ms cubic-bezier(0.4, 0, 1, 1),
              transform 200ms cubic-bezier(0.4, 0, 1, 1),
              visibility 0s linear 200ms;
}
.film[data-film-mode="play"][data-film-stop="deck"] .film__panel[data-stop="deck"],
.film[data-film-mode="play"][data-film-stop="kitchen"] .film__panel[data-stop="kitchen"],
.film[data-film-mode="play"][data-film-stop="bathroom"] .film__panel[data-stop="bathroom"],
.film[data-film-mode="play"][data-film-stop="carpentry"] .film__panel[data-stop="carpentry"],
.film[data-film-mode="play"][data-film-stop="basement"] .film__panel[data-stop="basement"],
.film[data-film-mode="play"][data-film-stop="close"] .film__panel[data-stop="close"] {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity 480ms cubic-bezier(0.22, 0.61, 0.36, 1) 140ms,
              transform 480ms cubic-bezier(0.22, 0.61, 0.36, 1) 140ms,
              visibility 0s linear 140ms;
}
@media (prefers-reduced-motion: reduce) {
  .film[data-film-mode="play"] .film__panel { transition: none; transform: none; }
}

/* ---- the transport ----
   Visible rather than hover-revealed, because the film starts itself and
   that is exactly the case the guidelines require a control for. */
.film__ui {
  position: absolute;
  left: clamp(1rem, 2.9cqw, 3rem);
  right: clamp(1rem, 2.9cqw, 3rem);
  bottom: clamp(1.15rem, 2.6cqh, 2.25rem);
  z-index: 6;
  display: flex;
  align-items: center;
  gap: var(--s-16);
}
.film[data-film-mode="still"] .film__ui { display: none; }

.film__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  flex: none;
  background: rgba(11, 10, 7, 0.6);
  border: 1px solid var(--line-on-dark);
  border-radius: var(--r-pill);
  color: var(--paper);
  cursor: pointer;
  transition: color 160ms var(--ease-ui), border-color 160ms var(--ease-ui),
              transform 160ms var(--ease-ui);
}
.film__btn:hover { color: var(--gold); border-color: var(--gold); }
.film__btn:active { transform: scale(0.94); }
.film__btn--step { width: 38px; height: 38px; }
.film__btn svg { display: block; fill: currentColor; stroke: none; }
.film__btn--step svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: square; }
/* One button, two glyphs, swapped by state rather than by two elements,
   so the accessible name and the focus ring stay on one control.
   ⛔ SCOPED TO .film__btn, AND THAT IS THE WHOLE FIX. Written bare, as
   `.film__ico--pause { display: none }`, it is specificity (0,1,0) and
   loses to `.film__btn svg { display: block }` at (0,1,1) three lines
   above — so the button rendered play AND pause side by side, which is
   how it first shipped. This file already carries the same lesson about
   `.prose a` outranking `.btn`. */
.film__btn .film__ico--pause { display: none; }
.film__btn[data-playing="true"] .film__ico--play { display: none; }
.film__btn[data-playing="true"] .film__ico--pause { display: block; }

/* ⛔ THE TAP TARGET IS THE BUTTON, NOT A HALO AROUND IT.
   This first shipped as a 10px dot with `::after { inset: -17px }` to grow
   the hit area to 44px — which works for one dot and breaks a ROW of them.
   At a 20px pitch each 44px box swallowed both its neighbours, so every dot
   except the last was unclickable: the one beside it intercepted the
   pointer. Playwright hit it while driving the slider for a screenshot; a
   person would have hit it by tapping the wrong stop.

   The fix is the ordinary one. The button IS the target — transparent,
   28x44 — and the visible dot is drawn inside it. The boxes tile instead of
   overlapping, the row looks identical, and 28x44 clears the 24x24 minimum
   with room to spare. */
.film__dots { display: inline-flex; align-items: center; gap: 0; }
.film__dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 44px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.film__dot::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: var(--r-pill);
  background: rgba(246, 243, 240, 0.34);
  transition: background-color 160ms var(--ease-ui), transform 160ms var(--ease-ui);
}
.film__dot:hover::before { background: var(--paper); transform: scale(1.2); }
.film__dot[aria-current="true"]::before { background: var(--sunset); transform: scale(1.3); }
/* .film__steps retired 2026-08-24: the steppers live in the panels. */

/* Progress. A hairline, not a scrollbar.
   ⛔ transform, never width: the rail updates on every animation frame of
   the playing film, and animating width forces a layout inside each one.
   scaleX is compositor-only. Same fix, same reason, as the scrub's rail. */
.film__bar-track {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  z-index: 7;
  background: rgba(246, 243, 240, 0.16);
}
.film[data-film-mode="still"] .film__bar-track { display: none; }
.film__bar {
  height: 100%;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left center;
  will-change: transform;
  background: var(--sunset);
}

/* ---- the phone ----
   The BAND, exactly as the scrub's phone layout was rebuilt on 2026-08-24
   after Daniel's real-device audit: the film sits at its own aspect under
   the masthead and the cards dock BELOW it, never over it. A full-bleed
   film with copy on top works on a 2374px canvas and drowns a 390px one. */
@media (max-width: 46rem) {
  .film__stage {
    aspect-ratio: auto;
    max-height: none;
    min-height: 0;
    overflow: visible;
    container-type: normal;
  }
  .film__video { position: static; aspect-ratio: 16 / 9; height: auto; }
  /* ⛔ NOT display:none ANY MORE. It was hidden for the band layout, which
     was right while nothing was over the film. The masthead sits on it now,
     and a 13px white menu glyph on a bright aerial measured 2.73:1. This is
     the header band ONLY, clamped to the film's own 16:9 box so it cannot
     wash the panels docked beneath it. */
  .film__scrim {
    display: block;
    inset: 0 0 auto;
    height: auto;
    aspect-ratio: 16 / 9;
    background: linear-gradient(to bottom, rgba(11, 10, 7, 0.8) 0%, rgba(11, 10, 7, 0.5) 14%, rgba(11, 10, 7, 0) 30%);
  }
  .film[data-film-mode="play"] .film__panels {
    position: static;
    /* ⛔ RESET `width`, NOT JUST `max-width`. The desktop rule sets
       `width: min(34cqw, 30rem)` on this container, and overriding only
       max-width left it carrying a ~160px width on a 390px phone: the
       panel rendered as a narrow column with two thirds of the screen
       black beside it. */
    width: auto;
    max-width: none;
    padding: var(--s-18) clamp(1rem, 4.4vw, 2rem) 0;
    /* ⛔ NOT A GRID STACK ANY MORE, AND THE VOID IS WHY. Stacking every
       panel into one grid area kept the section from changing height, but
       it also made the container as tall as the TALLEST stop: 409px on a
       412px phone, against roughly 250px of actual copy on the shorter
       ones. That surplus rendered as a screen of empty black under the
       text, and once the CTA moved below the panel it pushed the two
       controls that matter most down behind it.

       Instead the ACTIVE panel sits in flow and the inactive ones are
       absolutely positioned over it, so the container measures the stop you
       are actually reading. The cross-fade is untouched: both panels are
       still painted, still transitioned, still visibility-hidden when idle.
       The section does now change height between stops, which is the trade
       and is far less noticeable than the hole it replaces. */
    display: block;
    /* ⛔ RELATIVE, BUT RESET THE OFFSETS. The desktop rule parks this box with
       `inset: auto auto clamp(4.5rem,9cqh,7rem) clamp(1rem,2.9cqw,3rem)`, which
       a `position: static` container ignored and a `position: relative` one does
       NOT: the panel jumped 16px left and 83px up, straight over the video.
       Relative is still what is wanted, so the inactive panels position against
       this box rather than the section - the offsets just have to go with it. */
    position: relative;
    inset: auto;
  }
  .film[data-film-mode="play"] .film__panel {
    position: absolute;
    left: 0;
    top: 0;
    width: auto;
    padding: 4.4vw;
  }
  /* the one being read is the one that gives the container its height */
  .film[data-film-mode="play"][data-film-stop="deck"] .film__panel[data-stop="deck"],
  .film[data-film-mode="play"][data-film-stop="kitchen"] .film__panel[data-stop="kitchen"],
  .film[data-film-mode="play"][data-film-stop="bathroom"] .film__panel[data-stop="bathroom"],
  .film[data-film-mode="play"][data-film-stop="carpentry"] .film__panel[data-stop="carpentry"],
  .film[data-film-mode="play"][data-film-stop="basement"] .film__panel[data-stop="basement"],
  .film[data-film-mode="play"][data-film-stop="close"] .film__panel[data-stop="close"] {
    position: relative;
  }

  /* ⛔ NO EMPTY RESERVED BOX IN THE OPENING STATE. The panels are stacked
     in one grid area so the section does not change height as the stops
     change, which is right once the film is running and wrong before it
     starts: with every panel hidden, the container still reserved the
     tallest one and left a screen-deep black void between the hero card
     and the transport. Collapsed until there is a stop to show, so the
     one height change happens on the visitors own tap.
*/
  .film[data-film-mode="play"]:not([data-film-stop]) .film__panels { display: none; }
  .film__ui {
    position: static;
    /* Trimmed on the band: this row is the last thing in the film, and every
       pixel here decides whether the dots clear the fold on the opening slide. */
    padding: var(--s-8) clamp(1rem, 4.4vw, 2rem) var(--s-16);
    /* ⛔ IT MUST WRAP, and the reason is the tap targets. Six dots at the
       28px width that stops them overlapping is 168px, and with the play
       button and the two steppers the row wants ~360px — 24px of
       horizontal scroll on a 320px phone, which verify.mjs failed the whole
       page for. Wrapping keeps every control at full size and spends
       vertical space, which the band layout has. Shrinking the dots back
       would just reintroduce the overlap they were widened to fix. */
    flex-wrap: wrap;
    row-gap: var(--s-12);
  }
  .film__bar-track { position: static; }
}


/* The wordmark's opening moment on the film. It reuses .vhero__word and
   .vhero__strip whole — same type, same measured treatment.
   ⛔ PLACEMENT BELONGS TO .film__stack, NOT HERE. This block used to
   position the lockup absolutely across the whole stage and centre it,
   which was right while the wordmark was the only thing on the film. Once
   the film became the hero it shares that stage with the hero card, and an
   absolute overlay collided with it at ten of twelve window shapes — the
   precise failure .vhero__stack was built to prevent. All that is left
   here is the clip.

   ⛔ KEEP THE CLIP. The nested .vhero__lockup's radial ground is a ::before
   at `inset: … -8%`, so it reaches outside its box by design: harmless in
   the desktop stage, which is overflow:hidden, and 6px of horizontal
   scroll on a 390px phone, where the stage is overflow:visible for the
   band. The gradient is fully transparent at its edges, so clipping it is
   invisible.
   ⚠ It took an element sweep returning nothing to find that: a
   pseudo-element is not in querySelectorAll('*'), so the only symptom was
   the fixed masthead stretching to a 396px document. */
.film__lockup { /* no clip here — see .film below */ }

/* ⛔ CLIP AT THE SECTION, NOT AT THE LOCKUP. .film__lockup used to be an
   absolute full-stage overlay, so `overflow: hidden` on it was invisible.
   As a flow item in .film__stack its box is only as wide as the stack, so
   the same clip cut the wordmark's radial ground into a VISIBLE RECTANGLE
   across the picture. The section is full bleed, so clipping there is
   invisible and still stops the ::before at `inset: … -8%` producing 6px
   of horizontal scroll on a 390px phone.
   ⚠ overflow-x: clip, not hidden — hidden would make the section a scroll
   container and break the sticky masthead behaviour above it. */
.film { overflow-x: clip; }


/* ---- the film as the hero (2026-08-24, "move this up to the hero section")
   The opening composition is the wordmark centred and the card lower left,
   which is the same arrangement the scrub's hero had. Both belong to the
   film's opening state and both clear the moment the first stop arrives,
   so the panels never compete with the headline. */
.film__intro {
  position: absolute;
  inset: auto auto clamp(4.5rem, 9cqh, 7rem) clamp(1rem, 2.9cqw, 3rem);
  z-index: 6;
  width: min(34cqw, 32rem);
  opacity: 1;
  transition: opacity 420ms var(--ease), transform 420ms var(--ease);
}
.film[data-film-stop] .film__intro {
  opacity: 0;
  transform: translateY(-8px);
  /* ⛔ visibility, not opacity alone. A card faded to 0 still swallows
     clicks and still takes tab focus, so the hero CTAs would stay
     reachable behind a film they are no longer part of. This is the exact
     bug the scrub shipped twice; it is not being shipped a third time. */
  visibility: hidden;
  transition: opacity 420ms var(--ease), transform 420ms var(--ease),
              visibility 0s linear 420ms;
}
@media (prefers-reduced-motion: reduce) {
  .film__intro { transition: none; }
}
/* On the still path the film never starts, so the card is simply the
   hero: in flow, above the stops, at full width. */
.film[data-film-mode="still"] .film__intro {
  position: static;
  width: auto;
  padding: var(--s-24) clamp(1rem, 2.9vw, 3rem) 0;
}

/* The provenance line on the hero card. Small, but body-legible, and not
   styled as fine print — it is the only thing telling a visitor that the
   first picture on the site is not one of his. */
.film__intro .vhero__note {
  margin-top: var(--s-16);
  padding-top: var(--s-14);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
}

@media (max-width: 46rem) {
  /* The band: the card docks BELOW the film with the stop panels, never
     over it. Same rule, same reason, as the panels. */
  /* ⛔ FULL WIDTH, NOT INSET (Daniel, 2026-08-28: "the text should not be
     centered, it should be full width and left aligned like before").

     "Before" was an accident, and worth understanding. This rule used to say
     `padding: var(--s-20) ...`, and there is no --s-20 in the scale, so the
     whole declaration was invalid and dropped: the card ran edge to edge and
     looked right. Fixing the token switched the padding ON, which inset the
     card by 17px, and .vhero__card then adds 17.6px of its own plus a 1px
     border - so the H1 was laid out in 284px of a 390px screen and wrapped to
     three lines inside what read as a narrow centred box.

     The card carries its own padding, so the band does not need to add any.
     Vertical spacing stays; the horizontal inset goes. */
  .film[data-film-mode="play"] .film__intro,
  .film[data-film-mode="still"] .film__intro {
    position: static;
    width: auto;
    padding: var(--s-18) 0 0;
  }
  .film[data-film-stop] .film__intro { display: none; }

  /* ⛔ AND THE CTA TAKES ITS PLACE. The rule above is what removes the quote
     button and the phone number from every stop after the first. This puts
     them back, once, outside the panels so the rotating content cannot take
     them with it.

     Scoped three ways on purpose: the phone band only, the play path only
     (the still and no-JS paths keep the intro card, CTA and all, so a second
     copy there would be a duplicate), and only once a stop exists — before
     that the intro card is still on screen carrying its own. */
  .film[data-film-mode="play"][data-film-stop] .film__cta {
    display: block;
    padding: var(--s-18) clamp(1rem, 4.4vw, 2rem) 0;
  }

  /* ⛔ EXCEPT ON THE CLOSE, WHICH BRINGS ITS OWN. The closing panel is a
     destination rather than a stop: it carries the wordmark, a summary line,
     its own "Ask Max for a quote" and a Start again control. Adding the
     persistent block under it put the same orange button on screen twice,
     about 60px apart (Daniel, 2026-08-28). Same duplicate-control bug this
     file already fixed once for the play button, in a new place. */
  .film[data-film-mode="play"][data-film-stop="close"] .film__cta { display: none; }

  /* ⛔ THE PLAY CONTROL HAS TO BE ON SCREEN WITH THE FILM (Daniel,
     2026-08-28: "you do not have to scroll down to push play and then up
     again to watch the video").

     The panels are stacked in one grid area so the section does not change
     height as the stops change, which means the panel area always reserves
     the TALLEST stop: measured 409px on a 412px phone. With the transport
     sitting after it, the run came to video 232 + cta 54 + panels 409 +
     ui 86 = 781px, so on any phone with less than about 780px of usable
     height the play button and the dots sat below the fold. You scrolled
     down to start it, then back up to watch it.

     Reordering rather than shrinking, because that reserved height is a
     deliberate anti-jump decision and the panel text is the one thing here
     it is fine to scroll for. Video, CTA, then the controls comes to 372px
     and fits anything.

     ⛔ Scoped to [data-film-stop]. Before the first stop the opening card is
     still on screen carrying its own Start control, and reordering there
     would lift the dots above it for no reason. */
  /* ⛔ THE FLEX PARENT IS .film__stage, NOT .film. Everything on this
     section - the video, the opening card, the CTA, the panels and the
     transport - is a child of the stage; .film itself has exactly one
     element child. Written against .film the order values computed fine
     and moved nothing, because they were set on things that were not flex
     items of that box. Read the tree before ordering it. */
  .film[data-film-mode="play"][data-film-stop] .film__stage {
    display: flex;
    flex-direction: column;
  }
  /* ⛔ THE ORDER IS SLIDE ONE'S ORDER (Daniel, 2026-08-28: "the phone number
     and the cta need to be below the text, like it is on the first slide").
     The opening card reads rating, headline, lede, THEN the quote button and
     the number. A stop has to read the same way: the stop's copy, then the
     same two controls under it. Putting them above the panel made the film
     band open with a pair of buttons and buried the thing they refer to. */
  .film[data-film-mode="play"][data-film-stop] .film__video     { order: 1; }
  .film[data-film-mode="play"][data-film-stop] .film__stack     { order: 2; }
  .film[data-film-mode="play"][data-film-stop] .film__panels    { order: 3; }
  .film[data-film-mode="play"][data-film-stop] .film__cta       { order: 4; }
  .film[data-film-mode="play"][data-film-stop] .film__ui        { order: 5; }
  .film[data-film-mode="play"][data-film-stop] .film__bar-track { order: 6; }
}


/* The film's opening composition. ⛔ ONE FLOW, NOT TWO ANCHORS — the
   lockup at the top, the card at the bottom, and whatever is left between
   them. Laid out as a centred wordmark plus an absolutely-positioned card
   they overlapped at ten of twelve window shapes the moment the card grew
   to carry the H1, the lede, the CTAs and the provenance line. Siblings in
   a space-between column cannot collide at any size. Same pattern, same
   reasoning, as .vhero__stack. */
.film__stack {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(1rem, 4cqh, 3rem);
  padding: calc(var(--masthead-h) + clamp(0.75rem, 3cqh, 2.25rem))
           clamp(1rem, 2.9cqw, 3rem)
           clamp(4.5rem, 9cqh, 7rem);
  /* the film stays visible between the two blocks */
  pointer-events: none;
}
.film__stack > * { pointer-events: auto; }

/* Hidden everywhere by default. It is a phone-band affordance: on desktop the
   panels are an overlay on the film and the hero card is a few hundred pixels
   up the page, so nothing has gone missing and a second button pair would just
   crowd the picture. The media query below opts it in. */
.film__cta { display: none; }

/* ⛔ THE PHONE NUMBER RENDERS INK ON NIGHT WITHOUT THIS, AT 1.2:1.
   .btn--secondary is written for a light ground, and .vhero__card carries
   an override for exactly this reason with exactly this comment above it.
   This block is a Night surface too and sits OUTSIDE that card, so it needs
   its own copy. Shipped without it, the number was on screen and unreadable,
   which is worse than absent: it reads as a design choice.
   ⚠ gate.mjs did not catch it. Its contrast sweep reads the page at rest,
   and this block only exists once the tour has reached a stop. */
.film__cta .btn--secondary {
  color: var(--white);
  background: transparent;
  border-color: transparent;
  box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.5);
}
.film__cta .btn--secondary:hover {
  color: var(--gold);
  background: transparent;
  box-shadow: inset 0 -2px 0 var(--gold);
}
.film__lockup { text-align: center; }
/* ⚠ ONLY THE STOP PANEL WAS ASKED TO SHRINK. Narrowing the hero card too
   made it TALLER — the H1 wrapped to four lines and it went back to
   colliding with the wordmark. Same width as before, with a floor so a
   narrow window cannot squeeze it into a tall column. */
/* ⛔ WIDER, NOT TALLER (Daniel, 2026-08-24). The instinct with a box that
   takes too much screen is to shrink it, but a narrower card wraps the H1
   onto a third line and gets TALLER — and height is what covers the film,
   because the card is bottom-anchored and the picture is above it. Going
   wide puts the headline on two lines and buys back a whole line of
   height. Capped at 40rem so it can never become a banner. */
.film__intro { align-self: flex-start; width: min(max(41cqw, 24rem), 40rem, 94cqw); max-width: 100%; }

/* Both clear together once the film has something to say. */
.film[data-film-stop] .film__stack {
  opacity: 0;
  visibility: hidden;
  transition: opacity 260ms cubic-bezier(0.4, 0, 1, 1), visibility 0s linear 260ms;
}
.film__stack {
  opacity: 1;
  visibility: visible;
  transition: opacity 480ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
@media (prefers-reduced-motion: reduce) { .film__stack { transition: none; } }

/* Still path: no film ever starts, so this is simply the hero — in flow,
   full width, above the stops.

   ⛔⛔ THE MASTHEAD CLEARANCE MUST SURVIVE THIS RULE (2026-08-25). The play
   path pads the stack by `calc(var(--masthead-h) + …)` so the wordmark
   clears the fixed header. This rule used the `padding` SHORTHAND, which
   replaced that computed top value with a flat var(--s-24) — 24px of
   clearance under a 79px masthead. ELEVATED rendered *behind* the nav pill
   for anyone with "reduce motion" switched on, which is the whole still
   path: a real visitor setting, not an edge case. Daniel caught it in the
   browser; all five gates were green, because they assert the six stops are
   laid out as text and never that the hero is legible.
   ⚠ It is `padding-top`, not the shorthand, for exactly the reason above.
   Same family as the `padding` / `padding-inline` bug already on record. */
.film[data-film-mode="still"] .film__stack {
  position: static;
  padding: var(--s-24) clamp(1rem, 2.9vw, 3rem) 0;
  padding-top: calc(var(--masthead-h) + var(--s-24));
}
/* ⚠ EXCEPT ON THE PHONE BAND, WHERE IT WOULD BE A 78px HOLE. Under 46rem
   the video is a flow item and the stack sits BELOW it, nowhere near the
   masthead — measured: video 0..219, stack starts at 219. Clearing a header
   it is already clear of just pushes the wordmark down the page. */
@media (max-width: 46rem) {
  .film[data-film-mode="still"] .film__stack { padding-top: var(--s-18); }
}
.film[data-film-mode="still"] .film__intro { width: auto; }

@media (max-width: 46rem) {
  /* The band again: the card docks BELOW the film with the stop panels.
     ⛔ NO HORIZONTAL INSET HERE. The opening card is a .vhero__card and carries
     17.6px of its own padding plus a hairline border, so a gutter on this
     wrapper is a SECOND inset: the H1 was being laid out in 284px of a 390px
     screen and wrapped to three lines inside what read as a narrow centred
     box. Full width, left aligned, the card doing its own spacing. */
  .film__stack {
    position: static;
    padding: var(--s-12) 0 0;
    gap: var(--s-18);
  }
  .film__intro { width: auto; }
  /* ⛔ THE WORDMARK STAYS ON THE PHONE, AND IT SITS IN THE FLOW.
     It was display:none here, which is how it went missing from the thing
     Daniel liked. The first fix put it back as an absolute overlay ON the
     film band, and its white letters ran straight under the white menu
     button — the design gate measured the toggle at 1.00:1, white on white.
     A band layout does not need an overlay: the stack already sits BELOW
     the film, so the wordmark simply flows there, between the picture and
     the card. Nothing can collide with the masthead because nothing is
     under it. */
  .film__lockup { padding-top: var(--s-6); }
  .film__lockup .vhero__word { --word-size: min(15.2vw, 4rem); }
  .film__lockup .vhero__strip { font-size: max(0.6875rem, 2.4vw); letter-spacing: 0.24em; }
  .film[data-film-stop] .film__stack { display: none; }

  /* ⛔ AND DROP THE STACK'S BOTTOM PADDING ON THE BAND. A later global rule
     gives .film__stack `padding-bottom: clamp(3.75rem, 7cqh, 5.5rem)` to clear
     the transport where the stack is an overlay ON the film. Down here the
     stack is a flow item and the transport is a sibling below it, so that
     clearance is 60px of empty black between the opening card and the dots -
     which is what pushed the slider controls off the bottom of the screen.
     ⚠ Specificity, not order: the rule it overrides is a bare .film__stack
     LATER in the file, so a bare selector here would lose. */
  .film .film__stack { padding-bottom: 0; }
}

/* ⛔ ON A SHORT STAGE THE WORDMARK GOES, NOT THE HEADLINE. Even as
   siblings in a space-between column, a ~400px card and a large wordmark
   do not both fit in a stage under about 700px tall — qa-home still found
   them colliding at 1366x768, 1024x1366, 834x1112 and 768x1024. The
   decorative lockup is the part that can be spared: it is aria-hidden, it
   carries no keyword, and the brand is already in the masthead. The H1,
   the rating, the CTAs and the provenance line are not negotiable.
   ⚠ A CONTAINER query, not a media query — what matters is the height of
   the STAGE, which is driven by the 16:9 ratio and its clamps, not by the
   viewport. A 1024x1366 tablet has a tall window and a 576px stage. */
/* ⛔ ON A SHORT STAGE THE WORDMARK SHRINKS. IT DOES NOT DISAPPEAR.
   It was briefly hidden here, because a ~400px hero card and a full-size
   wordmark do not both fit in a stage under about 700px tall and qa-home
   found them colliding at four window shapes. Hiding it was the lazy fix:
   Daniel went looking for the big ELEVATED and it was gone. Sizing it
   against the stage's own HEIGHT keeps it on screen at every shape, and
   the type is still the largest thing on the page.
   ⚠ A CONTAINER query, not a media query — what matters is the height of
   the STAGE, driven by the 16:9 ratio and its clamps, not the viewport. A
   1024x1366 tablet has a tall window and a 576px stage. */
/* ⚠ 52rem CAUGHT THE COMMONEST DESKTOP THERE IS. A 1440x900 window gives
   an 810px stage — 50.6rem — so the shrink rule fired on the shape most
   visitors are on and the wordmark lost a third of its size for no reason
   (Daniel: "it used to look way better when it was bigger"). 44rem is the
   point where the stage genuinely cannot hold both. */
/* ⚠ THE THRESHOLD IS A MEASUREMENT, AND IT MOVED TWICE. 52rem caught a
   1440x900 window (810px stage = 50.6rem), which is the commonest desktop
   there is, and shrank the wordmark for no reason. 44rem missed 1366x768
   (706px = 44.1rem) and let it collide. 46rem is the line: 1440 keeps the
   big type, and 1366, 1280, 1024x1366 and 768x1024 all shrink. */
@container (max-height: 46rem) {
  .film__lockup .vhero__word { --word-size: min(9cqw, 13cqh); }
  .film__lockup .vhero__strip { font-size: var(--fs-12); letter-spacing: 0.22em; }
  .film__stack { gap: clamp(0.5rem, 2cqh, 1.5rem); }
}
@container (max-height: 36rem) {
  .film__lockup .vhero__word { --word-size: min(8cqw, 11cqh); }
}

/* ---- the steppers, in the info box ----
   ⛔ THEY MOVED OUT OF THE TRANSPORT BAR (Daniel, 2026-08-24). On a 1440
   window the arrows sat on the far right, about 900px from the panel they
   act on, which is a long way to travel to read the next thing. Beside the
   route counter they are where the eye already is.
   ⚠ Repeated in EVERY panel, not shared: an inactive panel is
   visibility:hidden, so a single shared control inside one would disappear
   with it. film.js binds them by delegation for the same reason. */
.film__nav {
  display: inline-flex;
  align-items: center;
  gap: var(--s-8);
}
.film__nav .tour__panel-count { margin-right: var(--s-6); }
.film__step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  background: none;
  border: 1px solid var(--line-on-dark);
  border-radius: var(--r-pill);
  color: var(--on-dark-soft);
  cursor: pointer;
  transition: color 160ms var(--ease-ui), border-color 160ms var(--ease-ui),
              transform 160ms var(--ease-ui), opacity 160ms var(--ease-ui);
}
.film__step svg { display: block; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: square; }
.film__step:hover { color: var(--gold); border-color: var(--gold); }
.film__step:active { transform: scale(0.94); }
/* The close has nowhere to go next. Shown rather than removed so the row
   does not change width on the last stop. */
.film__step[disabled] { opacity: 0.3; cursor: default; }
.film__step[disabled]:hover { color: var(--on-dark-soft); border-color: var(--line-on-dark); }

/* The bar keeps play and the dots; the arrows are gone from it. */
/* ⚠ THE BAR KEEPS PLAY AND THE DOTS TOGETHER. With the arrows moved into
   the panels, margin-left:auto pushed the dots to the far right and split
   one transport across both ends of a 1440px screen. They are one control:
   where you are, and whether it is running. */
.film__dots { margin-left: 0; }

/* ---- the opening card sits lower and lighter (Daniel, 2026-08-24) ----
   It was drowning the film: a tall glass panel starting a third of the way
   down a 900px window covered the best part of the opening shot. The card
   cannot lose its content — the H1, the rating, the CTAs and the
   provenance line all have to be there — so it loses HEIGHT instead:
   tighter padding, a tighter lede, a smaller provenance line, and it sits
   closer to the transport bar. Net effect is the top edge dropping about
   a fifth of the frame and that much more film showing above it. */
.film__intro .vhero__card { padding: clamp(1rem, 1.5cqw, 1.5rem) clamp(1.1rem, 1.7cqw, 1.75rem); }
.film__intro .vhero__rating { margin-bottom: var(--s-8); }
/* ⛔ NOT `balance` HERE, AND IT IS WHY WIDENING THE CARD DID NOTHING.
   .vhero__title carries `text-wrap: balance`, which evens the ragged edge
   by spreading the words over MORE lines — so the card got 76px wider and
   the headline stayed at three lines, which is the opposite of the point.
   `pretty` keeps the orphan control and lets the lines fill. */
/* ⚠ CAPPED SO THE HEADLINE LANDS ON TWO LINES. At 1.65rem the 62-character
   H1 wrapped to three even in a 590px card, and a three-line hero headline
   is a font-size error, not a copy-length one — the copy is fixed by the
   documented H1 exception at the top of home.mjs and may not be shortened. */
.film__intro .vhero__title {
  font-size: clamp(1.1rem, 1.6cqw, 1.45rem);
  margin-bottom: var(--s-8);
  text-wrap: pretty;
}
.film__intro .vhero__lede { font-size: clamp(0.9375rem, 1.02cqw, 1rem); margin-bottom: var(--s-14); }
/* ⚠ A MEASURE, NOT JUST A SIZE. Shrinking the note to buy height made its
   LINES longer — 90 characters, past the 80 the prose gate allows. Small
   type needs a shorter line, not a longer one. */
.film__intro .vhero__note {
  margin-top: var(--s-12);
  padding-top: var(--s-10);
  max-width: 46ch;
  font-size: max(0.6875rem, 0.72cqw);
  line-height: 1.45;
}

/* ⚠ 768x1024 is the tightest shape there is: a 432px 16:9 stage floored up
   to 736px, carrying a full hero card AND the lockup. The strip under the
   wordmark is the first thing to touch the card, so it gives up its
   breathing room there rather than the type giving up its size. */
@container (max-height: 42rem) {
  .film__lockup .vhero__strip { margin-top: var(--s-4); }
  .film__lockup .vhero__word { --word-size: min(8.5cqw, 11cqh); }
}
.film__stack { padding-bottom: clamp(3.75rem, 7cqh, 5.5rem); }

/* ⛔ IT IS A HEADER ROW NOW, NOT A FOOTER ONE (Daniel, 2026-08-28).
   It sat at the BOTTOM of the opening card, under the lede, the CTA, the
   phone number and the provenance note, so the one control that starts the
   film was the last thing on the card and fell off the bottom of a small
   screen. At the top it is reachable at once, and the arrows land in the
   same place as .tour__panel-head puts them one slide later, so the two
   rows read as one system. The hairline moved with it, above to below.

   ⚠ 12, not 14. The row used to spend margin-top 12 + padding-top 10 above
   itself; 14 + 10 below made the card 2px taller, which was enough to fail
   qa-home's 'card over strip' at 834x1112, where the desktop composition
   anchors the wordmark and the card at opposite ends of the film and has
   almost no slack. This is the same total spacing as before the move. */
.film__nav--intro {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  margin-bottom: var(--s-12);
  padding-bottom: var(--s-10);
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  gap: var(--s-8);
}
.film__nav--intro .tour__panel-count {
  margin-right: auto;
  font-size: var(--fs-12);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* ---- the deck sequence is not a hero ----
   It inherits .film__stage, whose min-height floor exists so the HERO can
   fit a full card and the wordmark on a portrait tablet. This film has
   neither: it is a picture with a caption. Left at the hero floor it made
   a 46rem-tall section out of a 16:9 clip and pushed its own panel below
   the fold on a 900px window. */
.film--deck .film__stage {
  min-height: 0;
  max-height: 78svh;
}
/* No brand moment here — the wordmark belongs to the hero. */
.film--deck .film__lockup { display: none; }

/* The intro play control: a text button, not a chip. It sits where the
   route counter sits in every other panel, so the row reads the same.  */
.film__start {
  display: inline-flex;
  align-items: center;
  gap: var(--s-8);
  margin-right: auto;
  padding: 0;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
  cursor: pointer;
  transition: color 160ms var(--ease-ui);
}
.film__start:hover { color: var(--white); }
.film__start svg { display: block; fill: currentColor; stroke: none; }
.film__start .film__ico--pause,
.film__start .film__start-off { display: none; }
.film__start[data-playing="true"] .film__start-on { display: none; }
.film__start[data-playing="true"] .film__start-off { display: inline; }
.film__start[data-playing="true"] .film__ico--play { display: none; }
.film__start[data-playing="true"] .film__ico--pause { display: block; }

/* ⛔ THE PARAGRAPH NEEDS THE DARK COLOUR TOO, NOT JUST THE ANCHOR. The
   link inside was correctly white, but .stick__copy leaves its children
   at Ink and verify.mjs measures the element that OWNS the text node —
   which reported 1.05:1 on a link that looked fine. Colour the container,
   not only the thing you can see. */
.stick__link { color: var(--on-dark-soft); margin: 0; }

/* ⚠ The strip is the last thing to touch the card on the two tightest
   shapes (1024x1366 and 768x1024, both a 736px stage). It gives up its
   leading before the wordmark gives up any more size. */
@container (max-height: 46rem) {
  .film__lockup .vhero__strip { margin-top: var(--s-4); font-size: var(--fs-12); }
  .film__stack { gap: clamp(0.4rem, 1.5cqh, 1.1rem); }
}

/* ============================================================
   UI/UX AUDIT FIXES (2026-08-25, ui-ux-pro-max CRITICAL + HIGH)
   Audited on 390 / 768 / 1440 against touch targets, touch spacing,
   cursor, icon-button names, mobile type size, line length, reduced
   motion and overflow. What follows is only what actually failed.
   ============================================================ */

/* ⛔ TOUCH TARGETS. Three controls were under 44px and all three are
   primary: the transport steppers (34x34), the "Start the tour" text
   button (147x14, a text run with no box at all) and the masthead CTA
   (132x29). A 14px-tall tap target on a phone is a miss, not a control. */
.film__step { width: 44px; height: 44px; }
.film__start { min-height: 44px; padding-block: var(--s-8); }
/* ⛔ NEVER SET `display` HERE. The masthead CTA is `display: none` under
   62rem, and setting `display: inline-flex` to make it a flex box for
   vertical centring resurrected it on every phone — it reappeared beside
   the menu button and pushed the bar past the viewport on all eleven
   pages. Padding gets the 44px height without touching the display mode
   that the responsive rule owns. */
.masthead__cta { padding-block: var(--s-10); }

/* ⚠ THE DOTS STAY 28x44 AND THAT IS A CONSIDERED TRADE, NOT AN OVERSIGHT.
   Six 44px-wide targets in a row would overlap each other at this pitch,
   which is the exact bug they were widened to fix in the first place.
   28x44 clears the WCAG 2.5.8 minimum (24x24) with room, and the boxes
   tile instead of colliding. Widening them reintroduces the worse fault. */

/* ⛔ THE PROVENANCE LINE WAS 11px ON A PHONE. It is the sentence that stops
   a visitor reading a generated house as one of Max's jobs, which makes it
   the most load-bearing small copy on the site, and it was set smaller than
   anything else on the page. The brand book already says body text never
   goes below 16px here; this was the exception quietly breaking it. */
@media (max-width: 46rem) {
  .film__intro .vhero__note { font-size: max(0.875rem, 3vw); }
  .vhero__rating { font-size: max(0.8125rem, 2.9vw); }
  .stages li { font-size: max(0.875rem, 2.9vw); }
}

/* ⚠ Nav links sat 5px apart, below the 8px minimum for adjacent targets.
   It matters on a tablet reaching for the pill, not only on a phone. */
.nav__pill { gap: var(--s-8); }

/* ⚠ TABLETS ARE TOUCH DEVICES TOO. The mobile type rules above were scoped
   to 46rem, so a 768px tablet kept the 11px provenance line and the 12px
   stage list while being tapped with the same thumb. Scoped to 62rem.
   The steppers shrank to 42px in the flex row for the same reason a flex
   child always does: nobody told it not to. */
.film__step { flex: none; }
@media (max-width: 62rem) {
  .film__intro .vhero__note { font-size: max(0.875rem, 1.6vw); }
  .stages li { font-size: max(0.875rem, 1.5vw); }
}

/* ⚠ THE AUDIT FIXES COST HEIGHT, AND THE TIGHT SHAPES PAID FOR IT. Raising
   the steppers to 44px and the provenance line to 14px made the hero card
   taller, and 834x1112 and 768x1024 went back to colliding with the
   wordmark. Accessibility wins over the brand flourish: the lockup gives
   up more size on those two shapes rather than the tap targets or the
   honesty line giving up anything. */
@container (max-height: 46rem) {
  .film__lockup .vhero__word { --word-size: min(7.5cqw, 10cqh); }
  .film__lockup .vhero__strip { font-size: max(0.625rem, 1.2cqw); letter-spacing: 0.2em; }
  .film__intro .vhero__note { margin-top: var(--s-8); padding-top: var(--s-8); }
}

/* ---- the answer band (2026-08-25) ----
   The answer-first lede used to be a single paragraph alone in a Paper
   section, which reads as a hole in the page. It is paired with the proof
   line now: the claim on the left, the three measured facts beside it.
   ⛔ NOT a card, and no box. The brand book has no third surface and this
   does not need one — a hairline between the two columns is the whole
   device. */
.answer {
  display: grid;
  gap: var(--s-36);
  align-items: start;
}
@media (min-width: 62.01rem) {
  .answer {
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    gap: clamp(2rem, 5vw, 5rem);
  }
  .answer__proof { border-left: 1px solid var(--line); padding-left: clamp(1.5rem, 3vw, 3rem); }
}
.answer__proof { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-24); }
.answer__proof li { display: grid; gap: var(--s-4); }
.answer__proof strong {
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(1.5rem, 2.6vw, 2.1rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink);
}
.answer__proof span { font-size: var(--fs-15); line-height: 1.5; color: var(--cedar); }
@media (max-width: 62rem) {
  .answer__proof { border-top: 1px solid var(--line); padding-top: var(--s-24); }
}

/* The brand returning at the close. Same lockup idea as the opening state,
   sized to a card rather than to the screen: the tour began with the name
   and ends with it, and the restart control sits under it. */
.film__close-brand {
  margin: 0 0 var(--s-14);
  font-family: var(--display);
  font-weight: 900;
  font-size: clamp(1.4rem, 2.4cqw, 2rem);
  line-height: 0.95;
  letter-spacing: -0.014em;
  color: var(--white);
}
.film__close-brand span {
  display: block;
  margin-top: var(--s-6);
  font-family: var(--sans);
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--gold);
}
.film__restart {
  display: inline-flex;
  align-items: center;
  gap: var(--s-8);
  min-height: 44px;
  padding: 0;
  background: none;
  border: 0;
  font-family: inherit;
  font-size: var(--fs-12);
  font-weight: var(--w-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--on-dark-soft);
  cursor: pointer;
  transition: color 160ms var(--ease-ui);
}
.film__restart:hover { color: var(--gold); }
.film__restart svg { display: block; }

/* ⛔ THE WORDMARK COMES BACK AT THE CLOSE. The stack is hidden for every
   stop, which was right while the stops were mid-tour and wrong at the end:
   the film finished with the brand nowhere on screen. The close is the one
   stop that gets it back. */
.film[data-film-stop="close"] .film__stack { opacity: 1; visibility: visible; }
.film[data-film-stop="close"] .film__intro { display: none; }


/* ============================================================
   AUDIT ROUND 2 (2026-08-25) — read back off the LIVE build at
   1440 / 768 / 390, not off the source.
   ============================================================ */

/* ⛔ TWO PLAY BUTTONS WERE ON SCREEN AT ONCE. The opening card's
   "Start the tour" and the rail's icon button sat 74px apart, both
   visible, both firing data-film-play. Two controls for one action is
   worse than either alone: it makes the visitor choose before they know
   there is nothing to choose. The rail button is the persistent one, so
   it waits until the intro card has gone. */
.film:not([data-film-stop]) .film__ui .film__btn { display: none; }

/* ⚠ The dots must not jump left when that button appears. The rail is
   flex; reserving the slot keeps the row still across the transition. */
.film:not([data-film-stop]) .film__ui { padding-left: 0; }

/* ⛔ THE PROOF LIST WAS SET AS ONE BLOCK OF TEXT. Every label sat hard
   against the underside of its number — "from 9 Google reviews" collided
   with the descender space of "5.0" — so the pairs read as one run
   rather than as three facts. They are the only numbers on the page that
   are not in the film, and they were the least legible thing in the band. */
.answer__proof strong { line-height: 1; display: block; }
.answer__proof span { display: block; margin-top: var(--s-8); }
.answer__proof li + li { margin-top: var(--s-24); }

/* ⚠ "One crew" is not a numeral and must not be set like one. At the
   figure size it ran half again as wide as "5.0" and pulled the whole
   stack off axis. It steps down to the size of a lead, which is what it
   actually is — a short phrase, not a measurement. */
.answer__proof li:last-child strong { font-size: var(--fs-lead, 1.125rem); letter-spacing: 0; }

/* ⚠ The still has to LOOK pressable, or making it pressable is a secret.
   Scoped to the tap path so the cursor never suggests a pause that a tap
   on the running film deliberately will not perform. */
.film[data-film-tap="true"] .film__stage { cursor: pointer; }


/* ============================================================
   ROUND 3 (2026-08-25) — six faults Daniel read off the live page.
   ============================================================ */

/* ⛔⛔ TWO INFO BOXES FOR A SPLIT SECOND ON EVERY LOAD (Daniel, #2).
   .film__panels is `position: relative` by default — that is the still and
   no-JS layout, where the six stops SHOULD read as a plain column. It only
   becomes an overlay under [data-film-mode="play"], which film.js sets when
   it runs. Between CSS parse and that line, all six panels lay out in flow
   underneath the hero card. That window is the flash, and on a slow first
   paint it is long enough to look like a broken page.

   Hidden until the film has decided what it is. film.js sets the mode
   unconditionally on BOTH paths (play and still) at the top of setup, so
   this can only persist if the script never runs at all — and that case is
   `html.no-js`, which is excluded here and keeps the full readable column. */
html:not(.no-js) .film:not([data-film-mode]) .film__panels { display: none; }

/* ⛔ THE HAIRLINE SAT HARD AGAINST THE NUMBERS (Daniel, #3). The rule that
   draws it also sets `padding-left`, and the very next rule sets
   `padding: 0` — the shorthand cancelled it, so the divider touched "5.0".
   THIRD instance of this exact trap in this file: the masthead clearance and
   the container inline padding were the other two. The gap is restored as a
   longhand AFTER the shorthand, which is the only order that survives. */
@media (min-width: 62.01rem) {
  .answer__proof { padding-left: clamp(1.75rem, 3.4vw, 3.25rem); }
}

/* ⚠ SECTION RHYTHM (Daniel, #4 and #6, then the 08-25 reorder). The deck
   lead was carrying hand-typed 18px/24px overrides, so a full-bleed colour
   change landed with less breathing room than any other section and ran
   straight into the film.

   THE RULE THIS SETTLED ON: a COLOUR CHANGE needs the full section padding.
   A seam INSIDE one ground needs about half of it, or the page grows a dead
   band. Those are different jobs and they are no longer the same number.

   After the reorder the deck lead follows the service grid, so it is now an
   inside-Night seam, not a colour change — and stacking two full paddings
   there measured 179px of empty black between the cards and the heading.
   The pair splits one gap between them: ~84px, against ~108px at a real
   colour change, so the seams stay ranked correctly by weight. */
.section--range { padding-bottom: var(--s-42); }
.section--lead-in { padding-top: var(--s-42); padding-bottom: var(--s-54); }
@media (max-width: 46rem) {
  .section--range { padding-bottom: var(--s-30); }
  .section--lead-in { padding-top: var(--s-30); padding-bottom: var(--s-36); }
}

/* ⛔ THE FOUR STAGE SENTENCES, ON THE PATH THAT CANNOT ANIMATE. In poster
   mode the track is one screen and the caption can never change, so each
   stage carries its own sentence and the section still teaches the order of
   work. Injected by site.js only on that path — the element does not exist
   anywhere else, so this cannot leak into the scrubbed layout. */
.stages__cap {
  display: block;
  margin-top: var(--s-8);
  font-size: var(--fs-14, 0.875rem);
  line-height: 1.45;
  letter-spacing: 0;
  text-transform: none;
  color: rgba(255, 255, 255, 0.78);
  max-width: 24rem;
}
.track[data-mode="poster"] .stages { align-items: start; }
.track[data-mode="poster"] .stages li { padding-right: var(--s-18); }
