/* Theme layer — the look, and only the look.
 *
 * site.css is the structural contract: semantic class names, a token block, mobile-first
 * breakpoints. Its header says a restyle should touch the tokens and a theme layer and
 * not those rules. This is that theme layer. It redefines the tokens site.css already
 * declares, then gives each semantic class a surface. It adds no new class the templates
 * do not already emit, and it names no company — bin/check-config-leak scans this file.
 *
 * THE LOOK. Pure black ground, thin uppercase Montserrat headings over Michroma body, every
 * corner square, full-bleed photographic bands, and flat near-black panels sitting on the
 * black with a one-pixel hairline. The only saturated element on the page is the logo
 * mark. The single departure from that reference is the header, which is frosted glass
 * rather than solid black — that was asked for explicitly.
 *
 * FOUR CONSTRAINTS THIS FILE IS WRITTEN AGAINST, each of which fails a check if ignored:
 *   1. The CSP is `style-src 'self'` with no 'unsafe-inline'. No <style> block, no style=
 *      attribute, and no third-party font host can ship. Hence @font-face over self-hosted
 *      woff2 in /assets/fonts, and hence every value below living in this file.
 *   2. tests/specs/layout.spec.mjs requires a computed `outline-style` other than `none`
 *      on every focused interactive element. So focus is an outline here, never a
 *      box-shadow ring standing in for one.
 *   3. The same suite asserts the mobile nav is a native <details> closed by default with
 *      a .site-nav__summary toggle. This restyles that disclosure; it does not replace it.
 *   4. The overflow sweep runs at 320px across twenty pages. Display type is clamped from
 *      a small floor, never sized from a viewport unit alone.
 */

/* ── Faces ───────────────────────────────────────────────────────────────────
   Self-hosted, latin subset, from the Google Fonts release. Third-party font hosting
   leaks a visitor's address to the host on every page view, and the CSP blocks it
   anyway. Both ship as one variable file covering 300–700; declaring the range rather
   than static faces per weight is several fewer requests. */
@font-face {
  font-family: 'Montserrat';
  src: url('/assets/fonts/montserrat-var.woff2') format('woff2-variations');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}
/* Michroma is the body face for the whole site, by instruction. It is a wide square
   display design with ONE weight and no italic, which has three consequences the rules
   below have to absorb: every line of text is roughly a third wider than the same line in
   a text face, so the type scale and the tracking are pulled down to compensate; `<strong>`
   cannot get a real bold, so emphasis is carried by colour instead; and nothing here may
   assume a weight axis exists. */
@font-face {
  font-family: 'Michroma';
  src: url('/assets/fonts/michroma-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
/* The header's two faces. Only the ExtraBold 800 cut of Saira Extra Condensed is fetched,
   not the family — the wordmark is the one string that uses it, and the other eight
   weights would be eight files nothing asks for. Aldrich ships a single weight and has no
   bold, so nothing below asks it for one; a synthesised bold on a square display face is
   a smear. */
@font-face {
  font-family: 'Saira Extra Condensed';
  src: url('/assets/fonts/saira-extra-condensed-800.woff2') format('woff2');
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Aldrich';
  src: url('/assets/fonts/aldrich-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ── Tokens ──────────────────────────────────────────────────────────────────
   Redefinitions of names site.css already declares, plus what this look adds.
   --radius: 0 squares every corner in the structural sheet without touching it.

   `:root:root`, NOT `:root`. site.css declares its dark palette on
   `:root:not([data-theme="light"])` inside a prefers-color-scheme query and on
   `:root[data-theme="dark"]`. Both compute to specificity (0,2,0); a plain `:root` is
   (0,1,0) and loses to them no matter how much later it loads. The symptom is that this
   design renders correctly for a visitor in light mode and half-inverted for one in dark
   mode — teal links, grey body copy, a #0b0b0d ground — which is most visitors, and which
   neither Playwright nor Lighthouse catches because both default to light. Doubling the
   pseudo-class matches (0,2,0) and source order does the rest. Every token override
   further down this file needs the same doubling for the same reason. */
:root:root {
  --black: #000000;
  --panel: #111111;
  --panel-raised: #171717;
  --white: #ffffff;

  --bg: var(--black);
  --bg-sunken: var(--panel);
  --bg-elevated: var(--panel-raised);
  --fg: var(--white);
  /* The reference sets body copy at full white. Over this much black that is fatiguing
     across a 3,000-word guide, so body text steps back one notch and headings, links and
     emphasis keep the full white. Both sit far above 4.5:1. */
  /* 0.56, not 0.72, and the reason is the no-underline rule above. axe's
     `link-in-text-block` requires an inline link to differ from the text around it by at
     least 3:1 when nothing but colour tells them apart. White on 72% white is 1.98:1 and
     failed on four page types. At 56% the body reads #949494: 3.2:1 against a white link
     and still 6.6:1 against the black ground, so both the link rule and the text rule
     pass. Raise this back toward 0.72 only if underlines come back. */
  --fg-muted: rgba(255, 255, 255, 0.56);
  --fg-faint: rgba(255, 255, 255, 0.46);
  --accent: var(--white);
  --accent-contrast: var(--black);
  --border: rgba(255, 255, 255, 0.12);
  --border-strong: rgba(255, 255, 255, 0.28);
  --radius: 0;

  --font-body: 'Michroma', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-display: 'Montserrat', ui-sans-serif, system-ui, -apple-system, sans-serif;
  /* The header only. Both are narrow display faces, so the fallbacks are narrow too —
     a fallback at the same size in a normal-width face would overflow the bar. */
  --font-wordmark: 'Saira Extra Condensed', 'Arial Narrow', Impact, sans-serif;
  --font-nav: 'Aldrich', 'Arial Narrow', Impact, sans-serif;

  /* Michroma sets about a third wider than a text face at the same size, and the 320px
     overflow sweep is not negotiable. site.css's scale is pulled down here rather than
     there, so the structural sheet stays face-agnostic for the next client. */
  --text-sm: 0.75rem;
  --text-base: clamp(0.8rem, 0.76rem + 0.16vw, 0.9rem);
  --text-lg: clamp(0.9rem, 0.84rem + 0.28vw, 1.1rem);

  /* Tracking. The reference tracks body copy at 1px/15px, which is what these values were
     before Michroma: that face carries its own generous sidebearings, so adding letter
     spacing on top is what pushes a paragraph off a 320px screen. Body copy is now zero
     and only the small uppercase labels stay open. */
  --track-body: 0;
  --track-read: 0;
  --track-display: 0.02em;
  --track-label: 0.1em;

  /* The header is the one frosted surface. One recipe, defined here, used once. */
  --frost-blur: 18px;
  --frost-tint: rgba(0, 0, 0, 0.58);
  --frost-sheen: linear-gradient(180deg,
      rgba(255, 255, 255, 0.07) 0%,
      rgba(255, 255, 255, 0.02) 55%,
      rgba(255, 255, 255, 0.04) 100%);
  --frost-border: rgba(255, 255, 255, 0.14);
  --frost-inset: inset 0 1px 0 rgba(255, 255, 255, 0.16);
  --frost-drop: 0 12px 40px rgba(0, 0, 0, 0.55);

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --dur: 400ms;
  --dur-slow: 900ms;

  --space-6: 3.5rem;
  --space-7: 5.5rem;

  /* The reference lays out on a 1400px container; site.css defaults to 1248. The wider
     figure is what lets a wordmark, five nav groups and a call to action share one row. */
  --container: 87.5rem;
  /* What the page's content actually sits in. Narrower than the bar above it on purpose:
     992px is a reading column, 1400px is a header. One value, so every section on every
     page lands on the same centred column. */
  --content: 62rem;

  /* The pinned block's real height: the bar plus the ticker strip under it, measured
     rather than guessed, because scroll-padding-top and the hero's height are both
     calculated from it and an anchor that lands under the header is the cost of being
     wrong. Re-measure it when either band changes. */
  --header-h: 8rem;
  /* The wordmark and the menu links, declared as a pair. They are the same face now, so
     the gap between them is what decides how far the links have to drop to sit on the
     wordmark's baseline — see --baseline-drop and .site-nav__list > li > a. */
  --brand-fs: clamp(2.96rem, 2.5rem + 1.72vw, 4.37rem);
  --nav-fs: 2.53rem;
  /* The disc's diameter, as a token: the lockup's layout is written in terms of it. */
  --mark-size: 4.95rem;
  /* The poured-pigment sweep, declared once. Two elements wear it — the buttons under the
     pointer and the hero badge always — and two copies of an eight-stop gradient is two
     places for it to drift. */
  --swirl: conic-gradient(from 0deg,
      #1b2370 0%, #2f8fd6 14%, #4cc4f0 24%, #cf2a55 40%,
      #e8582f 54%, #f5b53f 66%, #7a2a6a 82%, #1b2370 100%);
  /* The bar's own breathing room, one value so the ticker below it can cancel the
     bottom half exactly and sit flush with the header's edge. */
  --header-pad: 0.21rem;
  --veil: 0.55;
}

/* The design is black in both schemes: site.css flips its palette under
   prefers-color-scheme and there is nothing here for that flip to do. The block above now
   wins in both branches on its own, so no per-scheme restatement is needed — and none
   should be added back, because a second copy of the palette is a second place for it to
   drift. `tests/specs/layout.spec.mjs` has no colour assertion; the check that catches a
   regression here is reading the page with the browser in dark mode. */

body {
  background: var(--black);
  color: var(--fg);
  font-weight: 400;
  letter-spacing: var(--track-body);
}

/* ── Type ────────────────────────────────────────────────────────────────────
   Montserrat, uppercase, thin, tracked open, with the line box set to the cap height so
   a two-line heading closes up the way the reference's does. Sizes are clamped from a
   floor that fits 320px, because the overflow sweep starts there. */
h1, h2, h3 {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: var(--track-display);
  color: var(--fg);
}
h1 {
  font-weight: 300;
  font-size: clamp(2.1rem, 1.1rem + 4.4vw, 6.25rem);
  line-height: 1.02;
}
h2 {
  font-weight: 400;
  font-size: clamp(1.45rem, 1.1rem + 1.5vw, 2.375rem);
  line-height: 1.1;
}
h3 {
  font-weight: 400;
  font-size: clamp(1rem, 0.94rem + 0.35vw, 1.25rem);
  letter-spacing: 0.06em;
  line-height: 1.3;
}

p, li { color: var(--fg-muted); }
/* Michroma has no bold. `font-weight: 600` would be a synthesised smear on a square
   display face, so emphasis steps up in brightness instead — full white against body
   copy at 72%. */
strong { color: var(--fg); font-weight: 400; }

/* NO UNDERLINES ANYWHERE, by instruction — including the hover and visited states, and
   including the footer, where site.css turns one on for hover.
   Know the trade-off before restoring one: with the rule gone, a link differs from the
   body copy around it only in brightness (100% white against 72%). That is legible, but
   it is a single visual channel, which is what WCAG 1.4.1 asks you not to rely on. The
   focus outline below is untouched and is not an underline — leave it alone, the layout
   suite asserts it. */
a,
a:hover,
a:visited,
a:focus,
.site-footer__group a:hover,
.site-footer__legal a:hover,
.prose a { text-decoration: none; }

a { color: var(--accent); }
a:hover { color: var(--fg-muted); }

/* Constraint 2: a real outline, not a shadow pretending to be one. */
:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: 3px;
}

::selection { background: var(--white); color: var(--black); }

/* The bar never moves, so an anchor must not land underneath it. */
html { scroll-padding-top: calc(var(--header-h) + var(--space-2)); }

/* ── Header ──────────────────────────────────────────────────────────────────
   The reference's bar is solid black. This one is the same geometry in frosted glass, so
   the photographic bands read through it as the page scrolls under.

   `position: sticky` already pinned it during ordinary scrolling. What it did NOT survive
   is the elastic overscroll at the top of the document: flick upward past the start and
   the whole document — sticky header included — stretches down and springs back. Sticky
   positions against the scroll container, and during a rubber-band the container itself
   is what moves, so there is nothing for `top: 0` to hold onto. `overscroll-behavior-y`
   on the root turns that bounce off, which is the actual fix; `position: fixed` would
   also hold, but it takes the bar out of flow and then the page needs a spacer exactly as
   tall as a header whose height changes with the wordmark's line count. This keeps the
   header in flow and simply stops the page from stretching. */
html, body { overscroll-behavior-y: none; }

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  padding-block: var(--header-pad);
  border: 0;
  border-bottom: 1px solid var(--frost-border);
  background: var(--frost-sheen), var(--frost-tint);
  -webkit-backdrop-filter: blur(var(--frost-blur)) saturate(150%);
  backdrop-filter: blur(var(--frost-blur)) saturate(150%);
  box-shadow: var(--frost-inset);
  view-transition-name: site-header;
}

/* The mark is a circular badge with a transparent ground, so it needs no plate behind it
   and none is drawn. It is the only colour on the page and carries the brand alone. */
.brand { gap: 0.7rem; }
.brand__mark {
  /* Twice the diameter it was (deviation 60), then 0.9x of that (deviation 73). It is
     the tallest thing in the bar and therefore what sets the bar's height. */
  width: var(--mark-size);
  height: var(--mark-size);
  border-radius: 50%;
  object-fit: contain;
}
.brand__name {
  font-family: var(--font-wordmark);
  /* 800 is the only weight fetched, and it is the weight asked for. Anything else here
     would be synthesised, because there is no other cut to pick from. */
  font-weight: 800;
  /* 1.3x the previous size, by instruction. Saira Extra Condensed is a narrow face, so it
     takes the extra size without the bar growing — the opposite of the problem the face
     before it created. The tracking opens back up because a condensed face at display
     size sets too tight without it.

     Then 1.2x again on 2026-09-11, with the trade name itself shortened by a word. The
     wordmark is five characters narrower than it was, so it takes the extra size and
     still ends before the name it replaced did. Every band below is scaled by the same
     1.2 — the ratios between them were measured and none of them changed. */
  font-size: var(--brand-fs);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  line-height: 1;
  color: var(--white);
}

.site-nav__summary {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.75rem;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--fg);
  background: transparent;
  border: 1px solid var(--border-strong);
}

.site-nav__list a {
  /* The wordmark's face, by instruction — the bar reads as one lockup rather than as a
     logo next to a menu. 800 is the only weight of it that is fetched. The call to action
     keeps Aldrich: see `.site-nav__list .button` below. */
  font-family: var(--font-wordmark);
  font-weight: 800;
  /* Twice the previous size, by instruction. Declared as a token because the header needs
     the difference between this and the wordmark's size to sit them on one baseline. */
  font-size: var(--nav-fs);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--white);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}
.site-nav__list a:hover,
.site-nav__list a:focus-visible { color: var(--fg-faint); }
.site-nav__list a[aria-current="page"] { color: var(--white); }

/* ── Dropdowns ───────────────────────────────────────────────────────────────
   A group's panel opens on hover and on focus-within. The closed panel is CLIPPED, not
   hidden: `display: none` or `visibility: hidden` would take its links out of the focus
   order, and then `:focus-within` could never fire because nothing inside could ever be
   focused. Clipping leaves them focusable, so Tab walks into the group and the panel
   reveals itself — no script, no aria-expanded for a script to desynchronise, and it
   works under a CSP that forbids inline JavaScript. */
.site-nav__group { position: relative; }

/* The reference marks a group with a chevron. Drawn from two borders rather than a glyph,
   so it needs no icon font and inherits the link's colour. */
.site-nav__group > a::after {
  content: "";
  width: 0.34em;
  height: 0.34em;
  margin-inline-start: 0.5em;
  border-right: 0.1em solid currentColor;
  border-bottom: 0.1em solid currentColor;
  transform: translateY(-0.12em) rotate(45deg);
  transition: transform var(--dur) var(--ease);
}
.site-nav__group:hover > a::after,
.site-nav__group:focus-within > a::after { transform: translateY(0.04em) rotate(225deg); }

.site-nav__sub {
  margin: 0;
  padding: 0;
  list-style: none;
}
.site-nav__sub a {
  /* 1.7x the size it was, by instruction, the same factor the bar's own links took. The
     panel is `width: max-content` up to 22rem, so it widens with the type rather than
     wrapping a row — measured at 1440 with the longest label, "Questions & Answers". */
  font-size: 1.377rem;
  letter-spacing: 0.04em;
  padding-inline: var(--space-2);
  /* A fifth of a second from black to white, by instruction — the row used to snap. */
  transition: background-color 200ms var(--ease), color 200ms var(--ease);
}
/* The reference's dropdown row inverts to solid white under the pointer. */
.site-nav__sub a:hover,
.site-nav__sub a:focus-visible {
  background: var(--white);
  color: var(--black);
}
.site-nav__sub li + li { border-top: 1px solid var(--border); }

@media (min-width: 64rem) {
  :root:root {
    /* 8.2rem, not 8: baseline-aligning the row let the wordmark's descender past the
       bottom of the disc, so the bar is a pixel taller than the disc alone made it.
       Measured at 1280/1440/1920 after the change — 128.1, 127.9, 129.0. */
    --header-h: 8.2rem;
    --header-pad: 0.62rem;
    --brand-fs: 2.06rem;
    --nav-fs: clamp(2.07rem, 1.02rem + 1.16vw, 2.53rem);
  }
  /* One row, always. A header that wraps to two rows is a header 200px tall. */
  .site-header__inner { flex-wrap: nowrap; }
  .site-nav { flex: 1 1 auto; }
  .site-nav__list {
    flex-wrap: nowrap;
    gap: clamp(0.9rem, 0.1rem + 1vw, 2rem);
    /* Every item in the bar sits on ONE baseline, not on centred boxes: the wordmark, the
       links and the button are three different faces at three different sizes, and
       `align-items: center` lines up their boxes while their letters sit at three
       different heights. Baseline alignment is what makes the row read as one line. */
    align-items: baseline;
  }
  /* THE MENU SITS ON THE WORDMARK'S BASELINE, by instruction, and it does so structurally
     rather than by a nudge: the row is baseline-aligned, and the brand is arranged so the
     baseline it offers the row is the wordmark's own.

     That last part is the whole trick. A flex container with centred items has no text
     baseline to give — it synthesizes one from its first item's bottom edge, which here is
     the disc, and aligning a menu to the bottom of a circle is not what was asked for. So
     the disc comes out of the flow: the brand is a grid with one in-flow child, the
     wordmark, and the disc is positioned absolutely beside it and centred on it. The grid
     then reports the wordmark's baseline, the row aligns the menu to it, and the lockup is
     unchanged — the disc's middle still sits on the wordmark's middle, measured.

     The earlier arrangement centred everything on the disc. It is recorded in HANDOFF §5,
     deviation 98, along with what this cost the call to action. */
  .site-header__inner { align-items: baseline; }
  .brand {
    align-self: auto;
    position: relative;
    display: grid;
    align-items: center;
    /* The row's height still comes from the disc even though the disc no longer occupies
       any of it. */
    min-height: var(--mark-size);
    padding-inline-start: calc(var(--mark-size) + 0.7rem);
  }
  .brand picture {
    display: flex;
    position: absolute;
    inset-inline-start: 0;
    top: 50%;
    transform: translateY(-50%);
  }
  .site-nav { align-self: auto; }
  /* The nav links keep one shared baseline among themselves; the call to action does not
     join them, it centres in the list. */
  .site-nav__cta { align-self: center; }
  .site-nav__list .button { align-items: center; }


  /* The panel hangs off its group, sized to its widest row and clipped shut. */
  .site-nav__sub {
    position: absolute;
    top: 100%;
    inset-inline-start: 0;
    z-index: 60;
    width: max-content;
    min-width: 100%;
    max-width: 22rem;
    /* Solid, not frosted. The header is the one glass surface on the page; a translucent
       panel over body copy leaves the text underneath legible through the rows, which is
       what it did at 0.94 alpha, and the reference's own dropdown is flat black. */
    background: var(--black);
    border: 1px solid var(--frost-border);
    box-shadow: var(--frost-drop);
    clip-path: inset(0 0 100% 0);
    opacity: 0;
    transition: clip-path var(--dur) var(--ease), opacity var(--dur) var(--ease);
  }
  .site-nav__group:hover > .site-nav__sub,
  .site-nav__group:focus-within > .site-nav__sub {
    clip-path: inset(0);
    opacity: 1;
  }
  /* A two-word wordmark, five groups and a call to action share one row from 1024px up,
     and there is no slack at the bottom of that range: let the name wrap and it drops a
     second line straight through the link set. It stays on one line and shrinks instead,
     and the button holds its own line too. */
  .brand { flex: 0 0 auto; min-width: 0; }
  .brand__name { white-space: nowrap; letter-spacing: 0.03em; }
  :root:root { --mark-size: 2.03rem; }
  .site-nav__list .button { white-space: nowrap; }
}
/* 1024–1279px is the tightest the one-row bar ever gets: an iPad in landscape has the
   desktop navigation and 944px of usable width for all of it. Everything in the row is
   sized down here and back up above it — measured, not guessed: at 1024 the wordmark
   ends where the first link begins with nothing to spare. */
@media (min-width: 64rem) and (max-width: 79.99rem) {
  :root:root { --header-h: 7.1rem; --brand-fs: 1.56rem; --nav-fs: 1.48rem; --mark-size: 3.24rem; }
  .site-nav__list { gap: 0.5rem; }

  .site-nav__list .button { padding: 0.58rem 1.1rem; font-size: 0.9rem; }
  .site-nav__list > li:first-child { margin-inline-start: 0.5rem; }

}
/* 1280–1440 is the second tight band: the wordmark has reached its floor and the links
   have reached theirs, and the two together are within a few pixels of the 1400px
   container. The free-space split waits until there is free space to split. */
@media (min-width: 80rem) and (max-width: 89.99rem) {
  :root:root { --nav-fs: 1.62rem; --mark-size: 3.78rem; }
  .site-nav__list { gap: 0.6rem; }

  .site-nav__list > li:first-child { margin-inline-start: 0.75rem; }

}
@media (min-width: 80rem) {
  /* The wordmark's size here supersedes the 80–90rem band above it deliberately: source
     order decides between two equally specific declarations, and this is the one that
     has been rendering since the band was written. */
  :root:root { --brand-fs: clamp(2.4rem, 0.16rem + 2.5vw, 4.37rem); }
  .brand__name { letter-spacing: 0.03em; }
  /* The mark, not the padding, is what sets the bar's height once the padding comes down:
     at 3.25rem it held the bar 12px taller than the 0.7x target on its own. */
  :root:root { --mark-size: 4.68rem; }
}
@media (min-width: 90rem) {
  /* An auto margin on each side of the link set, so the free space splits evenly and the
     links centre in the bar with the call to action pinned right — the reference's
     arrangement. `justify-content` cannot do this: one auto margin would eat all the
     slack and pack the links against the wordmark. Below 90rem there is no slack, so the
     autos would collapse to zero and the set would pack left anyway. */
  .site-nav__list > li:first-child { margin-inline-start: auto; }
  .site-nav__cta { margin-inline-start: auto; }
}
/* Above 1600 the content container stops growing at 1400 but the bar does not have to:
   the wordmark is at its 2x maximum by then and the five groups need more room than the
   text column does. The bar spans the page; the reading measure underneath it does not. */
@media (min-width: 100rem) {
  .site-header__inner { max-width: min(112rem, 100%); }
}

@media (max-width: 63.99rem) {
  .site-header__inner { flex-wrap: nowrap; }
  .site-nav { flex: 0 0 auto; order: 0; position: relative; }
  .site-nav[open] .site-nav__list {
    position: absolute;
    inset-inline-end: 0;
    top: calc(100% + 0.75rem);
    z-index: 60;
    width: max-content;
    min-width: 14rem;
    max-width: min(20rem, calc(100vw - 2 * var(--gutter)));
    /* Five groups plus their thirty-odd children do not fit a phone, so the panel scrolls
       inside itself rather than running off the bottom of the screen. */
    max-height: calc(100svh - var(--header-h) - var(--space-3));
    overflow-y: auto;
    overscroll-behavior: contain;
    margin: 0;
    padding: var(--space-2);
    /* Solid for the same reason the desktop panel is: the photograph behind it was
       reading straight through the menu. */
    background: var(--black);
    border: 1px solid var(--frost-border);
    box-shadow: var(--frost-drop);
  }
  .site-nav[open] .site-nav__list > li + li { border-top: 1px solid var(--border); }
  .site-nav[open] .site-nav__list a { padding-inline: var(--space-1); }

  /* No hover on a touch screen, so nothing is clipped here: the children sit inline under
     their group, indented, always open. The chevron would be a lie and comes off. */
  .site-nav__group > a::after { display: none; }
  .site-nav__sub {
    padding-inline-start: var(--space-2);
    border-inline-start: 1px solid var(--border);
    margin-block-start: 0.25rem;
  }
  .site-nav__sub li + li { border-top: 0; }
  .site-nav__sub a { font-size: 1.156rem; }
}

/* ── Service-area ticker ─────────────────────────────────────────────────────
   A second, thinner band under the bar, carrying the same idea as the reference site's
   marquee. It belongs to the header rather than the page: the bar's bottom padding is
   cancelled here so the two surfaces close up into one pinned block with a hairline
   between them.

   SPEED. `--ticker-dur` is not a taste value. The strip is meant to travel at the same
   rate as the old site's ticker, which was measured at 146.9 px/s (16154px of track,
   half of it consumed in 55s). The duration below is this run's rendered width divided by
   that rate — change the list, the face or the tracking and the duration has to be
   re-derived, or the speed changes with it.

   Aldrich ships one weight. The bold asked for here is the browser's synthesis of 400,
   because there is no 700 cut of this face to fetch — see HANDOFF §5, deviation 74. */
.site-ticker {
  /* Derived, not chosen: the run's rendered width divided by 117.49 px/s, which is the old
     site's measured 146.86 taken to the 0.8x the owner asked for. At 4214.3px that is
     35.87s. The run has grown three times — twice as cities were added, once when the
     separator became a star, which is 5.6px wider than the middle dot it replaced across
     40 of them. Change the list or the separator again and this number has to be
     re-derived, or the speed changes with it. */
  --ticker-dur: 35.87s;
  margin-block: var(--header-pad) calc(-1 * var(--header-pad));
  border-top: 1px solid var(--frost-border);
  background: rgba(0, 0, 0, 0.32);
}
.site-ticker__run {
  font-family: var(--font-nav);
  font-weight: 700;
  /* Under the nav links at every width — the strip is a footnote to the bar, not a second
     row of navigation. */
  font-size: 0.72rem;
  letter-spacing: 0.17em;
  text-transform: uppercase;
  color: var(--fg-muted);
  padding-block: 0.4rem;
}
/* The separator is drawn, not typed: it keeps the place names as clean text for anything
   reading the markup, and it also divides the two runs where they meet. */
.site-ticker__run span::after {
  content: "★";
  padding-inline: 1.15rem;
  color: var(--fg-faint);
}

/* ── Buttons ─────────────────────────────────────────────────────────────────
   White block, black label, square, wide track. Hover inverts to an outline. */
.button {
  font-family: var(--font-body);
  font-weight: 400;
  /* 1.3x, by instruction, and centred in its box. */
  font-size: 1.06rem;
  letter-spacing: 0.075em;
  text-transform: uppercase;
  text-align: center;
  justify-content: center;
  align-items: center;
  padding: 0.8rem 1.6rem;
}
/* ── Buttons ─────────────────────────────────────────────────────────────────
   EVERY rectangular button on the site, not just the header's — by instruction, they all
   rest the same and react the same. Frosted glass at rest, like the header bar, and a
   swirl of colour under the pointer. The palette is sampled from the reference clip the
   owner supplied — indigo, azure, magenta, orange, gold, purple — the colours of poured
   pigment before it is troweled flat, which is the product.

   THIS IS CSS, NOT WEBGL, AND THAT IS DELIBERATE. The old site drew its background with a
   fragment shader (`MW_EPX_Web/public/assets/bg.js`) and the domain-warped fbm marble in
   it is the effect being echoed here. That file is also §7's cautionary tale: its
   no-WebGL branch replaced `document.body.innerHTML`, so the whole site rendered as an
   error string on any browser without WebGL. A rotating conic gradient reproduces the
   swirl at no risk — nothing to fail, nothing to feature-detect, no canvas, and no script
   at all, which matters under a CSP that forbids inline JavaScript. */
.button {
  position: relative;
  /* A stacking context of its own, so the gradient below can sit at z-index -1: above
     this element's own background, below its label. Without it the -1 escapes and the
     swirl paints behind the whole header. */
  isolation: isolate;
  overflow: hidden;
  /* Hollow at rest, by instruction — an outline and nothing behind it, like the "Call"
     button was. The colour arrives only under the pointer. */
  background: transparent;
  border: 1px solid var(--border-strong);
  color: var(--white);
}
.button::before {
  content: "";
  position: absolute;
  /* Oversized and centred so the rotation never sweeps a corner into view. */
  inset: -75%;
  z-index: -1;
  background: var(--swirl);
  filter: blur(10px) saturate(175%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
  animation: cta-swirl 7s linear infinite;
  animation-play-state: paused;
}
.button:hover::before,
.button:focus-visible::before {
  opacity: 1;
  animation-play-state: running;
}
@keyframes cta-swirl { to { transform: rotate(1turn); } }

/* The label is small uppercase white, so it needs 4.5:1 against whatever passes under it.
   The gold stop is the bright one: unscrimmed it gives 2.6:1 and the label disappears as
   the sweep turns. 0.4 black over the whole gradient takes gold to roughly 5:1 and every
   other stop higher, and the saturation above buys back the vividness the scrim costs. */
.button::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(0, 0, 0, 0.4);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.button:hover::after,
.button:focus-visible::after { opacity: 1; }

.button:hover,
.button:focus-visible {
  color: var(--white);
  border-color: rgba(255, 255, 255, 0.4);
}

/* `.button--quiet` was the outline variant beside a filled one. Every button is now an
   outline, so the modifier has nothing left to say — it keeps a fainter border so a
   secondary action beside a primary one is still the quieter of the two. */
.button--quiet { border-color: var(--border); }

/* site.css sets `.site-nav__list .button { color: var(--accent-contrast) }` at (0,2,0),
   which beats a plain `.button` no matter how late this file loads — it is what left the
   header's label black on black glass. Matched here rather than fought with !important. */
.site-nav__list .button,
.site-nav__list .button:hover,
.site-nav__list .button:focus-visible { color: var(--white); }
/* The call to action keeps the face it has, by instruction — the menu links moved to the
   wordmark's and it did not. It needs saying explicitly because `.site-nav__list a` is
   more specific than `.button`, so the button has always taken the menu's face rather
   than the body face `.button` asks for; changing the menu without this line would have
   dragged the button along with it. */
.site-nav__list .button {
  font-family: var(--font-nav);
  font-weight: 400;
}

/* A still sweep rather than none: the colour is the point, the rotation is decoration. */
@media (prefers-reduced-motion: reduce) {
  .button::before,
  .badge::before { animation: none; }
}

@media (min-width: 64rem) {
  .button { padding: 0.9rem 2.6rem; }
  /* Smaller than it was, by instruction, and smaller than the page's own buttons: this
     one sits in a bar that just lost 30% of its height. */
  .site-nav__list .button { padding: 0.72rem 1.75rem; font-size: 1.04rem; }
}

/* ── Page banner ─────────────────────────────────────────────────────────────
   Interior pages open on a short near-black band: the title centred in thin uppercase
   with the breadcrumb trail under it. The breadcrumb is first in the markup because it
   belongs before the heading for a screen reader; `order` puts it under the title on
   screen without the template having to choose one over the other. */
.page-hero {
  background: var(--panel);
  border-bottom: 0;
  padding-block: var(--space-5);
  text-align: center;
}
.page-hero .container {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.page-hero h1 { order: 1; margin-block: 0; max-width: 20ch; }
.page-hero .breadcrumb { order: 2; margin-block-start: var(--space-2); }
.page-hero .badge { order: 0; margin-block-end: var(--space-2); }
.page-hero__subtitle { order: 3; margin-block: var(--space-2) 0; }
.page-hero__tagline { order: 4; }
.page-hero__intro { order: 5; margin-block-start: var(--space-2); }

.page-hero__subtitle,
.page-hero__intro {
  max-width: 60ch;
  color: var(--fg-muted);
}
.page-hero__tagline {
  /* The display h1 sets its line box to the cap height, so a following line at
     `--space-2` sits on the descenders. */
  margin-block: var(--space-3) 0;
  max-width: 46ch;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.75rem;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--fg-faint);
}

@media (min-width: 64rem) {
  .page-hero { padding-block: var(--space-6); }
}

/* The homepage opens on the photograph instead: full-bleed, veiled, with the heading set
   at poster scale over it. This is the one place the coated-floor image appears at size,
   which is also why it is the LCP element and why nothing else on the page is fixed. */
.home .page-hero {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: min(38rem, calc(100svh - var(--header-h)));
  padding-block: var(--space-6);
}
.home .page-hero h1 {
  font-size: clamp(2.35rem, 0.8rem + 6.6vw, 6.25rem);
  max-width: 18ch;
}

/* THE HERO VIDEO. Three layers, and their order is the point: the still photograph is the
   hero's own background at the bottom, the video sits above it at -2, and the veil that
   makes white type legible is a pseudo-element at -1 rather than part of the background
   shorthand. A gradient inside `background` can only darken the still, which would leave
   the heading sitting on unveiled video the moment the first frame paints.
   `isolation: isolate` on the hero keeps both negative layers inside it.

   The video is absolutely positioned, so it contributes nothing to layout and cannot move
   the heading: CLS stays at zero whether it loads, stalls or fails. */
.page-hero__still,
.page-hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 55%;
}
.page-hero__still { z-index: -3; }
.page-hero__video { z-index: -2; }

/* THE STAGE. The clip is the backdrop to everything that scrolls over it: the heading
   first, then the page's prose. The backdrop spans the stage and pins one screen of itself
   to the top, so the text moves and the picture does not; hero.js spends the clip's seconds
   over exactly that distance, which is why the last second and the last paragraph arrive
   together. When the stage ends the backdrop releases and the estimate form sits on the
   page's own black.

   Nothing here is set by script, so nothing moves while JavaScript loads and a visitor
   without it reads the same text over a still. `svh` because a phone's toolbars change
   `vh` mid-scroll, which would retune the mapping underfoot. */
.hero-stage { position: relative; }
.hero-stage__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: clip;
}
.hero-stage__pin {
  position: sticky;
  top: var(--header-h);
  height: calc(100svh - var(--header-h));
  isolation: isolate;
}
/* The heading keeps a screen to itself; the prose then crosses the same picture instead of
   sitting on the panel it uses elsewhere. */
.hero-stage > .page-hero {
  background: transparent;
  min-height: calc(100svh - var(--header-h));
}
.hero-stage > .container.prose {
  padding-block: var(--space-6) var(--space-7);
  /* Body copy over moving picture. The veil is deliberately light enough to leave the clip
     readable as a moving picture, so the legibility the veil used to buy is bought here
     instead: a tight shadow for the letterform's own edge and a wide one for the frames
     where a light streak crosses a line, which no fixed veil can predict. */
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.92), 0 2px 12px rgba(0, 0, 0, 0.8);
}

/* THE VEIL DEEPENS AS THE PROSE ARRIVES. A heading over a picture wants the picture; four
   hundred words over the same picture want to be read. hero.js writes `--stage-veil` from
   the same scroll progress it uses for the clip — light while the heading has the screen to
   itself, heavy by the time the first paragraph is on it. The declared value is the one a
   visitor without JavaScript gets, and it is the readable one: the still is then permanent,
   and prose beats poster. Lighthouse cannot judge any of this — it measures text contrast
   against the CSS background colour, not against a video — so it was set by eye. */
.hero-stage__pin::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  --veil-mid: var(--stage-veil, 0.72);
  background: linear-gradient(180deg,
    rgba(0, 0, 0, calc(var(--veil-mid) * 0.8)) 0%,
    rgba(0, 0, 0, var(--veil-mid)) 45%,
    rgba(0, 0, 0, calc(var(--veil-mid) * 1.1)) 100%);
  pointer-events: none;
}
/* A phone gets more of it, because a phone gets a narrower column: the same paragraphs run
   twice as far down the picture. */
@media (max-width: 63.99rem) {
  .hero-stage__pin::after { --veil-mid: var(--stage-veil-mobile, 0.8); }
}

/* Reduced motion: no pinning, no scrubbing, no stage. The hero is a hero and the prose is
   prose, both on the page's own background; hero.js removes the clip entirely and the
   photograph behind it is what stays. */
@media (prefers-reduced-motion: reduce) {
  .hero-stage__bg { position: static; }
  .hero-stage__pin { position: static; height: min(38rem, calc(100svh - var(--header-h))); }
  .hero-stage > .page-hero { background: var(--panel); min-height: 0; }
}

/* ── Buttons ─────────────────────────────────────────────────────────────────
   White block, black label, square, wide track. Hover inverts to an outline. */
.button {
  font-family: var(--font-body);
  font-weight: 400;
  /* 1.3x, by instruction, and centred in its box. */
  font-size: 1.06rem;
  letter-spacing: 0.075em;
  text-transform: uppercase;
  text-align: center;
  justify-content: center;
  align-items: center;
  padding: 0.8rem 1.6rem;
}
/* ── Buttons ─────────────────────────────────────────────────────────────────
   EVERY rectangular button on the site, not just the header's — by instruction, they all
   rest the same and react the same. Frosted glass at rest, like the header bar, and a
   swirl of colour under the pointer. The palette is sampled from the reference clip the
   owner supplied — indigo, azure, magenta, orange, gold, purple — the colours of poured
   pigment before it is troweled flat, which is the product.

   THIS IS CSS, NOT WEBGL, AND THAT IS DELIBERATE. The old site drew its background with a
   fragment shader (`MW_EPX_Web/public/assets/bg.js`) and the domain-warped fbm marble in
   it is the effect being echoed here. That file is also §7's cautionary tale: its
   no-WebGL branch replaced `document.body.innerHTML`, so the whole site rendered as an
   error string on any browser without WebGL. A rotating conic gradient reproduces the
   swirl at no risk — nothing to fail, nothing to feature-detect, no canvas, and no script
   at all, which matters under a CSP that forbids inline JavaScript. */
.button {
  position: relative;
  /* A stacking context of its own, so the gradient below can sit at z-index -1: above
     this element's own background, below its label. Without it the -1 escapes and the
     swirl paints behind the whole header. */
  isolation: isolate;
  overflow: hidden;
  /* Hollow at rest, by instruction — an outline and nothing behind it, like the "Call"
     button was. The colour arrives only under the pointer. */
  background: transparent;
  border: 1px solid var(--border-strong);
  color: var(--white);
}
.button::before {
  content: "";
  position: absolute;
  /* Oversized and centred so the rotation never sweeps a corner into view. */
  inset: -75%;
  z-index: -1;
  background: var(--swirl);
  filter: blur(10px) saturate(175%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
  animation: cta-swirl 7s linear infinite;
  animation-play-state: paused;
}
.button:hover::before,
.button:focus-visible::before {
  opacity: 1;
  animation-play-state: running;
}
@keyframes cta-swirl { to { transform: rotate(1turn); } }

/* The label is small uppercase white, so it needs 4.5:1 against whatever passes under it.
   The gold stop is the bright one: unscrimmed it gives 2.6:1 and the label disappears as
   the sweep turns. 0.4 black over the whole gradient takes gold to roughly 5:1 and every
   other stop higher, and the saturation above buys back the vividness the scrim costs. */
.button::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(0, 0, 0, 0.4);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.button:hover::after,
.button:focus-visible::after { opacity: 1; }

.button:hover,
.button:focus-visible {
  color: var(--white);
  border-color: rgba(255, 255, 255, 0.4);
}

/* `.button--quiet` was the outline variant beside a filled one. Every button is now an
   outline, so the modifier has nothing left to say — it keeps a fainter border so a
   secondary action beside a primary one is still the quieter of the two. */
.button--quiet { border-color: var(--border); }

/* site.css sets `.site-nav__list .button { color: var(--accent-contrast) }` at (0,2,0),
   which beats a plain `.button` no matter how late this file loads — it is what left the
   header's label black on black glass. Matched here rather than fought with !important. */
.site-nav__list .button,
.site-nav__list .button:hover,
.site-nav__list .button:focus-visible { color: var(--white); }
/* The call to action keeps the face it has, by instruction — the menu links moved to the
   wordmark's and it did not. It needs saying explicitly because `.site-nav__list a` is
   more specific than `.button`, so the button has always taken the menu's face rather
   than the body face `.button` asks for; changing the menu without this line would have
   dragged the button along with it. */
.site-nav__list .button {
  font-family: var(--font-nav);
  font-weight: 400;
}

/* A still sweep rather than none: the colour is the point, the rotation is decoration. */
@media (prefers-reduced-motion: reduce) {
  .button::before,
  .badge::before { animation: none; }
}

@media (min-width: 64rem) {
  .button { padding: 0.9rem 2.6rem; }
  /* Smaller than it was, by instruction, and smaller than the page's own buttons: this
     one sits in a bar that just lost 30% of its height. */
  .site-nav__list .button { padding: 0.72rem 1.75rem; font-size: 1.04rem; }
}

/* ── Page banner ─────────────────────────────────────────────────────────────
   Interior pages open on a short near-black band: the title centred in thin uppercase
   with the breadcrumb trail under it. The breadcrumb is first in the markup because it
   belongs before the heading for a screen reader; `order` puts it under the title on
   screen without the template having to choose one over the other. */
.page-hero {
  background: var(--panel);
  border-bottom: 0;
  padding-block: var(--space-5);
  text-align: center;
}
.page-hero .container {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.page-hero h1 { order: 1; margin-block: 0; max-width: 20ch; }
.page-hero .breadcrumb { order: 2; margin-block-start: var(--space-2); }
.page-hero .badge { order: 0; margin-block-end: var(--space-2); }
.page-hero__subtitle { order: 3; margin-block: var(--space-2) 0; }
.page-hero__tagline { order: 4; }
.page-hero__intro { order: 5; margin-block-start: var(--space-2); }

.page-hero__subtitle,
.page-hero__intro {
  max-width: 60ch;
  color: var(--fg-muted);
}
.page-hero__tagline {
  /* The display h1 sets its line box to the cap height, so a following line at
     `--space-2` sits on the descenders. */
  margin-block: var(--space-3) 0;
  max-width: 46ch;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.75rem;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--fg-faint);
}

@media (min-width: 64rem) {
  .page-hero { padding-block: var(--space-6); }
}

/* The homepage opens on the photograph instead: full-bleed, veiled, with the heading set
   at poster scale over it. This is the one place the coated-floor image appears at size,
   which is also why it is the LCP element and why nothing else on the page is fixed. */
.home .page-hero {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: min(38rem, calc(100svh - var(--header-h)));
  padding-block: var(--space-6);
}
.home .page-hero h1 {
  font-size: clamp(2.35rem, 0.8rem + 6.6vw, 6.25rem);
  max-width: 18ch;
}

@media (min-width: 64rem) {
  .home .page-hero { min-height: min(44rem, calc(100svh - var(--header-h))); }
}

/* THE PHOTOGRAPH IS THE REDUCED-MOTION HERO, and only that. It used to be the homepage's
   background at every size; the video replaced it, and the video's poster — its own first
   frame — is what now covers the hero before playback, while the file fails, and with
   JavaScript off. The one case none of those cover is a visitor who asks for reduced
   motion, because nav.js removes the video element outright for them. Loading 128KB of
   photograph on every other visit to serve that one case cost the homepage its LCP: it is
   a CSS background, so it is discovered only after the stylesheet parses, and it raced the
   poster for a throttled connection. Declared here it is fetched by the visitors who need
   it and by nobody else. */
@media (prefers-reduced-motion: reduce) {
  .home .page-hero {
    background:
      image-set(
        url('/assets/images/bg/ground-1280.avif') type('image/avif'),
        url('/assets/images/bg/ground-1280.webp') type('image/webp'),
        url('/assets/images/bg/ground-1280.jpg') type('image/jpeg'))
        50% 60% / cover no-repeat;
  }
  @media (min-width: 64rem) {
    .home .page-hero {
      background:
        image-set(
          url('/assets/images/bg/ground-1920.avif') type('image/avif'),
          url('/assets/images/bg/ground-1920.webp') type('image/webp'),
          url('/assets/images/bg/ground-1920.jpg') type('image/jpeg'))
          50% 60% / cover no-repeat;
    }
  }
  @media (min-width: 90rem) {
    .home .page-hero {
      background:
        image-set(
          url('/assets/images/bg/ground-2560.avif') type('image/avif'),
          url('/assets/images/bg/ground-2560.webp') type('image/webp'),
          url('/assets/images/bg/ground-2560.jpg') type('image/jpeg'))
          50% 60% / cover no-repeat;
    }
  }
}

/* The badge wears the buttons' sweep, and wears it always — no pointer involved, by
   instruction. Same scrim underneath for the same reason: an 11px white label needs 4.5:1
   and the gold stop gives 2.6:1 unscrimmed. */
.badge {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.6875rem;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--white);
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: 0;
  padding: 0.5em 1.1em;
}
.badge::before {
  content: "";
  position: absolute;
  inset: -75%;
  z-index: -1;
  background: var(--swirl);
  filter: blur(10px) saturate(175%);
  animation: cta-swirl 7s linear infinite;
}
.badge::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(0, 0, 0, 0.4);
}

.breadcrumb ol { justify-content: center; margin-block-end: 0; }
.breadcrumb {
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  color: var(--fg-faint);
}
.breadcrumb a { color: var(--fg-muted); }
.breadcrumb a:hover { color: var(--white); }

/* ── Sections ────────────────────────────────────────────────────────────────
   The reference centres every section heading over its content. That is right for the
   listing sections and wrong for a 3,000-word guide, where a centred heading detaches
   from the column it introduces — so the rule stops at .prose. */
.site-main section { margin-block: var(--space-6); }

.site-main > article > section:not(.prose) > h2,
.site-main > article > section:not(.prose) > .container > h2 {
  text-align: center;
  margin-block-end: var(--space-4);
}

/* ── Panels ──────────────────────────────────────────────────────────────────
   Flat near-black rectangles on the black, told apart by a single hairline rather than
   by a shadow. No blur: the reference has none, and ten blurred surfaces is the most
   expensive thing a phone can be asked to paint. */
.card,
.build__step,
.gallery__item {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 0;
  padding: var(--space-3);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.card:hover,
.build__step:hover,
.gallery__item:hover {
  background: var(--panel-raised);
  border-color: var(--border-strong);
}

/* The four build steps are the reference's icon boxes: centred, generous, the numeral
   doing the work an icon does there. */
.build__step { text-align: center; padding-block: var(--space-4); }
.build__step h3 { color: var(--fg); }
.build__step h3::before { color: var(--fg-faint); }

.card h3 a { color: var(--fg); text-decoration: none; }
.card h3 a:hover { color: var(--fg-muted); }
.card__meta { color: var(--fg-faint) !important; letter-spacing: var(--track-label); text-transform: uppercase; font-size: 0.6875rem; }
.card__price { color: var(--fg) !important; }

/* site.css sizes the skip link from padding and the base type size, which this theme
   pulls down to fit Michroma — that took it to 40px and under the 44px floor the layout
   suite asserts. It gets the floor explicitly rather than inheriting one. */
.skip-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}

.price {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(2rem, 1.4rem + 2.4vw, 3.25rem);
  letter-spacing: var(--track-display);
}

/* ── Swatches and gallery ────────────────────────────────────────────────────
   The reference's project tile: the image with no frame, its name under it in small
   uppercase. Colour never carries the identification on its own — every tile is named
   in text, because "Raven" and "Obsidian" are two dark blends a customer names on the
   phone. */
.swatch {
  background: transparent;
  border: 0;
  overflow: hidden;
}
.swatch img {
  border: 0;
  border-radius: 0;
  transition: transform 0.9s var(--ease), opacity var(--dur) var(--ease);
}
.swatch:hover img { transform: scale(1.06); opacity: 0.82; }
.swatch__name {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg);
}

.gallery img { border-radius: 0; }
.gallery__caption {
  font-family: var(--font-body);
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg);
}

/* ── Prose, tables, the long read ────────────────────────────────────────────
   `.container.prose` takes .prose's 68ch max-width and .container's auto margins, so the
   reading column centred itself while the headings beside it stayed left. The measure is
   right; the centring was not. Constrain the children instead — and give the reading
   pages a narrower shell than the listing pages, because a 68ch column left-aligned
   inside a 1400px one is 500px of dead space beside every paragraph. */
/* ONE column, not one measure per block. `--measure` is `68ch`, and `ch` resolves against
   each element's OWN font size — so an h2 in Montserrat at 38px computed 1711px (no
   constraint at all), a paragraph in Michroma at 14.4px computed 931px, and an h3
   computed 901px. Each block then centred itself at its own width, which put three
   different left edges down the same column: the "Residential" heading started 16px left
   of the list under it and the section headings 38px left of both. Constrain the COLUMN
   and let every block fill it, and there is one edge by construction. 58rem is the same
   reading measure the body face used to produce, expressed in a unit that does not change
   per element.

   The column also keeps the PAGE's container width, and takes the measure on its children
   instead. Narrowing `.container.prose` itself centred the reading column at a different
   left edge from every `.container` section on the same page — 296px against 60px on a
   colour catalogue — which is the same defect one level up. One container width site-wide,
   one measure inside it, one left edge everywhere. */
/* Centred on the page, by instruction, and still one edge for everything.
   `margin-inline: auto` on the prose blocks alone would have centred the reading column
   at a different left edge from the `.container` sections beside it — the exact defect
   the note above describes, one level up. So the CONTENT container narrows instead: every
   `.container` inside <main> is the same 62rem and `.container` already centres itself, so
   prose blocks, section headings, swatch grids and card rows all share one centred column.
   The header bar keeps the full `--container` width; the bar spans the page, the reading
   measure does not. */
.site-main .container { max-width: var(--content); }
.container.prose > :not(table):not(.scroll-x) { max-width: 100%; }

/* The homepage reads as a landing page rather than a document, so its section headings
   centre over the column. The guides and the legal set keep theirs ranged left. */
.home .container.prose > h2 { text-align: center; }

/* Tracking comes back in for the long read — see --track-read above. */
.prose p,
.prose li,
.prose dd { line-height: 1.85; letter-spacing: var(--track-read); }
.prose h2 { margin-block-start: var(--space-6); }

.prose th {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.6875rem;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--fg);
  border-block-end: 1px solid var(--border-strong);
}
.prose th, .prose td, .specs th, .specs td { border-block-end: 1px solid var(--border); }
.specs th { font-weight: 400; color: var(--fg); }
.specs td { color: var(--fg-muted); }

.faq dt {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.0625rem;
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--fg);
  margin-block-start: var(--space-4);
  padding-block-start: var(--space-2);
  border-block-start: 1px solid var(--border);
}
.faq dd { letter-spacing: var(--track-read); }

.legal__meta,
.legal__contact { color: var(--fg-faint); font-size: 0.8125rem; letter-spacing: 0.08em; }

/* ── Form ────────────────────────────────────────────────────────────────────
   Dark wells with a hairline, exactly like the reference's newsletter block. Labels stay
   visible above their field — a placeholder is not a label. */
.field label {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 0.6875rem;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--fg-faint);
}
.field input,
.field select,
.field textarea {
  background: var(--panel);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 0;
  padding: 0.75rem var(--space-2);
  transition: border-color var(--dur) var(--ease);
}
.field input:focus,
.field select:focus,
.field textarea:focus { border-color: var(--border-strong); }
.field input::placeholder,
.field textarea::placeholder { color: var(--fg-faint); }

/* The panel centres in its column. site.css sizes it in `ch`, which resolves against each
   element's own font size and left-aligns whatever it lands on — the same defect that put
   every section on the site off its column (HANDOFF §5, deviation 71). A rem width and an
   auto margin put the form on the page's centre line, and its internal two-up grid is
   untouched. */
/* The submit button centres under the form. It is inline-flex, so the centring has to come
   from the button's own box rather than from text alignment on the paragraph around it. */
.estimate-form .button {
  display: flex;
  width: fit-content;
  margin-inline: auto;
}
.estimate-form { max-width: 46rem; margin-inline: auto; }

/* The select's own drop-down, in the header dropdown's clothes. Chrome 135 and up let a
   select opt into being styled like any other element; every other engine ignores the
   whole block and keeps its native popup, which is why this is a @supports and not a
   rewrite of the control into divs. A hand-built listbox would need script the CSP does
   not allow and would drop the keyboard and mobile behaviour the native control has. */
@supports (appearance: base-select) {
  .field select,
  .field select::picker(select) { appearance: base-select; }
  .field select { justify-content: space-between; gap: var(--space-2); }
  /* Chrome's default chevron is a glyph in the platform's own face. Replaced by the same
     two rotated borders the nav group's chevron is drawn from, so the two match. The
     element carries the borders itself — a pseudo-element cannot hang off a
     pseudo-element — and the glyph goes out with `appearance: none`. */
  .field select::picker-icon {
    appearance: none;
    /* The UA still paints its own arrow inside the box; it is drawn in currentColor. */
    color: transparent;
    width: 0.4em;
    height: 0.4em;
    margin-block-end: 0.2em;
    margin-inline-end: 0.3em;
    border-right: 1px solid var(--fg-muted);
    border-bottom: 1px solid var(--fg-muted);
    transform: rotate(45deg);
    transition: transform var(--dur) var(--ease);
  }
  .field select:open::picker-icon { transform: translateY(0.15em) rotate(225deg); }
  .field select::picker(select) {
    /* Under the control, always. Left to itself Chrome hangs the panel so the current
       option sits over the control, which put four rows on top of the field above this
       one. These four lines pin it to the control's bottom edge instead. */
    position-try-fallbacks: none;
    inset: auto;
    top: anchor(bottom);
    left: anchor(left);
    min-width: anchor-size(width);
    background: var(--black);
    border: 1px solid var(--frost-border);
    box-shadow: var(--frost-drop);
    padding: 0;
    margin-block-start: 0.25rem;
  }
  .field select option {
    font-family: var(--font-body);
    letter-spacing: var(--track-read);
    color: var(--fg);
    background: var(--black);
    padding: 0.7rem var(--space-2);
    /* A fifth of a second, black to white, the same crossing as the nav panel's rows. */
    transition: background-color 200ms var(--ease), color 200ms var(--ease);
  }
  .field select option:hover,
  .field select option:focus { background: var(--white); color: var(--black); }
  .field select option + option { border-top: 1px solid var(--border); }
  /* The tick Chrome reserves a column for shifts every label sideways; the panel already
     shows the current value in the closed control. */
  .field select option::checkmark { display: none; }
}
.field--check label {
  color: var(--fg-muted);
  font-weight: 400;
  font-size: 0.85rem;
  letter-spacing: var(--track-read);
  text-transform: none;
}
.field--check input { accent-color: var(--white); }
.estimate-form__consent { color: var(--fg-faint); letter-spacing: var(--track-read); }
.estimate-form__status { letter-spacing: var(--track-label); text-transform: uppercase; font-size: 0.8125rem; }

/* ── The closing call to action ──────────────────────────────────────────────
   The reference closes its pages on a full-bleed photographic band. Same photograph as
   the homepage hero, held darker so the type over it needs no plate. */
.cta {
  position: relative;
  border: 0;
  padding-block: var(--space-7);
  text-align: center;
  /* Its own copy of the photograph at 960px, not the hero's at 1280. The band is held at
     0.72 black, so nothing above that width is visible in it — and on the homepage the
     hero's image is no longer this file, so the two stopped sharing a download. 128KB
     racing the largest paint for a throttled connection cost the homepage two points. */
  background:
    linear-gradient(180deg, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.72)),
    image-set(
      url('/assets/images/bg/ground-cta-960.avif') type('image/avif'),
      url('/assets/images/bg/ground-cta-960.jpg') type('image/jpeg'))
      50% 50% / cover no-repeat;
}
.cta h2 { margin-block: 0 var(--space-2); }
.cta p { margin-inline: auto; max-width: 52ch; }
.cta .button { margin-block-start: var(--space-2); }
.cta .button + .button { margin-inline-start: var(--space-1); }

/* ── Footer ──────────────────────────────────────────────────────────────────
   Four columns on a near-black band, uppercase tracked headings, links in body type,
   and a hairline over the copyright line. */
.site-footer {
  margin-top: 0;
  background: var(--panel);
  border-block-start: 1px solid var(--border);
  padding-block: var(--space-6) var(--space-3);
  color: var(--fg);
}
.site-footer__grid { gap: var(--space-4) var(--space-3); }
/* site.css sizes the grid for four link groups; this footer carries five, and the fifth
   wrapping onto its own row leaves a hole the width of the page. The wider container
   here has room for all six cells. */
@media (min-width: 80rem) {
  .site-footer__grid { grid-template-columns: 1.3fr repeat(5, minmax(0, 1fr)); }
}
/* The mark replaces the wordmark here, at 1.5x the header's diameter. The header's is
   5.5rem, so this is 8.25rem. `.site-footer__name` no longer exists in the markup. */
.site-footer__logo {
  display: inline-block;
  margin-block-end: var(--space-3);
  line-height: 0;
}
.site-footer__logo img {
  width: 8.25rem;
  height: 8.25rem;
  border-radius: 50%;
}

/* Caps and weight, by instruction — this is the one substantiated claim on the page and
   it is meant to be read. Michroma has no bold, so the weight comes from size, tracking
   and full white against the muted copy around it. */
.site-footer__insured {
  margin-block-start: var(--space-2);
  font-size: 0.8125rem;
  letter-spacing: var(--track-label);
  color: var(--white);
}
.site-footer__meta,
.site-footer__contact { color: var(--fg-muted); letter-spacing: var(--track-read); line-height: 2; }
/* The phone number and the address are inline links in a paragraph, so they inherit the
   line box and nothing else. At Michroma's smaller size that line box is 17px, which is
   under the 24px minimum target size and leaves under 24px between one link and the next.
   Padding gives them the height; the doubled line-height gives them the separation. */
/* inline-flex with the tap floor, not padding: the layout suite's own minimum is 44px,
   stricter than the 24px Lighthouse asks for, and it measures anything that is not a bare
   inline box. Phone and address each sit on their own line here, so the taller target
   costs nothing. */
.site-footer__contact a {
  color: var(--fg);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}
.site-footer__heading {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 0.875rem;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--fg);
  margin-block-end: var(--space-2);
}
.site-footer__group a,
.site-footer__legal a {
  color: var(--fg-muted);
  text-decoration: none;
  letter-spacing: var(--track-read);
  transition: color var(--dur) var(--ease);
}
.site-footer__group a:hover,
.site-footer__legal a:hover { color: var(--white); text-decoration: none; }
.site-footer__bottom {
  border-block-start: 1px solid var(--border);
  padding-block-start: var(--space-3);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  color: var(--fg-faint);
}

/* Half the leading, by instruction, and the footer loses the height with it. It happens
   at the desktop breakpoint and not below it: the rows are 44px because a finger needs
   44px, and that floor is a phone's, not a mouse's. 1.5rem is exactly the 24px SC 2.5.8
   asks of any pointer, so the tightened rows still clear the standard — going to a true
   half of 44 would not. `tests/specs/layout.spec.mjs` measures the 44px floor at 390px
   wide, which is the width where it means something. */
@media (min-width: 64rem) {
  .site-footer__group a,
  .site-footer__legal a,
  .site-footer__contact a { min-height: 1.5rem; }
  .site-footer__meta,
  .site-footer__contact { line-height: 1.5; }
  .site-footer__logo { margin-block-end: var(--space-2); }
}

/* ── Motion ──────────────────────────────────────────────────────────────────
   The reference fades each section up as it enters. Here that is a scroll-driven
   animation and a cross-document view transition — no library, nothing to hydrate, and
   no state left behind if the browser has neither. */
@view-transition { navigation: auto; }

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .site-main > article > section,
    .site-main > article > .container,
    .cards,
    .build,
    .swatches,
    .gallery {
      animation: theme-rise var(--dur-slow) var(--ease) both;
      animation-timeline: view();
      animation-range: entry 0% entry 50%;
    }
  }
}
/* Movement only. There is no opacity in this animation and there must not be: a
   scroll-driven fade leaves every element below the fold sitting at its `from` value, so
   an audit that walks the whole document — axe does — reads real body text at a tenth of
   its opacity and calls it a contrast failure. It cost /colors/quartz/ five accessibility
   points once, and /colors/metallic/ at sixty swatches fails it every run. Capping the
   fade at a quarter of the range was the first attempt and it is not enough, because the
   elements that fail are the ones that have not entered the range at all. The slide reads
   as the same effect and cannot make text unreadable. */
@keyframes theme-rise {
  from { transform: translateY(26px); }
}

::view-transition-old(root),
::view-transition-new(root) { animation-duration: var(--dur); }

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* ── Mobile ──────────────────────────────────────────────────────────────────
   The reference halves its display sizes below the tablet breakpoint and stacks every
   grid. The stacking is already site.css's default; what is left is the type, the header
   height and the one blurred surface. */
@media (max-width: 63.99rem) {
  /* Measured, not guessed: the wordmark takes two lines below 33.5rem and the bar is
     140px there; above it the name fits one line and the bar is back to 128. A token
     that under-declares the height hides an anchor target underneath it, so each half of
     the band gets the height it actually has. */
  :root:root { --frost-blur: 12px; --header-pad: 0.28rem; --header-h: 8.8rem; }
  /* The disc is deliberately left shrinkable here. Pinning it to its declared diameter
     was tried and reverted: at 375 and 390 — the two commonest phone widths — a full-size
     disc plus the wordmark beside it overruns the menu button, and the header's own
     `overflow: hidden` turns that into letters disappearing under a border rather than
     into a scrollbar anyone would notice. The flex shrink IS the responsive ramp: the
     disc gives up width only where the row has none to spare, and is back to its full
     5.04rem from 480px up. */
  /* Three words rather than the one a reference wordmark gets: it wraps to two lines on
     a phone, so the tracking comes in and the leading closes up to keep it off the mark. */
  /* The wordmark takes two lines on a phone at this size, so the mark grows with it —
     at 2.5rem it read as a bullet beside a headline rather than half of a lockup. */
  .brand__name { letter-spacing: 0.02em; line-height: 1.0; }
  :root:root { --mark-size: 5.04rem; }
}
@media (min-width: 33.5rem) and (max-width: 63.99rem) {
  :root:root { --header-h: 8.1rem; }
}
/* 360px and under. Sarpanch Black is a wide face and nineteen characters of it do not fit
   beside a menu button on a 320px screen — the overflow sweep starts there, so this band
   exists to hold that one width rather than to shrink every phone. */
@media (max-width: 22.5rem) {
  :root:root { --header-h: 6.9rem; --brand-fs: 2.11rem; --mark-size: 2.34rem; }


  .site-main section { margin-block: var(--space-5); }
  .cta { padding-block: var(--space-6); }
  .site-footer { padding-block: var(--space-5) var(--space-3); }
}
