/* Hallmark · genre: editorial · macrostructure: Feature Stack
 * theme: custom — vibe "quarried stone, desert light, cut by hand"
 *   paper  oklch(96.5% 0.009 72) · accent oklch(48% 0.140 42)
 *   display Fraunces · body IBM Plex Sans
 *   axes: light / high-contrast-serif / warm
 *
 * v4 — flipped dark → light, plus SEO/GEO and a video.
 *   · six pages of near-black read as heavy rather than expensive, and the
 *     archive fought it: these photographs are almost all white marble and
 *     pale porcelain, which sat on the dark ground as bright rectangles in
 *     a void. On limestone paper they sit *in* the page. Dark is preserved
 *     under [data-theme="dark"] in tokens.css — one attribute switches back.
 *   · body weight 300 → 400. The 300 was compensating for light-on-dark
 *     optical weight; on pale paper it read thin.
 *   · accent 64% → 48%: the dark theme's mid-tone accent fails on near-white.
 *   · grain 0.045 → 0.028 — the same noise that gave the dark page tooth
 *     makes a light page look dirty.
 *   · material study video (images/make-video.py), built from the client's
 *     own photographs rather than generated. The ONE script on the site sits
 *     beside it: prefers-reduced-motion cannot stop an autoplaying video
 *     from CSS, so JS swaps autoplay for controls. With JS off it just plays.
 * nav: N9 edge-aligned minimal (knobs: CTA=outlined, wordmark=serif, padding=spacious)
 * hero: H6 photographic fold (knobs: image=full-height right bleed,
 *       caption=text column, text=hard left) — was H2 split diptych in v1/v2
 * feature: F2 sticky-scroll stack (knobs: pinned=left, pane=photography, steps=4)
 *          — below 60rem the plates become a horizontal snap rail instead of
 *            a column; see the 60rem block for why (13-screen phone page,
 *            12,400px tablet page)
 * services: F3 tabular spec sheet (knobs: cols=2, rules=every row)
 * cta: C3 typographic link (knobs: underline=solid, hover=thicken, arrow=→)
 * footer: Ft4 dense colophon (knobs: family=sans, layout=blocks, includes=service area)
 * enrichment: E7 grain plate (feTurbulence, 0.045) — real client photography
 * proof: none — stats, logos and testimonials stripped as unverifiable
 * pre-emit critique: P5 H5 E5 S5 R4 V5
 *
 * v2 — first pass was correct but plain. What changed and why:
 *   · accent now appears AT REST as the "set line" above every heading —
 *     v1 defined a palette and only ever used it on hover, so the page was
 *     greyscale on near-black. Motivated, not decorative: a setter strikes a
 *     layout line before anything is bonded, which is what the copy is about.
 *   · grain plate over the whole page — flat colour fields were the main
 *     reason restraint read as plain. Stone has tooth.
 *   · section titles 2.44rem → clamp(2.44, 3vw+1rem, 4rem). v1 had no scale
 *     contrast anywhere.
 *   · four identical stacks → default / --bleed / default / --reverse, so the
 *     page stops repeating itself after the first section.
 *   · statement band on a raised surface between the work and the spec sheet.
 *   · motion added: one scroll-driven settle via view-timeline, CSS-only.
 *     Everything is visible by default; the animation lives inside
 *     @supports, so an unsupporting browser gets a finished page.
 *
 * v3 — "go all out". Where the drama actually comes from:
 *   · hero rebuilt H2 → H6: 100svh, image column bleeding off the right
 *     edge entirely, nav absolute over it. v2's hero was a modest photo in
 *     a modest column; this is the page's one moment of real scale.
 *     Dedicated 1200×1800 asset (images/make-hero.py) — the grid plates cap
 *     at 1400px and would have been soft at full height.
 *   · parallax: frames crop, photographs travel inside them at a different
 *     rate. scale(1.06) is the headroom that stops the drift exposing an
 *     edge; .plate and .hero__figure carry overflow so the movement is
 *     contained and the page never scrolls sideways.
 *   · the set line now DRAWS (scaleX) slightly ahead of its heading — line,
 *     then title, then body. It is a struck line; it should behave like one.
 *   · statement scaled to clamp(2.75rem, 7vw, 7rem).
 *   · two legibility fixes the drama forced: the hero caption moved off the
 *     photograph into the text column (12.8px type will not read over that
 *     checkerboard at any text-shadow), and .chip--nav went opaque because
 *     the top scrim alone left checkers showing through it. A blurred
 *     backdrop would have been glassmorphism, which this genre bans.
 *
 * slop test 58/58 — verified in-browser, not asserted:
 *   contrast: pass (40–41) — 12 pairs measured; --color-neutral lifted
 *             56%→64% after .spec/.caption/.colophon measured 3.97–4.23:1
 *   mobile:   pass (34, 49, 50–57) — 320/375/414/768/960 no h-scroll,
 *             no undersized standalone targets, no two-line clickables,
 *             hero fits 1280×800 fold. Page height now 8–10 screens at every
 *             width (was 13.4 on a phone, 13.8 on a 960px tablet).
 *             Rails scroll inside their own overflow container, so the page
 *             itself never scrolls sideways.
 *   honest:   pass (46) — zero quantitative claims on the page
 *   tokens:   pass (48) · chrome: pass (47) · icons: pass (30)
 *   nav/foot: pass (42–45) — N9 + Ft4, neither is the AI default shape
 *   gate 23:  accent footprint ~0.01% of viewport (set lines only) — well
 *             under the 5% ceiling despite now being visible at rest
 *   gate 27:  both motion primitives sit inside
 *             prefers-reduced-motion: no-preference
 *   gate 29:  grain is monochrome, static, 0.045 — the documented E7 recipe,
 *             not an aurora blob
 *   mobile v2: --reverse's order swap and --bleed's negative margin both
 *             stand down below 60rem; without that the reversed section put
 *             its photographs above its own heading on a phone (measured,
 *             then fixed)
 *   gate 56:  trivial — page nav is not sticky, .stack__pane is the only
 *             sticky element, so no two-sticky bleed is possible
 *   gate 39:  n/a — no inputs on this page
 */

/* ── Reset ─────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  /* clip, never hidden — hidden kills position:sticky in the stack */
  overflow-x: clip;
}

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

img { max-width: 100%; height: auto; display: block; }

body {
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-body);
  /* 400 on light paper. The 300 here was compensating for light-on-dark
     optical weight; on a pale ground it reads thin and washed out. */
  font-weight: 400;
  font-size: var(--text-base);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-style: normal;           /* roman display, always */
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: -0.025em;
  margin: 0;
  /* long words in a display size must break rather than push the page wide */
  overflow-wrap: anywhere;
  min-width: 0;
}

p { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
dl, dd { margin: 0; }

/* ── Material ──────────────────────────────────────────────────────────
   A fixed grain plate over the whole page. Flat colour fields were the
   single biggest reason the first pass read as plain — stone has tooth,
   and a few percent of noise gives the dark surface something to catch.
   Fixed, not scrolling, so it behaves like paper stock rather than an
   element. Inert to the pointer and hidden from assistive tech.          */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── The set line ──────────────────────────────────────────────────────
   A short accent rule above every major heading. This is the one place
   the accent appears at rest, and it is motivated rather than decorative:
   a tile setter strikes a layout line before anything is bonded, which is
   what the hero copy is about. Roughly 0.2 % of the viewport.            */
.setline { display: block; margin-block-end: var(--space-md); }
.setline::before {
  content: "";
  display: block;
  width: 2.5rem;
  height: var(--rule-thick);
  background: var(--color-accent);
}

/* ── Focus — instant, never transitioned ───────────────────────────── */
:focus-visible {
  outline: var(--rule-thick) solid var(--color-focus);
  outline-offset: 3px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: var(--space-sm);
  z-index: var(--z-top);
  padding: var(--space-2xs) var(--space-sm);
  background: var(--color-paper-3);
  color: var(--color-ink);
  text-decoration: none;
}
.skip:focus { left: var(--space-sm); }

/* ── Shared: typographic link ──────────────────────────────────────── */
.link {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2xs);
  color: var(--color-ink);
  font-size: var(--text-base);
  text-decoration: none;
  white-space: nowrap;                 /* clickable text never wraps */
  border-bottom: var(--rule-hair) solid var(--color-rule);
  padding-block-end: var(--space-3xs);
  transition: border-color var(--dur-short) var(--ease-out),
              color var(--dur-short) var(--ease-out);
}
.link:hover { color: var(--color-accent); border-bottom-color: var(--color-accent); }
.link:active { color: var(--color-muted); border-bottom-color: var(--color-muted); }
.link__arrow { transition: transform var(--dur-short) var(--ease-out); }
.link:hover .link__arrow { transform: translateX(3px); }

.link--lg {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-variant-numeric: tabular-nums;
  padding-block-end: var(--space-3xs);
}

/* ── Shared: outlined chip ─────────────────────────────────────────── */
.chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  min-height: 44px;                    /* AA hit target at every width */
  padding: var(--space-2xs) var(--space-md);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-none);
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 400;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  transition: border-color var(--dur-short) var(--ease-out),
              color var(--dur-short) var(--ease-out),
              background-color var(--dur-short) var(--ease-out);
}
.chip:hover { border-color: var(--color-accent); color: var(--color-accent); }
.chip:active { background: var(--color-paper-2); }
/* Opaque, because it crosses the hero photograph. The top scrim alone left
   checkerboard reading through the chip; a blurred backdrop would have been
   glassmorphism, which this genre doesn't do. A solid chip is just an object
   sitting on the picture. */
.chip--nav { background: var(--color-paper); }
.chip--primary { border-color: var(--color-ink); }
.chip--primary:hover { border-color: var(--color-accent); }
.chip__arrow { transition: transform var(--dur-short) var(--ease-out); }
.chip:hover .chip__arrow { transform: translateX(3px); }

/* ── Shared: caption ───────────────────────────────────────────────── */
.caption {
  margin-block-start: var(--space-xs);
  color: var(--color-neutral);
  font-size: var(--text-sm);
  letter-spacing: 0.04em;
  line-height: 1.5;
}

/* ── Nav — edge-aligned, nothing between ───────────────────────────────
   In normal flow by default. It only lifts out and floats on the page
   that actually has a hero behind it — on the interior pages there is no
   photograph to sit over, and an absolute bar there would hang above the
   page head instead of pushing it down.                                  */
.nav {
  position: relative;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-lg) var(--page-inline);
}

/* Home only: the hero starts at the top of the viewport and runs its image
   behind the bar. The hero's top scrim keeps the wordmark and phone legible
   where they cross the photograph. */
body:has(.hero) .nav {
  position: absolute;
  inset-inline: 0;
  inset-block-start: 0;
}

.wordmark { text-decoration: none; color: var(--color-ink); display: block; }
.wordmark__name {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1;
}
.wordmark__kind {
  display: block;
  margin-block-start: var(--space-3xs);
  color: var(--color-neutral);
  font-size: var(--text-xs);
  font-weight: 400;
  letter-spacing: 0.24em;
  text-transform: uppercase;
}

/* ── H6 · Hero — full-height, image bleeding off the right ─────────────
   Replaces the v2 diptych, which was a modest photo in a modest column.
   The image column now runs the full height of the viewport and off the
   right edge entirely — the page's one moment of real scale. Text stays
   hard left and clear of the photograph: the checkerboard is far too busy
   to carry type over it, so the drama comes from bleed and size, not from
   overlapping something unreadable.                                      */
.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding-block: var(--space-4xl) var(--space-3xl);
  padding-inline: var(--page-inline);
  overflow: clip;      /* contains the parallax drift, never the page */
}

.hero__text {
  position: relative;
  z-index: var(--z-raised);
  width: min(100%, var(--page-max));
  margin-inline: auto;
}

.hero__figure {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  width: clamp(16rem, 42vw, 44rem);
  margin: 0;
  overflow: hidden;
  z-index: var(--z-base);
}
.hero__figure img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Two scrims, both doing real work rather than decoration: the top one
   keeps the nav legible where it crosses the photograph, the side one
   lets the image dissolve into the page instead of butting against the
   text in a hard vertical seam. */
.hero__figure::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom, var(--color-paper) 0%, transparent 22%),
    linear-gradient(to right, var(--color-paper) 0%, transparent 34%);
}

.hero__display {
  font-size: var(--text-display);
  font-weight: 300;
  max-width: 11ch;
}

.hero__lede {
  margin-block-start: var(--space-lg);
  max-width: 38ch;
  color: var(--color-muted);
  font-size: var(--text-md);
  line-height: 1.55;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
  margin-block-start: var(--space-xl);
}

/* Lives in the text column, not on the photograph. Over the image it sat on
   bright checkers and no amount of text-shadow made 12.8px type read there —
   a scrim strong enough to fix it would have killed the picture. */
.hero__caption {
  margin-block-start: var(--space-xl);
  max-width: 34ch;
  color: var(--color-neutral);
  font-size: var(--text-sm);
  letter-spacing: 0.04em;
  line-height: 1.5;
}

/* ── F2 · Sticky-scroll stack ──────────────────────────────────────── */
.work {
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--page-inline);
  border-block-start: var(--rule-hair) solid var(--color-rule);
}

.stack {
  display: grid;
  /* minmax(0,…) on the image track or the photos force the page wide */
  grid-template-columns: 4fr minmax(0, 6fr);
  gap: clamp(var(--space-lg), 6vw, var(--space-3xl));
  padding-block: var(--space-3xl);
  border-block-end: var(--rule-hair) solid var(--color-rule);
}
.stack:last-child { border-block-end: 0; }

.stack__pane {
  position: sticky;
  top: var(--space-2xl);
  align-self: start;
  z-index: var(--z-sticky);
}

.stack__title {
  font-size: var(--text-section);
  font-weight: 300;
}

/* ── Structural variation ──────────────────────────────────────────────
   The first pass ran four identical stacks, so by the second section you
   had seen the whole page. Each stack now composes differently: the
   default sits pane-left, --reverse flips it, and --bleed pushes its lead
   plate past the container edge. The pane copy and rhythm stay constant
   so the variation reads as composition, not inconsistency.              */
.stack--reverse { grid-template-columns: minmax(0, 6fr) 4fr; }
.stack--reverse .stack__pane  { order: 2; }
.stack--reverse .stack__plates { order: 1; }

/* Lead plate escapes the container on one side — the page's widest moment */
.stack--bleed .plate--lead { margin-inline-end: calc(var(--page-inline) * -1); }
.stack--bleed .plate--lead img { aspect-ratio: 16 / 9; }

.stack__body {
  margin-block-start: var(--space-md);
  max-width: 46ch;          /* 45ch floor — under it the measure reads choppy */
  color: var(--color-muted);
  line-height: 1.65;
}

.spec {
  margin-block-start: var(--space-lg);
  border-block-start: var(--rule-hair) solid var(--color-rule);
}
.spec li {
  padding-block: var(--space-xs);
  border-block-end: var(--rule-hair) solid var(--color-rule);
  color: var(--color-neutral);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* Plates are fixed windows onto the photograph, not the whole frame. Left at
   their native 3:4 the portrait phone shots stack to a 15,000px page and each
   one fills a screen on its own; cropping to a set ratio gives the pane a
   rhythm and lets the lead image carry the hierarchy. */
.stack__plates {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-md);
  align-content: start;
}

.plate { margin: 0; background: var(--color-paper-2); overflow: hidden; }

.plate img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: center;
}

.plate--lead { grid-column: 1 / -1; }
.plate--lead img { aspect-ratio: 4 / 3; }

/* An even-numbered final plate would sit alone in the left column with a
   hole beside it — span it instead. Sets of three already fill their row. */
.stack__plates > .plate:last-child:nth-child(even) { grid-column: 1 / -1; }
.stack__plates > .plate:last-child:nth-child(even) img { aspect-ratio: 3 / 2; }

/* ── Statement band ────────────────────────────────────────────────────
   One typographic moment, on a raised surface. Breaks the four-stack
   run and gives the page somewhere to breathe between the work and the
   spec sheet. Not a quote and not attributed — there are no real
   testimonials, so this is the brand speaking in its own voice.          */
.statement {
  background: var(--color-paper-2);
  border-block: var(--rule-hair) solid var(--color-rule);
}
.statement__inner {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-3xl) var(--page-inline);
}
.statement__text {
  max-width: 20ch;
  font-family: var(--font-display);
  font-size: var(--text-statement);
  font-weight: 300;
  line-height: 1.1;
  letter-spacing: -0.025em;
  overflow-wrap: anywhere;
  min-width: 0;
}
.statement__note {
  margin-block-start: var(--space-lg);
  max-width: 52ch;
  color: var(--color-muted);
  line-height: 1.65;
}

/* ── F3 · Services as a spec sheet ─────────────────────────────────── */
.sheet {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-3xl) var(--page-inline) var(--space-2xl);
}

.sheet__title {
  font-size: var(--text-section);
  font-weight: 300;
  margin-block-end: var(--space-xl);
}

.sheet__list { border-block-start: var(--rule-hair) solid var(--color-rule); }

.row {
  display: grid;
  grid-template-columns: 4fr 8fr;
  gap: var(--space-md);
  padding-block: var(--space-md);
  border-block-end: var(--rule-hair) solid var(--color-rule);
}

.row__key {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 400;
  letter-spacing: -0.01em;
}

.row__val {
  max-width: var(--measure);
  color: var(--color-muted);
  line-height: 1.6;
}

/* ── C3 · Closing CTA ──────────────────────────────────────────────── */
.close {
  display: grid;
  grid-template-columns: 7fr minmax(0, 4fr);
  gap: clamp(var(--space-lg), 6vw, var(--space-3xl));
  align-items: center;
  max-width: var(--page-max);
  margin-inline: auto;
  /* deliberately tighter on top than the sheet above it — sections do not
     all share one padding value */
  padding: var(--space-2xl) var(--page-inline) var(--space-4xl);
}

.close__display {
  font-size: var(--text-display-s);
  font-weight: 300;
  max-width: 16ch;
}

.close__body {
  margin-block-start: var(--space-lg);
  max-width: 48ch;
  color: var(--color-muted);
  font-size: var(--text-md);
  line-height: 1.6;
}

.close__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-md);
  margin-block-start: var(--space-xl);
}

.close__figure { margin: 0; background: var(--color-paper-2); }

/* ── Ft4 · Dense colophon ──────────────────────────────────────────── */
.colophon {
  border-block-start: var(--rule-hair) solid var(--color-rule);
  background: var(--color-paper-2);
}

.colophon__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-xl);
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-2xl) var(--page-inline) var(--space-xl);
}

.colophon__block p {
  color: var(--color-neutral);
  font-size: var(--text-sm);
  line-height: 1.65;
}

.colophon__head {
  margin-block-end: var(--space-2xs);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.colophon__block a {
  color: var(--color-muted);
  text-decoration: none;
  white-space: nowrap;
  border-bottom: var(--rule-hair) solid var(--color-rule);
  transition: color var(--dur-short) var(--ease-out),
              border-color var(--dur-short) var(--ease-out);
}
.colophon__block a:hover { color: var(--color-accent); border-bottom-color: var(--color-accent); }
.colophon__hours { font-variant-numeric: tabular-nums; }

.colophon__tail {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-md) var(--page-inline) var(--space-xl);
  border-block-start: var(--rule-hair) solid var(--color-rule);
  color: var(--color-neutral);
  font-size: var(--text-sm);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* ── Motion ────────────────────────────────────────────────────────────
   Scroll-driven, CSS-only, via view-timeline — no JS and no observer.
   Everything is fully visible by default and the animation only exists
   inside the @supports block, so a browser without view-timeline shows a
   finished page rather than blank elements waiting for a script.

   One primitive, used once per element: a short rise-and-settle as a
   section enters. Plates get a slower, smaller version so the images feel
   weighted rather than springy. Nothing repeats, nothing loops.          */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    @keyframes settle {
      from { opacity: 0; transform: translateY(var(--rise, 18px)); }
      to   { opacity: 1; transform: none; }
    }

    .stack__pane,
    .statement__inner,
    .sheet__title,
    .close__inner {
      animation: settle linear both;
      animation-timeline: view();
      animation-range: entry 8% cover 26%;
    }

    .plate {
      --rise: 26px;
      animation: settle linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 22%;
    }

    /* The set line draws rather than fades — it is a struck line, so it
       should behave like one. Runs slightly ahead of the heading it sits
       above, which is the whole choreography: line, then title, then body. */
    @keyframes strike { from { transform: scaleX(0); } to { transform: scaleX(1); } }
    .setline::before {
      transform-origin: left center;
      animation: strike linear both;
      animation-timeline: view();
      animation-range: entry 6% cover 18%;
    }

    /* Parallax. The frame crops; the photograph travels inside it at a
       different rate from the page. scale(1.06) is the headroom that keeps
       the drift from exposing an edge. This is why .plate and .hero__figure
       carry overflow — the movement is contained, never the page. */
    @keyframes drift {
      from { transform: translateY(-3.2%) scale(1.06); }
      to   { transform: translateY(3.2%)  scale(1.06); }
    }
    .plate img,
    .hero__figure img {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: cover;
    }
  }
}

/* ── Responsive ────────────────────────────────────────────────────── */

/* 60rem — layout breakpoint. Grids collapse, the sticky pane unsticks. */
@media (max-width: 60rem) {
  .close {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }

  /* The hero's absolute image column is a desktop composition — at phone
     width a 42vw sliver behind the text is neither legible nor dramatic.
     It returns to document flow as a full-bleed band beneath the copy, and
     the scrims come off since nothing overlaps any more. */
  .hero {
    min-height: 0;
    display: block;
    padding-block: var(--space-4xl) 0;
    overflow: visible;
  }

  .hero__figure {
    position: static;
    width: auto;
    margin-block-start: var(--space-2xl);
    margin-inline: calc(var(--page-inline) * -1);
  }
  .hero__figure img { height: auto; aspect-ratio: 4 / 5; }
  .hero__figure::after { display: none; }

  .hero__caption { margin-block-start: var(--space-lg); }

  .stack {
    grid-template-columns: minmax(0, 1fr);
    padding-block: var(--space-2xl);
  }

  .stack__pane {
    position: static;              /* sticky is hostile on touch scroll */
    top: auto;
  }

  /* The desktop variants have to stand down once everything is one column.
     --reverse's order swap would otherwise put a section's photographs
     above its own heading, and --bleed's negative margin is redundant
     because the rail already runs to both screen edges. */
  .stack--reverse .stack__pane,
  .stack--reverse .stack__plates { order: initial; }
  .stack--bleed .plate--lead { margin-inline-end: 0; }
  .stack--bleed .plate--lead img { aspect-ratio: 4 / 5; }

  /* Below the layout breakpoint the plates become a snap rail rather than a
     column. Stacked full-width portraits ran 13 screens of near-identical
     rectangles on a phone, and the 2-up grid was worse on a tablet — with the
     pane unstuck it produced 850px-wide plates and a 12,400px page. As a rail
     the heading stays put while you swipe its projects, and the section keeps
     a shape of its own instead of being a squashed desktop. */
  .stack__plates {
    display: flex;
    gap: var(--space-sm);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* full-bleed to the screen edges so it reads as a rail, not a column */
    margin-inline: calc(var(--page-inline) * -1);
    padding-inline: var(--page-inline);
    scroll-padding-inline: var(--page-inline);
  }
  .stack__plates::-webkit-scrollbar { display: none; }

  /* 78% leaves the next plate peeking — that edge is the swipe affordance,
     which is why there is no "swipe →" label anywhere. Capped so a 960px
     tablet doesn't get a 750px plate. */
  .stack__plates > .plate,
  .stack__plates > .plate--lead,
  .stack__plates > .plate:last-child:nth-child(even) {
    grid-column: auto;
    flex: 0 0 min(78%, 30rem);
    scroll-snap-align: start;
  }
  /* one ratio across the rail — mixed ratios in a flex row leave ragged tops */
  .stack__plates > .plate img,
  .stack__plates > .plate--lead img,
  .stack__plates > .plate:last-child:nth-child(even) img {
    aspect-ratio: 4 / 5;
  }

  .stack__plates:focus-visible {
    outline: var(--rule-thick) solid var(--color-focus);
    outline-offset: 2px;
  }

  .row {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-2xs);
  }

  /* Standalone links measured 29–37px tall — under the 44px touch floor.
     Applied from 60rem down so tablets (768px) are covered too, not just
     phones. The colophon's inline phone/email are left alone: they
     duplicate controls that already meet the target in the nav and the
     closing section — the WCAG 2.5.8 equivalent-control exception. */
  .link,
  .link--lg,
  .skip {
    align-items: center;
    min-height: 44px;
  }
}

/* 40rem — typography breakpoint. This is a phone; space is a luxury. */
@media (max-width: 40rem) {
  .nav { padding-block: var(--space-md); }

  .hero__display { font-size: var(--text-3xl); max-width: 100%; }
  .hero__lede { font-size: var(--text-base); }
  .hero__actions { gap: var(--space-sm); }
  .hero__actions .chip { width: 100%; }

  .stack__title { font-size: var(--text-xl); }

  .close__display { font-size: var(--text-2xl); }
  .close__body { font-size: var(--text-base); }
  .link--lg { font-size: var(--text-base); }

  .colophon__grid { gap: var(--space-lg); }
}

/* ── Reduced motion ────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  /* the sticky pin is spatial motion — release it */
  .stack__pane { position: static; }
}

/* ══════════════════════════════════════════════════════════════════════
   Multi-page — nav links, interior page heads, gallery, services, contact
   ══════════════════════════════════════════════════════════════════════ */

/* ── Nav links ─────────────────────────────────────────────────────────
   The bar keeps its edge-aligned discipline now that it carries five
   destinations: wordmark hard left, everything else hard right, no
   border-bottom and no centred link cluster. Current page is marked with
   the accent rule rather than a filled pill, so the nav uses the same set
   line as every heading on the site. */
.nav__end {
  display: flex;
  align-items: center;
  gap: clamp(var(--space-md), 3vw, var(--space-xl));
}

.nav__links {
  display: flex;
  align-items: center;
  gap: clamp(var(--space-sm), 2vw, var(--space-lg));
}

.nav__links a {
  position: relative;
  color: var(--color-muted);
  font-size: var(--text-sm);
  letter-spacing: 0.06em;
  text-decoration: none;
  white-space: nowrap;
  padding-block: var(--space-2xs);
  transition: color var(--dur-short) var(--ease-out);
}
.nav__links a:hover { color: var(--color-ink); }

.nav__links a[aria-current="page"] { color: var(--color-ink); }
.nav__links a[aria-current="page"]::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  height: var(--rule-thick);
  background: var(--color-accent);
}

/* ── Interior page head ────────────────────────────────────────────────
   Deliberately not the home page's 100svh photograph. Repeating that on
   every page would flatten the hierarchy between them — the hero is the
   home page's move and nowhere else's. */
.pagehead {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-4xl) var(--page-inline) var(--space-2xl);
}

.pagehead__title {
  font-size: var(--text-display-s);
  font-weight: 300;
  max-width: 16ch;
}

.pagehead__lede {
  margin-block-start: var(--space-lg);
  max-width: 52ch;
  color: var(--color-muted);
  font-size: var(--text-md);
  line-height: 1.55;
}

/* ── Gallery (work) ────────────────────────────────────────────────────
   Portrait phone photographs, so the grid is auto-fill on a portrait
   minimum rather than a fixed column count — the archive decides the
   rhythm instead of the breakpoint. */
.gallery {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-2xl) var(--page-inline);
  border-block-start: var(--rule-hair) solid var(--color-rule);
}

.gallery__head { margin-block-end: var(--space-xl); }
.gallery__title { font-size: var(--text-section); font-weight: 300; }
.gallery__sub {
  margin-block-start: var(--space-2xs);
  color: var(--color-muted);
  max-width: 46ch;
}

.gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
  gap: var(--space-md);
}
.gallery__grid .plate { background: var(--color-paper-2); overflow: hidden; }
.gallery__grid .plate img { aspect-ratio: 4 / 5; object-fit: cover; width: 100%; }

.plate__caption {
  padding: var(--space-xs) var(--space-sm) var(--space-sm);
  color: var(--color-neutral);
  font-size: var(--text-sm);
  line-height: 1.45;
}
/* On the home page the plates are a rail; captions there would fight the
   sticky pane's copy for the same attention. */
.stack__plates .plate__caption { display: none; }

/* ── Services ──────────────────────────────────────────────────────────
   Each service is a two-column record: the name and its one-line summary
   at left, the detail at right. Same bones as the home page's spec sheet
   so the two pages read as one system. */
.jump {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: 0 var(--page-inline) var(--space-xl);
}
.jump ul { display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-md); }
.jump a {
  color: var(--color-muted);
  font-size: var(--text-sm);
  letter-spacing: 0.04em;
  text-decoration: none;
  white-space: nowrap;
  border-bottom: var(--rule-hair) solid var(--color-rule);
  padding-block-end: var(--space-3xs);
  transition: color var(--dur-short) var(--ease-out),
              border-color var(--dur-short) var(--ease-out);
}
.jump a:hover { color: var(--color-accent); border-bottom-color: var(--color-accent); }

.service {
  display: grid;
  grid-template-columns: 4fr minmax(0, 6fr);
  gap: clamp(var(--space-lg), 5vw, var(--space-3xl));
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-2xl) var(--page-inline);
  border-block-start: var(--rule-hair) solid var(--color-rule);
  scroll-margin-block-start: var(--space-2xl);
}

.service__title { font-size: var(--text-xl); font-weight: 300; }
.service__lede {
  margin-block-start: var(--space-sm);
  max-width: 34ch;
  color: var(--color-muted);
  line-height: 1.6;
}
.service__body p { max-width: 58ch; color: var(--color-ink); line-height: 1.7; }
.service__body .spec { margin-block-start: var(--space-lg); }

/* ── Areas ─────────────────────────────────────────────────────────── */
.sheet__note {
  margin-block-start: var(--space-lg);
  max-width: 58ch;
  color: var(--color-muted);
  line-height: 1.65;
}

/* ── Contact ───────────────────────────────────────────────────────── */
.contact {
  display: grid;
  grid-template-columns: 5fr minmax(0, 5fr);
  gap: clamp(var(--space-lg), 6vw, var(--space-3xl));
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-xl) var(--page-inline) var(--space-3xl);
  border-block-start: var(--rule-hair) solid var(--color-rule);
}

.contact__head {
  font-size: var(--text-lg);
  font-weight: 400;
  font-family: var(--font-body);
  letter-spacing: 0.02em;
}
.contact__line { margin-block-start: var(--space-md); }
.contact__hours,
.contact__note {
  margin-block-start: var(--space-lg);
  max-width: 44ch;
  color: var(--color-muted);
  line-height: 1.65;
}

/* ── Statement gets an optional link ───────────────────────────────── */
.statement__inner .link { margin-block-start: var(--space-xl); }

/* ── Responsive — multi-page pieces ────────────────────────────────── */
@media (max-width: 60rem) {
  .service { grid-template-columns: minmax(0, 1fr); }
  .contact { grid-template-columns: minmax(0, 1fr); }
  .pagehead { padding-block-start: var(--space-3xl); }

  /* 44px floor from the layout breakpoint down, so tablets get it too.
     At the desktop padding these measured 36px, and primary navigation is
     the last thing that should be a fiddly target on a touch screen. */
  .nav__links a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
}

@media (max-width: 40rem) {
  /* Wordmark and phone hold the top row; the links drop to their own row
     underneath. No hamburger, because a hamburger needs JavaScript and
     five short labels fit on one line at 320px. */
  .nav { flex-wrap: wrap; row-gap: var(--space-sm); }
  .nav__end { width: 100%; flex-direction: column-reverse; align-items: flex-start; gap: var(--space-sm); }
  .nav__links { width: 100%; justify-content: space-between; gap: var(--space-2xs); }
  .nav__links a { font-size: var(--text-xs); letter-spacing: 0.04em; }
  .chip--nav { width: 100%; }

  .pagehead__title { font-size: var(--text-2xl); }
  .pagehead__lede { font-size: var(--text-base); }
  .gallery__grid { grid-template-columns: minmax(0, 1fr); }
}


/* ── Method steps (about) ──────────────────────────────────────────────
   F4 step sequence. Numerals are normally an AI tell and eyebrows are off
   by default across this site — they earn their place here because the
   content is genuinely ordinal: this is the order the work happens in, and
   step 03 cannot precede step 02 on a real job. Number sits ABOVE its
   heading in the same column, never beside it. */
.method {
  max-width: var(--page-max);
  margin-inline: auto;
}

.step {
  display: grid;
  grid-template-columns: 4fr minmax(0, 6fr);
  gap: clamp(var(--space-lg), 5vw, var(--space-3xl));
  padding: var(--space-2xl) var(--page-inline);
  border-block-start: var(--rule-hair) solid var(--color-rule);
}

.step__num {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 300;
  font-variant-numeric: tabular-nums;
  color: var(--color-accent-dim);
  line-height: 1;
  margin-block-end: var(--space-2xs);
}

.step__title {
  font-size: var(--text-lg);
  font-weight: 300;
  max-width: 18ch;
}

.step__body p {
  max-width: 58ch;
  color: var(--color-ink);
  line-height: 1.75;
}

@media (max-width: 60rem) {
  .step { grid-template-columns: minmax(0, 1fr); }
}

/* ── Material study (video) ────────────────────────────────────────────
   Full-bleed band. The video is the client's own photographs in motion —
   see images/make-video.py. object-fit keeps it filling the band at any
   aspect, and the fixed height stops a 16:9 file dictating how much of a
   phone screen it eats. */
.material {
  margin-block: var(--space-2xl);
  border-block: var(--rule-hair) solid var(--color-rule);
  background: var(--color-paper-2);
}

.material__head {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-xl) var(--page-inline) var(--space-md);
  font-size: var(--text-xl);
  font-weight: 300;
}

.material__video {
  display: block;
  width: 100%;
  height: clamp(15rem, 46vh, 30rem);
  object-fit: cover;
  background: var(--color-paper-3);
}

.material__caption {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-sm) var(--page-inline) var(--space-xl);
  color: var(--color-neutral);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* With reduced motion the script swaps autoplay for controls — give them
   room to sit against the frame rather than on top of the tile. */
@media (prefers-reduced-motion: reduce) {
  .material__video { height: auto; aspect-ratio: 16 / 9; }
}

/* ── Detail grid ───────────────────────────────────────────────────────
   Tight crops from the same 14 photographs — the drains, niches, bench
   folds and pattern junctions the wide shots throw away. Small tiles on
   purpose: these are 300–700px windows and do not survive being scaled up. */
.gallery--detail { background: var(--color-paper-2); }

.detail__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
  gap: var(--space-md);
}

.detail {
  margin: 0;
  background: var(--color-paper-3);
  overflow: hidden;
}
.detail img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}
.detail__caption {
  padding: var(--space-xs) var(--space-sm) var(--space-sm);
  color: var(--color-neutral);
  font-size: var(--text-sm);
  line-height: 1.45;
}

/* Service illustration — only on the three services we actually have a
   photograph of. The other four render text-only rather than borrow an
   image of different work. */
.service__figure {
  margin: var(--space-lg) 0 0;
  background: var(--color-paper-2);
  overflow: hidden;
}
.service__figure img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

/* ── Detail strip ──────────────────────────────────────────────────────
   Three details in a band, for pages that are otherwise all type. Three
   up on desktop, two on tablet, one on a phone — never a squeeze. */
.strip {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-2xl) var(--page-inline);
  border-block-start: var(--rule-hair) solid var(--color-rule);
}
.strip__head { margin-block-end: var(--space-lg); }
.strip__title { font-size: var(--text-xl); font-weight: 300; }

.strip__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-md);
}
.strip__grid .detail { background: var(--color-paper-2); }

@media (max-width: 60rem) {
  .strip__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 40rem) {
  .strip__grid { grid-template-columns: minmax(0, 1fr); }
}

/* FAQ — homepage, sits between the statement and the estimate ask.
   Two columns on wide screens so the question reads as a heading against its
   answer rather than above it; stacked below 60rem. Hairline rule per item,
   matching the rest of the page's structure. */
.faq {
  max-width: var(--page-max);
  margin-inline: auto;
  padding: var(--space-3xl) var(--page-inline) var(--space-2xl);
}
.faq__title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: 300;
  letter-spacing: -0.02em;
  margin-block-end: var(--space-xl);
}
.faq__item {
  display: grid;
  grid-template-columns: 5fr minmax(0, 7fr);
  gap: clamp(var(--space-md), 4vw, var(--space-2xl));
  padding-block: var(--space-lg);
  border-block-start: var(--rule-hair) solid var(--color-rule);
}
.faq__item:last-child { border-block-end: var(--rule-hair) solid var(--color-rule); }
.faq__q {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: 1.25;
  margin: 0;
  text-wrap: balance;
}
.faq__a {
  margin: 0;
  max-width: var(--measure);
  color: var(--color-neutral);
  line-height: 1.6;
}
@media (max-width: 60rem) {
  .faq__item { grid-template-columns: 1fr; gap: var(--space-2xs); }
}
