/* ============================================================
   Donato Architecture Studio — shared design system
   ----------------------------------------------------------
   Shared across every page: palette, typography, foundations,
   header, footer, the small repeating utilities (label, reveal,
   text-link). Page-specific layout rules live inline in each
   page's <head> <style> block.

   Design language (rebuilt — Prompt 14): typewriter / blueprint.
   Pure black on Donato's brand drawing-paper grey, with a slate-
   blue accent. IBM Plex Mono for display, IBM Plex Sans for body.
   ============================================================ */

/* ============================================================
   SELF-HOSTED WEBFONTS (Prompt 316)
   ----------------------------------------------------------
   Was fonts.googleapis.com. Self-hosted per this repo's vendor/
   rule ("self-hosted only, no CDN references in production") and
   to drop an 807ms render-blocking cross-origin round trip.
   Descriptors below are copied from Google's own served CSS
   (css2?family=IBM+Plex+Mono:wght@300;400;500;700&family=IBM+Plex+Sans:wght@300;400;500),
   latin subset, INCLUDING unicode-range — do not simplify it:
   the range is what keeps a glyph outside it (e.g. U+2192 "→")
   falling through to the system font exactly as it does today.

   Weights are the ones that actually RENDER, measured in-browser,
   not grepped: Mono 300/400/500/700, Sans 300/400/500. Mono 700
   is used by exactly one element (.register__title on 404.html,
   an <h2> with no font-weight, so it takes the UA default bold).
   600 is genuinely unused on both families and is not shipped.

   IBM Plex Sans is a VARIABLE font: all three weights resolve to
   one file, so the three rules below are not a mistake.
   IBM Plex is licensed OFL-1.1 — self-hosting is permitted.
   ============================================================ */

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../vendor/fonts/ibm-plex-mono-300.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../vendor/fonts/ibm-plex-mono-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../vendor/fonts/ibm-plex-mono-500.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../vendor/fonts/ibm-plex-mono-700.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 300;
  font-stretch: 100%;
  font-display: swap;
  src: url('../vendor/fonts/ibm-plex-sans-var.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url('../vendor/fonts/ibm-plex-sans-var.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url('../vendor/fonts/ibm-plex-sans-var.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
                 U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
                 U+2215, U+FEFF, U+FFFD;
}

/* TOKENS */
:root {
  /* Color palette — sampled from Donato's existing brand */
  --ink: #000000;          /* pure black for primary text and high-contrast moments */
  --paper: #D8D8D8;        /* their actual brand grey — drawing-paper grey */
  --paper-light: #E8E8E8;  /* derived — slightly lighter for content sections */
  --paper-soft: #F2F2F2;   /* derived — even lighter for breathing zones */
  --slate: #8191A5;        /* drawing-paper blueprint accent — value calibrated
                              to live donatoarchitecturestudio.com rendering
                              (Audit N + Prompt 66; prior #8091A0 was a tiny drift).
                              DECORATIVE ONLY on paper backgrounds (dots, hairline
                              accents, borders, marker glyphs ◇ →). Fails WCAG as
                              text on paper at ~2.22:1 — never use as text color on
                              paper. OK as over-photo text accent (.hero__head .amp).
                              Text-grade companion is --slate-deep. */
  --slate-deep: #3D4E62;   /* TEXT-grade blueprint slate. 6.04:1 on paper (#D8D8D8),
                              6.87:1 on paper-light. Use for sheet codes, eyebrows,
                              tracked-caps labels, accent text — anywhere a darker
                              blueprint tone is wanted that must remain readable.
                              Darkened from prior #5C7185 (3.36:1) — Prompt 34
                              contrast remediation. */
  --graphite: #5A5A5A;     /* mid-grey for secondary text */
  --rule: #B8B8B8;         /* hairline rules and dividers */
  --white: #FFFFFF;        /* breathing white where needed */

  /* Typography */
  --display: 'IBM Plex Mono', 'Courier New', monospace;
  --sans: 'IBM Plex Sans', 'Helvetica Neue', Arial, sans-serif;

  /* Type-scale tokens (Prompt 134 — Audit X Phase 1, safe consolidation).
     Collapses near-duplicate font sizes to one source of truth so drift
     can't reappear. Two bug fixes are baked in here: --fs-section-title
     unifies the .section-head .title that rendered ~52px on some pages
     and ~44px on others; --fs-onward unifies .onward__title (56 vs 48).
     Judgment-call merges (the 10/11/12 mono-caps label ladder, the hero
     display cap, the 16→17 body-prose steps) are deferred to Phase 2. */
  --fs-body:          17px;                     /* base body prose */
  --fs-body-fluid:    clamp(16px, 1.3vw, 19px); /* body that scales up on wide viewports */
  --fs-small:         15px;                     /* small secondary body */
  --fs-detail:        13px;                     /* supporting detail in dense list rows (Services type register + capabilities); sized at or below the compact labels it sits under (Prompt 154) */
  --fs-lede:          clamp(20px, 2.3vw, 30px); /* intro ledes */
  --fs-section-title: clamp(26px, 3.6vw, 44px); /* .section-head .title (canonical) */
  --fs-onward:        clamp(32px, 4.4vw, 56px); /* .onward__title (canonical) */

  /* Layout */
  --gut: clamp(20px, 4vw, 56px);
  --maxw: 1440px;
  --section: clamp(80px, 11vw, 160px);
}

/* FOUNDATIONS */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* Prompt 284 — `clip`, not `hidden`. Both crop horizontal overflow
   identically, but `hidden` makes the root a scroll container, which
   silently kills every `position: sticky` on the site (privacy.html's
   TOC shipped broken from the start; contact.html:144 documents the
   same trap). `clip` crops without establishing a scroll container.
   Safe pairing: overflow-y stays at its initial `visible`, and the
   two-value rule only degrades `clip` to `hidden` when the other axis
   is neither visible nor clip. The one shorthand override on body,
   `body.menu-open { overflow: hidden }`, is scoped inside
   @media (max-width: 900px), where .toc is position: static, so the
   mobile scroll lock and the desktop sticky never coexist. */
html, body { overflow-x: clip; max-width: 100%; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--fs-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  touch-action: pan-y;
}
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; border: 0; background: transparent; color: inherit; }

/* SELECTION */
::selection { background: var(--ink); color: var(--paper); }

/* ============================================================
   FOCUS (Prompt 283) — sitewide keyboard focus ring.
   Before this rule the site had NO global focus style, so every
   keyboard user got Chrome's default rgb(0,95,204) blue, a color
   absent from the palette. slate-deep is the system's designated
   interactive token (hover / active / form feedback), and three
   page-local rules (contact .inquiry-start + .radio, projects
   .prow__photo) had already converged on exactly
   `2px solid var(--slate-deep)` — this promotes that convention
   to the cascade rather than inventing a new one.

   Deliberately NOT overridden: components that already declare
   their own designed focus treatment at higher specificity keep
   it (contact/services .field__* set `outline: none` plus an ink
   border and slate-deep inset underline; PhotoSwipe's donato
   buttons set `outline: 0` plus a fill swap). Offset 3px puts the
   ring OUTSIDE ink-filled buttons, so it lands on paper and reads.
   Only genuinely dark SURFACES need the light-ring override.
   ============================================================ */
:focus-visible {
  outline: 2px solid var(--slate-deep);
  outline-offset: 3px;
}
/* Dark surfaces — ring switches to paper so it reads on ink. */
.cookie-banner :focus-visible,
.pswp :focus-visible {
  outline-color: var(--paper-soft);
}

/* SHELL */
.shell {
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding-left: var(--gut);
  padding-right: var(--gut);
}

/* LABEL — tracked-out monospace caps */
.label {
  font-family: var(--display);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--graphite);
}

/* TEXT LINK — quiet underline that resolves on hover.
   Prompt 67 — min-height: 44px brings the tap target to Apple HIG
   minimum without changing visual weight (existing padding + font
   stay; flex-centering keeps text optically positioned). */
.text-link {
  font-family: var(--display);
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 4px 0;
  position: relative;
  min-height: 44px;
}
.text-link::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: var(--ink);
  transform-origin: left;
  transition: transform 480ms cubic-bezier(.2,.7,.2,1), background 480ms ease;
}
.text-link:hover::after { transform: scaleX(1.04); background: var(--slate-deep); }
.text-link:hover { color: var(--slate-deep); }
.text-link .arrow { display: inline-block; transition: transform 480ms cubic-bezier(.2,.7,.2,1); }
.text-link:hover .arrow { transform: translateX(4px); }

/* Page hero counter — the "Sheet X / Label" line atop each main page hero.
   Consolidated from 6 page-local copies + 3 project-page copies (Audit R,
   Prompt 81). NOT the per-section sheet codes (.section-head .num/.label —
   different component, queued for a separate future audit). */
.page-hero__counter {
  font-family: var(--display);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--graphite);
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 0 0 22px 0;
}
.page-hero__counter .me { color: var(--ink); font-weight: 500; }
/* Two-tier page header (Prompt 139) — big page name + slate dot above the
   location line and the section rule. Shared by services / projects / contact
   / about so the four in-scope pages carry one identical top-header treatment.
   Values match the former About split title: 60px desktop / 40px mobile. */
.page-hero__name {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(40px, 4.2vw, 60px);
  line-height: 0.98;
  letter-spacing: -0.015em;
  margin: 0 0 12px 0;
  color: var(--ink);
}
.page-hero__name .dot {
  display: inline-block;
  width: 9px;
  height: 9px;
  background: var(--slate);
  border-radius: 50%;
  transform: translateY(-2px);
  margin-left: 3px;
}

/* Two-tier page header, lower tier — the "San Francisco · Bay Area"
   location line under .page-hero__name. Consolidated in Prompt 307
   (Audit AD, AD.08) from five page-local names for one component:
   .w-hero__sub, .p-hero__sub, .c-hero__sub, .svc-hero__sub and
   .ab-tophead__sub, all six declarations identical.

   The .key separator colour had ALREADY drifted: graphite on
   projects/services/about, --rule on privacy/contact. Graphite is the
   default here because it is the majority; privacy.html and
   contact.html keep a one-line page-local override so this
   consolidation changes no rendered pixel. Picking one for both is a
   design decision, not a refactor, and is left to be made deliberately. */
.page-hero__sub {
  font-family: var(--display);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--graphite);
  margin: 0;
}
.page-hero__sub .key { color: var(--graphite); margin: 0 8px; }
.page-hero__loc { white-space: nowrap; }

/* Editorial pull-quote — mono-italic statement + dashed attribution.
   Consolidated from index.html + 404.html (Audit U, Prompt 88). max-width
   stays page-local: index desktop is unconstrained (the quote wraps the
   floated portrait inside .signature__inner), index mobile clamps to 22ch,
   and 404 uses 20ch as a standalone editorial column — layout-contextual,
   not drift. */
.signature__q {
  font-family: var(--display);
  font-style: italic;
  font-weight: 300;
  font-size: clamp(28px, 4vw, 56px);
  line-height: 1.18;
  letter-spacing: -0.008em;
  color: var(--ink);
  margin: 0 0 28px 0;
}
.signature__attr {
  font-family: var(--display);
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--graphite);
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: 14px;
}
.signature__attr::before {
  content: "";
  display: inline-block;
  width: 28px; height: 1px;
  flex-shrink: 0;
  background: var(--ink);
}

/* ============================================================
   MOTION — two coordinated behaviors. Prompt 37 #2.
   ----------------------------------------------------------
   B1. Hairlines draw on intersection (.section-head::after).
       One-shot.
   B2. Hero entrance staggered on page load via @keyframes +
       per-step animation-delay ([data-hero-step]).
   Nothing else animates. B3 used to settle the section-head
   numbers on intersection; Audit V removed the .num markup and
   Prompt 304 (Audit AD, AD.03) removed the rules it left behind.
   .reveal stays as the JS hook that adds .is-in but carries no
   generic fade — its visual effect lives in B1's rules.
   ============================================================ */

/* B2 — staggered hero entrance (page-load keyframe + delay) */
@keyframes hero-step-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
[data-hero-step] {
  opacity: 0;
  animation: hero-step-in 720ms cubic-bezier(.2,.7,.15,1) both;
}
[data-hero-step="1"] { animation-delay: 80ms; }
[data-hero-step="2"] { animation-delay: 180ms; }
[data-hero-step="3"] { animation-delay: 280ms; }
[data-hero-step="4"] { animation-delay: 380ms; }
[data-hero-step="5"] { animation-delay: 480ms; }
[data-hero-step="6"] { animation-delay: 580ms; }

/* Reduced-motion: instant, no transitions or keyframes */
@media (prefers-reduced-motion: reduce) {
  [data-hero-step] { opacity: 1; animation: none; }
}

/* ============================================================
   SECTION-HEAD — system override.
   Replaces per-page static border-top with an animated hairline
   (B1) drawn on intersection. Adds two slate-deep structural
   ornaments (Prompt 37 #4):
     - ::before : small vertical tick at the start of the hairline
                  (drafting-instrument tick mark on a drawing plan)
     - .num::after : small slate-deep dot terminator on the sheet
                  code, marking the section's "current sheet"
   The .num itself also settles in (B3) when the section gains
   .is-in (added by the IntersectionObserver in js/site.js).
   body-prefix gives this rule higher specificity than per-page
   <style> .section-head declarations so it consistently wins.
   ============================================================ */
body .section-head {
  border-top: 0;
  position: relative;
}
.section-head::before {
  content: "";
  position: absolute;
  left: 0; top: -4px;
  width: 1px; height: 9px;
  background: var(--slate-deep);
  z-index: 2;
}
.section-head::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 1px;
  background: var(--ink);
  transform: scaleX(0); transform-origin: left;
  transition: transform 900ms cubic-bezier(.2,.7,.15,1);
  z-index: 1;
}
.section-head.is-in::after { transform: scaleX(1); }

@media (prefers-reduced-motion: reduce) {
  .section-head::after { transform: scaleX(1); transition: none; }
}

/* Section head — per-section layout + typography.
   Consolidated from 9 page-local copies (Audit S, Prompt 83). Pairs with
   the body .section-head motion + decoration base above, which owns the
   ::before tick and the ::after hairline draw, both left untouched. The
   .num entrance animation and the .num::after dot terminator that used to
   live alongside them were deleted in Prompt 304 (Audit AD, AD.03): Audit V
   removed the .num markup, so they had matched nothing since.
   The static border-top the per-page copies carried was inert, because the
   body-prefixed rule above zeroes it and the visible top line is the animated
   ::after, so it is dropped.
   Defaults capture the "big" title group + the 72px margin rhythm; pages that
   differ keep a 1–2 line page-local override (compact title; tighter margin). */
.section-head {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(20px, 3vw, 48px);
  align-items: baseline;
  padding-top: 18px;
  margin-bottom: clamp(40px, 6vw, 72px);
}
.section-head .label { font-size: 12px; }
.section-head .title {
  font-family: var(--display);
  font-weight: 400;
  /* Prompt 134 — canonical section title. Was clamp(28px,4vw,52px) here
     while 5 pages overrode to clamp(26px,3.6vw,44px); unified to the
     smaller, more-used value (project pages + contact) per Audit X and
     Patrick's "reduce sizes" note. The page-local overrides were removed. */
  font-size: var(--fs-section-title);
  line-height: 1.05;
  letter-spacing: -0.005em;
  margin: 14px 0 0 0;
  max-width: 22ch;
}
@media (max-width: 700px) {
  .section-head { grid-template-columns: 1fr; }
}

/* ============================================================
   FOOTER COLOPHON — copyright line + Idea Jar badge
   ------------------------------------------------------------
   Consolidated in Prompt 307 (Audit AD, AD.09) from 9 byte-identical
   page-local copies plus a 10th written as inline style attributes on
   kansas-street.html, which no stylesheet could reach.

   MEASURED, so the next reader does not re-investigate: the mobile
   left-align below is INERT on every page. .badge-block is a flex item
   in .colophon-row and sizes to its content (198px = the anchor's own
   width at 390px), so text-align has nothing to distribute. Kansas
   missing it was therefore a real gap with no visual consequence. The
   rule is kept because it starts mattering the moment .badge-block
   stops being a content-sized flex item.
   The badge grey (#999999) is deliberately not a palette token: it is
   the vendor badge's own colour, not part of Donato's ramp. It now
   lives here once instead of ten times.
   ============================================================ */
.colophon-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
  padding: 16px 0 0 0;
  flex-wrap: wrap;
}
.colophon-row .label { color: var(--graphite); }
.badge-block { text-align: right; }
.ij-badge {
  text-decoration: none;
  /* Prompt 314 (Audit AE.03): was #999999 = 2.32:1 on --paper-light,
     failing WCAG AA. --graphite measures 5.63:1 and is already the
     colophon-row label colour, so no new token is introduced. */
  color: var(--graphite);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
}
.ij-badge img { height: 20px; width: auto; }
@media (max-width: 900px) {
  .badge-block { text-align: left; }
}

/* ============================================================
   HEADER
   ============================================================ */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  padding: 22px 0;
  transition: background 320ms ease, backdrop-filter 320ms ease, padding 320ms ease, border-color 320ms ease;
  border-bottom: 1px solid transparent;
}
.site-header.is-scrolled {
  background: rgba(216, 216, 216, 0.82);
  backdrop-filter: saturate(140%) blur(18px);
  -webkit-backdrop-filter: saturate(140%) blur(18px);
  padding: 14px 0;
  border-bottom: 1px solid var(--rule);
}
.header-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 32px;
}
.wordmark {
  font-family: var(--display);
  font-weight: 500;
  font-size: 18px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--ink);
  line-height: 1;
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
}
.wordmark .dot {
  display: inline-block;
  width: 6px; height: 6px;
  background: var(--slate);
  border-radius: 50%;
  transform: translateY(-2px);
}
.is-overlay .wordmark { color: var(--white); }
.is-overlay .wordmark .dot { background: var(--slate); }

.nav { display: flex; gap: 38px; align-items: baseline; }
.nav a {
  font-family: var(--display);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  position: relative;
  padding: 4px 0;
  transition: color 220ms ease;
}
.is-overlay .nav a { color: var(--white); }
.nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 100%;
  bottom: -2px;
  height: 1px;
  background: var(--slate);
  /* Prompt 37 #6 — drafting-instrument easing (sharper end,
     slower middle) replaces the prior generic cubic-bezier. */
  transition: right 460ms cubic-bezier(.2,.68,.12,1);
}
.nav a:hover::after { right: 0; }
.nav .cta { color: var(--slate-deep); font-weight: 500; }
.is-overlay .nav .cta { color: var(--slate); }

/* ACTIVE STATE — current page indicator. The wordmark is the
   Home indicator; the homepage marks no nav item current.
   Prompt 37 #6: active underline draws in on page load (140ms
   delay, same easing as hover) — reads as the sheet being
   annotated. Hovering a sibling does not touch this state. */
@keyframes nav-active-draw {
  from { right: 100%; }
  to   { right: 0; }
}
.nav a[aria-current="page"] {
  color: var(--slate-deep);
}
.nav a[aria-current="page"]::after {
  right: 0;
  background: var(--slate-deep);
  animation: nav-active-draw 460ms cubic-bezier(.2,.68,.12,1) 140ms both;
}
.is-overlay .nav a[aria-current="page"] {
  color: var(--slate);
}
.is-overlay .nav a[aria-current="page"]::after {
  background: var(--slate);
}
@media (prefers-reduced-motion: reduce) {
  .nav a::after { transition: none; }
  .nav a[aria-current="page"]::after { animation: none; }
}

.menu-toggle { display: none; width: 32px; height: 28px; position: relative; }
.menu-toggle span {
  position: absolute;
  left: 0; right: 0;
  height: 1px;
  background: var(--ink);
  transition: transform 320ms ease, opacity 320ms ease, top 320ms ease, background 320ms ease;
}
.menu-toggle span:nth-child(1) { top: 8px; }
.menu-toggle span:nth-child(2) { top: 16px; }
.is-overlay .menu-toggle span { background: var(--white); }
body.menu-open .menu-toggle span:nth-child(1) { top: 12px; transform: rotate(45deg); }
body.menu-open .menu-toggle span:nth-child(2) { top: 12px; transform: rotate(-45deg); }
body.menu-open.is-overlay .menu-toggle span { background: var(--ink); }
body.menu-open.is-overlay .wordmark { color: var(--ink); }
body.menu-open.is-overlay .wordmark .dot { background: var(--slate); }

@media (max-width: 900px) {
  /* z-index, not a position change: .menu-toggle is already position:
     relative but computes z-index: auto, and .nav carries an explicit
     POSITIVE z-index (90), which promotes it to a later paint layer than
     any auto-z sibling no matter the tree order. The toggle is already
     the later sibling, so DOM order cannot win this; it needs a number.
     95 is above .nav (90) inside .site-header's own stacking context and
     is unrelated to the header's z-index: 100, which orders the header
     against the page. The S8 rule (body.menu-open .cookie-banner: 80)
     is in the ROOT context against the header's 100, so it is unaffected
     and the banner stays below the nav while the menu is open. */
  .menu-toggle { display: block; z-index: 95; }
  .nav {
    position: fixed;
    inset: 0;
    background: var(--paper);
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 28px;
    z-index: 90;
    opacity: 0;
    pointer-events: none;
    transition: opacity 360ms ease;
  }
  .nav a {
    color: var(--ink) !important;
    font-size: 16px;
    letter-spacing: 0.22em;
    /* Prompt 73 — HIG tap-area on mobile menu links only (desktop
       header skipped to avoid +24px header bar growth). Fits within
       the existing column gap: 28px without reflow. */
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
  body.menu-open .nav { opacity: 1; pointer-events: auto; }
  body.menu-open { overflow: hidden; }
  /* Sweep S8 — the cookie banner (z-index 900) sits above the mobile nav
     overlay (z-index 90) and occludes the last nav item at short landscape
     viewports; 740x320 put it straight over Contact. Drop it BELOW the nav
     while the menu is open, rather than hiding it: the banner animates on
     `transform 0.4s ease` between translateY(100%) and translateY(0), and
     display:none would kill that transition and re-trigger it on close.
     Lowering z-index alone leaves the transform, the .is-visible state, and
     the consent flow completely untouched. The nav is opaque and inset:0, so
     the banner is fully covered while open and returns to normal stacking on
     close. */
  body.menu-open .cookie-banner { z-index: 80; }
  .nav a[aria-current="page"] {
    color: var(--slate-deep) !important;
  }
  /* Prompt 62 — Mobile: scrolled header is fully opaque rather than 82%
     translucent + backdrop-filter blur. iOS Safari does not reliably
     apply backdrop-filter to fixed elements, which let underlying
     content show through the header and read as a stacked-text overlap.
     Prompt 70 — backdrop-filter: none is critical, not cosmetic: with
     opaque bg the blur has nothing to filter, but the backdrop-filter
     property itself still establishes a new containing block per CSS
     spec, trapping the nested .nav's `position: fixed; inset: 0` to
     the header's bounding box (~59px) instead of the viewport. That
     collapsed the mobile menu panel and let menu items overlap page
     content. Removing the property restores full-viewport fixed
     positioning on the nav. */
  .site-header.is-scrolled {
    background: var(--paper);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* Footer nav — same active-state pattern as primary nav */
.footer-col a[aria-current="page"] {
  color: var(--slate-deep);
  font-weight: 500;
}

/* ============================================================
   FOOTER
   ============================================================ */
.site-footer {
  background: var(--paper-light);
  padding: 80px 0 24px 0;
  border-top: 1px solid var(--rule);
}
.footer-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 48px;
  align-items: start;
  padding-bottom: 56px;
  border-bottom: 1px solid var(--rule);
}
.footer-wordmark {
  font-family: var(--display);
  font-weight: 500;
  font-size: 22px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
}
.footer-wordmark .dot {
  display: inline-block;
  width: 6px; height: 6px;
  background: var(--slate);
  border-radius: 50%;
  transform: translateY(-2px);
}
.footer-tag {
  font-family: var(--sans);
  color: var(--graphite);
  font-size: var(--fs-small);
  margin: 16px 0 0 0;
  max-width: 30ch;
  line-height: 1.55;
}
.footer-col h2 {
  font-family: var(--display);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--graphite);
  font-weight: 500;
  margin: 0 0 18px 0;
}
.footer-col a, .footer-col p {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink);
  display: block;
  margin: 0 0 8px 0;
  transition: color 220ms ease;
}
/* Prompt 73 — HIG tap-area: anchors lift to 44px min-height. Prompt 242
   (Arlene's edit doc item 4): block-level flex at BASE so every footer
   column stacks one link per line — the Studio nav column included.
   Previously inline-flex with a :last-child Reach-column override
   (Prompt 80); that override is now the base rule and is removed. */
.footer-col a {
  display: flex;
  align-items: center;
  min-height: 44px;
}
.footer-col a:hover { color: var(--slate-deep); }
/* Prompt 324 — Instagram row in the Reach column. The glyph is Meta's
   official file, byte-identical in vendor/instagram/ (source + terms in
   its README.txt), painted through a CSS mask so it takes --slate, the
   wordmark dot's token, without the file being edited: Meta permits any
   SOLID colour provided nothing else about the glyph changes. Their two
   hard rules are enforced here, not chosen: never below 29x29, and clear
   space of half the glyph on every side. The gap to the handle IS that
   clear space. Vertically, the 44px row (Prompt 73) centres the 29px
   glyph with 7.5px to spare, plus the 8px row margin: 15.5px to the
   email row above. That is why the row needs no extra height and the
   column keeps its rhythm. Hover is the column's own (text colour
   only); the glyph stays slate. */
.footer-ig { --ig-glyph: 29px; gap: calc(var(--ig-glyph) / 2); }
.footer-ig__glyph {
  flex: none;
  width: var(--ig-glyph);
  height: var(--ig-glyph);
  background: var(--slate);
  -webkit-mask: url('../vendor/instagram/Instagram_Glyph_Black.svg') center / contain no-repeat;
  mask: url('../vendor/instagram/Instagram_Glyph_Black.svg') center / contain no-repeat;
}
/* Forced-colours mode drops author backgrounds, which would erase a
   mask-painted glyph; keep it visible in the system text colour. */
@media (forced-colors: active) {
  .footer-ig__glyph { forced-color-adjust: none; background: CanvasText; }
}
@media (max-width: 900px) {
  .footer-grid { grid-template-columns: 1fr; gap: 40px; }
}

/* ============================================================
   COOKIE BANNER — consent gate, ink bar at viewport bottom
   ============================================================ */
.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 900;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
  padding: 16px clamp(20px, 4vw, 48px);
  background: var(--ink);
  transform: translateY(100%);
  transition: transform 0.4s ease;
}
.cookie-banner.is-visible { transform: translateY(0); }
.cookie-banner__copy {
  margin: 0;
  flex: 1;
  min-width: 240px;
  font-family: var(--sans);
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--paper);
}
.cookie-banner__copy a {
  color: var(--white);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.cookie-banner__btn {
  font-family: var(--display);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--paper);
  border: 0;
  padding: 12px 28px;
  min-height: 44px;
  cursor: pointer;
  transition: background 0.2s ease;
}
.cookie-banner__btn:hover { background: var(--white); }
