/* ==========================================================================
   Visual stylesheet — first visual-design pass.
   Design tokens live in two places, loaded before this file:
     1. /css/tokens.css   — generated from src/_data/tokens.json (colors, type
                             scale, radii, shadows, motion — the DTCG system).
     2. /css/fonts.css    — self-hosted Roboto @font-face rules.
   Every value below is a var(--...) reference — no magic numbers, per the
   house token-conventions and acceptance criterion #12.
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  /* Critic fix: in-page anchors (#cases, #lead-on) and keyboard focus
     (e.g. tabbing to a skip-link target, or a browser's find-in-page
     jump) must land BELOW the sticky header, not underneath it — without
     this, the header (position: sticky, always on top, z-index:20)
     covers the first ~4-4.75rem of whatever anchor/target just scrolled
     to the very top of the viewport. --rail-header-height is the
     MEASURED header height (see tokens.json's header-height-estimate
     description for the full computation: padding-block + the taller of
     the brand's line-box or the mobile nav-toggle's 44px min-height) —
     the same token the rail's sticky offset already relies on, so both
     mechanisms agree on one real number instead of drifting apart. */
  scroll-padding-top: calc(var(--rail-header-height) + var(--wf-space-3));
}

body {
  margin: 0;
  font-family: var(--wf-font);
  font-size: var(--wf-text-base);
  line-height: var(--wf-text-lh);
  color: var(--color-text);
  background: var(--color-surface);
  /* Reflow insurance: long unbroken strings (long case titles, meta rows)
     must not force horizontal scroll at narrow widths (320-375px). */
  overflow-wrap: break-word;
}

/* --------------------------------------------------------------------------
   Grain (Direction 2.0 P4 §6) — a single SVG feTurbulence tile (one data
   URI, reused by every ground below), applied as a fixed, decorative,
   non-interactive texture layer per section. "Thematically perfect on a
   paper site" per the spec: a near-invisible paper-grain micro-texture,
   not a tint or a filter over content.

   P4 FIX (post-ship blocker, live-browser-verified): the FIRST version of
   this rule tried to keep the grain behind content by giving the overlay
   z-index:0 and then giving every content wrapper z-index:1 — a "the
   overlay is one layer, content is the layer above it" scheme that only
   holds if EVERY current and future descendant keeps re-asserting that
   z-index. That's exactly the "whack-a-mole z-index" failure mode: it's
   brittle by construction, and it broke in the browser (grain visually
   occluding the terracotta band's own text became reachable by
   elementFromPoint) even though the individual rules looked correct in
   isolation. Rebuilt on a guarantee that doesn't depend on chasing
   z-index down the tree:

     INVARIANT: a grain overlay carries ONLY the grain background-image,
     NEVER a background-color/background shorthand with a color — the
     ground color belongs to the section itself (body/main/.on-ink/.wf-band
     already paint their own background). And every grain overlay sits at
     z-index:-1 inside a parent that establishes its OWN stacking context
     via `isolation: isolate` — `isolation: isolate` (with no z-index
     needed on the parent) creates a new stacking context whose local
     stacking order is entirely self-contained, so z-index:-1 on the
     overlay is guaranteed to paint behind EVERYTHING ELSE inside that
     one parent, including every normal-flow (position:static) descendant,
     with no need to touch any child's position/z-index at all. This is
     the same guarantee a browser gives a `position: fixed` root stacking
     context — just scoped to one section instead of the whole page.

   WHY THIS CANNOT AFFECT ANY TEXT/BACKGROUND CONTRAST PAIR: the grain
   layer paints strictly behind its parent's real content (per the
   isolate + z-index:-1 guarantee above), so it never overlaps a glyph's
   pixels — text always renders fully opaque, at its already-verified
   color, on top of the grain. The "worst case" is therefore not a
   blended color at all: it is mathematically UNCHANGED (the grain layer
   contributes 0% to any text pixel), which is why the manifest does not
   add a "grain-affected" pair — every existing pair in this file remains
   exactly as measured with grain present. Opacity (4% on paper, 8-9% on
   the darker ink/band grounds, both within the spec's 4-6%/8-10% ranges)
   is a purely cosmetic choice with no accessibility implication once
   that stacking guarantee holds. -------------------------------------- */
main,
.wf-hero.on-ink,
footer.on-ink,
.wf-band {
  position: relative;
  isolation: isolate;
}

main::before,
.wf-hero.on-ink::before,
footer.on-ink::before,
.wf-band::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.5 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-repeat: repeat;
}

/* Opacity per ground — dark/warm grounds carry more grain before it
   becomes visible (§6); the paper ground (<main>, covering every ordinary
   light section by default) stays at the quieter end of the range. */
main::before {
  opacity: 0.04;
}

.wf-hero.on-ink::before,
footer.on-ink::before,
.wf-band::before {
  opacity: 0.09;
}

h1, h2, h3, h4 {
  font-family: var(--wf-font);
  color: var(--color-text);
  line-height: 1.2;
  font-weight: var(--font-weight-regular);
}

p {
  margin: 0 0 var(--wf-space-3) 0;
}

/* --------------------------------------------------------------------------
   Home section heads ("What I lead on", "Selected work") — the ONLY h2s
   that take the serif display voice (proposal §1.2's table). Case-page
   section heads (.wf-prose h2) and About's section heads stay Roboto —
   serif is reserved for singular home moments, never a repeated grid.
   -------------------------------------------------------------------------- */
.wf-section-head {
  font-family: var(--font-serif);
  font-size: var(--font-h2);
  font-weight: var(--font-serif-weight-head);
  letter-spacing: var(--tracking-h2);
  line-height: 1.15;
  text-wrap: balance;
}

a {
  color: var(--color-accent-strong);
}

/* :visited kept identical to the unvisited link color rather than
   introducing an unverified new shade — teal-deep already passes 4.5:1+ on
   every ground it's used on (paper 5.4:1, surface-alt 5.1:1), so "on
   palette and still AA" is satisfied without guessing at a new hex. */
a:visited {
  color: var(--color-accent-strong);
}

.on-ink a:visited {
  color: var(--color-accent-inv);
}

ul {
  padding-left: 1.25rem;
}

/* --------------------------------------------------------------------------
   Text selection — teal-deep fill, paper text by default. Matches the
   site's accent color instead of the browser default blue.

   Direction 2.0 P4 (§6): the ink hero/footer and the terracotta band are
   new DARK/WARM grounds this site didn't have before — the default
   teal-deep-fill selection would sit directly on ink/terracotta-ink
   without enough separation to read as a clean highlight, and pairing
   paper-on-paper (if a ground's own text color were reused blindly) would
   be illegible. So each ground gets its OWN selection pair, scoped by
   section, so selection never inverts to an illegible or muddy
   combination on any of the site's three grounds (paper / ink / band).
   -------------------------------------------------------------------------- */
::selection {
  background: var(--color-accent-strong);
  color: var(--color-paper);
}

/* Paper-on-ink selection: on the hero/footer's dark ground, invert to a
   paper-filled highlight with ink text — the same relationship the ink
   ground already has with its own text, just swapped, so selection reads
   as "the ground's own colors, inverted" rather than an unrelated color
   dropped on top. */
.on-ink ::selection {
  background: var(--color-paper);
  color: var(--color-ink);
}

/* Paper-on-terracotta selection: the mid-home band gets the same paper
   ring/paper text treatment already used for its focus ring (§10 #6) —
   consistent "the band's own family, inverted" logic. */
.wf-band ::selection {
  background: var(--color-paper);
  color: var(--color-surface-band);
}

/* --------------------------------------------------------------------------
   Focus style — global, non-negotiable, never removed (§4.4 / AC#6)
   Teal-deep on light sections (5.19:1); ink sections override to
   terracotta-ink via .on-ink (§ Hero / Footer below).
   -------------------------------------------------------------------------- */
:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.on-ink :focus-visible,
.on-ink:focus-visible {
  outline-color: var(--color-focus-inv);
}

/* --------------------------------------------------------------------------
   Reduced motion guard — global, non-negotiable (§1.8 / AC#9).
   Every transition/animation in this file lives inside interactive-state
   rules that are themselves harmless without motion (hover/focus states
   still apply instantly) — this guard collapses the tweening to ~0 while
   leaving the end-state (color, position, shadow) intact.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* --------------------------------------------------------------------------
   Scroll-driven reveals (Direction 2.0 P4 §7) — see src/js/scroll-reveal.js
   for the IntersectionObserver that adds the two classes below.

   Default-visible-without-JS contract: [data-reveal] elements ONLY dim/
   offset once html.wf-reveal-ready is present (added by the script the
   instant it runs) — so if the script fails to load, throws, or JS is
   disabled, every [data-reveal] element simply renders at its normal
   opacity/position, exactly as if this feature didn't exist. Nothing is
   ever hidden by CSS alone.
   -------------------------------------------------------------------------- */
html.wf-reveal-ready [data-reveal] {
  opacity: 0;
  transform: translateY(12px);
}

@media (prefers-reduced-motion: no-preference) {
  html.wf-reveal-ready [data-reveal] {
    transition: opacity var(--motion-slow) var(--ease-out),
                transform var(--motion-slow) var(--ease-out);
  }
}

html.wf-reveal-ready [data-reveal].wf-revealed {
  opacity: 1;
  transform: none;
}

/* --------------------------------------------------------------------------
   Layout helpers
   -------------------------------------------------------------------------- */

.wf-container {
  max-width: var(--wf-container-max);
  margin-inline: auto;
  padding-inline: var(--wf-container-pad);
}

@media (min-width: 48rem) {
  .wf-container {
    padding-inline: var(--wf-container-pad-lg);
  }
}

.wf-prose-measure {
  max-width: var(--wf-measure);
}

/* --------------------------------------------------------------------------
   Interior-page shell (layout addendum §1, §2) — About, Contact, case pages.

   Used together with .wf-container (class="wf-container wf-page"), so it
   inherits the container's max-width + inline padding — the same left
   padding edge the header brand sits on — WITHOUT re-centering a narrow
   column inside it. Two jobs:
     1. Page-top rhythm: one deliberate gap between the header's bottom
        hairline and the page's first heading, shared by every interior
        page so none can ship flush again (was: .wf-prose had no
        padding-block at all; .wf-case had its own, now removed above).
     2. Left-anchored reading column at >=64rem: a two-track grid puts the
        66ch measure in track 1 (starting at the container's left padding
        edge, same as the brand) and turns the leftover width into a
        right-hand rail instead of a symmetric, centered dead gutter.
   Below 64rem this is a plain single column — the grid is a >=64rem-only
   enhancement, nothing about DOM order or semantics changes at any width.
   -------------------------------------------------------------------------- */
.wf-page {
  padding-block-start: var(--wf-space-page-top);
  padding-block-end: var(--wf-space-7);
}

@media (min-width: 48rem) {
  .wf-page {
    padding-block-start: var(--wf-space-page-top-lg);
  }
}

/* Reading-measure cap — ALL widths, not just >=64rem (critic FIX-FIRST
   blocker, 2026-07: below 64rem .wf-page was a plain block with nothing
   capping its children, so paragraphs/headings ran to the full container
   width — up to ~83-113ch on About/case pages at 768-1023px).

   Why this is a GRID at every width, not just >=64rem, and not a
   `max-width: 66ch` rule on the children directly — verified by testing
   the simpler options first and finding each one broken:
     1. `max-width: 66ch` directly on `.wf-page > *`: `ch` is relative to
        the FONT-SIZE OF THE ELEMENT IT'S APPLIED TO. This put 66ch on the
        <h1> too, computed at the h1's own much larger font, so the box
        came out far WIDER than 66 real characters (~1187px instead of
        ~593px at a 768px viewport) — it silently defeated the cap on
        exactly the elements that needed it (confirmed .wf-case h1's own
        pre-existing max-width:66ch rule had the identical latent bug).
     2. `max-width: 66ch` on `.wf-page` itself (to force a 1rem context):
        `.wf-page` shares its element with `.wf-container`, whose
        `margin-inline: auto` would then center the now-narrower box —
        reintroducing the centered-column bug at a narrower width.
   Fix: CSS GRID TRACK SIZING resolves `ch` against the GRID CONTAINER's
   own font-size, not each grid item's — confirmed by test (a grid item
   with a 32px font-size, in a `minmax(0, 66ch)` track on a 16px-font
   container, renders at the CORRECT 66ch-at-16px width, not its own
   font's 66ch). Making `.wf-page` a single-column grid at every width
   (not just >=64rem) gets the cap right everywhere with zero new wrapper
   elements and zero template changes — the >=64rem media query below just
   adds the second (rail) column on top of the same mechanism. */
.wf-page {
  display: grid;
  grid-template-columns: minmax(0, var(--wf-measure));
}

@media (min-width: 64rem) {
  .wf-page {
    grid-template-columns: minmax(0, var(--wf-measure)) 1fr;
    column-gap: var(--wf-space-6);
  }
}

/* Every direct child lives in track 1 (the measure column); at >=64rem,
   track 2 (the right rail) is whitespace by default — nothing is
   centered, at any width. */
.wf-page > * {
  grid-column: 1;
}

/* --------------------------------------------------------------------------
   The rail, done right (Direction 2.0 §2) — numbered, tethered, sticky
   marginalia, applied IDENTICALLY on About and case pages (proposal §2.3).
   This replaces the old unfurnished right-rail eyebrows (which had no
   number, no rule, no sticky behavior, no tether — the root cause of "hard
   to understand how those titles fit in").

   Root mechanism: the <h2> stays exactly where .wf-page's grid already
   puts it (track 1, the measure column — see the .wf-page rules above;
   DOM order, heading level, and the heading's own visible text are all
   untouched, AC#11). At >=64rem, the h2 becomes a `position: relative`
   anchor and grows a RAIL MARKER out of ::before (the number) and ::after
   (the hairline rule + vertical tether), both absolutely-positioned and
   pushed out to the right into the rail's whitespace (.wf-page's track 2)
   via `left: calc(100% + gap)`, then pinned with `position: sticky` so
   the marker pins while its section scrolls past and releases when the
   next one arrives (§2.1's "running head" behavior). The section's own
   <h2> text is the "label" the spec describes — no duplicate copy is
   generated, so there is no markup change needed on About or on any case
   markdown file, and nothing can drift out of sync with the real heading.
   This sidesteps a grid-nesting conflict (the h2 is a grid ITEM of the
   page grid, not itself a grid container) that an earlier nested-grid
   version of this rule hit.

   >=64rem only (proposal §2.2): below that, this whole block never applies
   — the counter still increments (harmless) but nothing is repositioned;
   h2s stay in normal flow, full size, exactly where the DOM already puts
   them.
   -------------------------------------------------------------------------- */
@media (min-width: 64rem) {
  /* Case pages: .wf-prose is a flat run of <h2> section heads; About: each
     <section> has its own <h2>. Both start a shared counter so numbering
     is 01→0N within EACH page independently. */
  .wf-prose,
  .wf-page--about {
    counter-reset: wf-rail-section;
  }

  .wf-page--about > section > h2,
  .wf-prose > h2 {
    counter-increment: wf-rail-section;
    position: relative;
  }

  /* The number: Noto Serif, tabular-nums, chrome, sticky under the header.
     Anchored to the h2's own box (which sits in track 1) and pushed out
     into track 2's whitespace — --wf-space-6 mirrors the exact
     column-gap .wf-page uses between its two tracks, so the marker starts
     flush with the rail's left edge. Decorative generated content is
     never exposed to the accessibility tree, so the real <h2> text alone
     carries the heading's semantics. */
  .wf-page--about > section > h2::before,
  .wf-prose > h2::before {
    content: counter(wf-rail-section, decimal-leading-zero);
    position: sticky;
    top: var(--rail-sticky-top);
    display: block;
    width: max-content;
    margin-left: calc(100% + var(--wf-space-6));
    font-family: var(--font-serif);
    font-size: var(--font-h3);
    font-weight: var(--font-serif-weight-hero);
    font-variant-numeric: tabular-nums;
    color: var(--color-rail-number);
    line-height: 1;
  }

  /* The 40px hairline rule (echoes the placeholder mark) plus the vertical
     tether running toward the section content — the piece that was
     missing (§2.1) — drawn as one generated box: a short solid top edge,
     then a dashed run below it, via a single background-image so no extra
     DOM node is needed. Sits directly under the number, same rail column. */
  .wf-page--about > section > h2::after,
  .wf-prose > h2::after {
    content: "";
    position: sticky;
    top: calc(var(--rail-sticky-top) + 2.5rem);
    display: block;
    width: var(--rail-rule-width);
    height: 6rem;
    margin-left: calc(100% + var(--wf-space-6));
    margin-top: 0.5rem;
    background-image:
      linear-gradient(to right, var(--color-rail-tether), var(--color-rail-tether)),
      linear-gradient(to bottom, var(--color-rail-tether) 1px, transparent 1px);
    background-size: var(--rail-rule-width) 1px, 1px 6px;
    background-repeat: no-repeat, repeat-y;
    background-position: top left, top left;
  }
}

.wf-box {
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: var(--wf-space-4);
}

/* Cards that are links (case cards, proof cards when linked) lift on
   hover/focus — transform + shadow + border-color only (§5.3, AC#10). */
@media (prefers-reduced-motion: no-preference) {
  .wf-box {
    transition: transform var(--motion-base) var(--ease-out),
                box-shadow var(--motion-base) var(--ease-out),
                border-color var(--motion-base) var(--ease-out);
  }
}

.wf-case-card:hover,
.wf-case-card:focus-within {
  transform: translateY(-3px);
  box-shadow: var(--shadow-hover);
  border-color: var(--color-border-strong);
}

/* --------------------------------------------------------------------------
   Skip link (§4.11)
   -------------------------------------------------------------------------- */

.wf-skiplink {
  position: absolute;
  top: -999px;
  left: 0;
  z-index: 100;
  background: var(--color-surface);
  color: var(--color-text);
  border: 2px solid var(--color-accent-strong);
  border-radius: var(--radius-sm);
  padding: var(--wf-space-2) var(--wf-space-3);
  font-family: var(--wf-font);
}

.wf-skiplink:focus {
  top: var(--wf-space-2);
  left: var(--wf-space-2);
  box-shadow: var(--shadow-md);
}

/* --------------------------------------------------------------------------
   Header / primary nav (§4.1)
   -------------------------------------------------------------------------- */

/* Sticky persistent header (proposal §3.1). At rest it's identical to
   before (flat paper, hairline border). "position: sticky" pins it to the
   viewport top at every scroll depth; the ELEVATION (backdrop blur +
   shadow) only switches on past the scroll threshold (see
   header-elevation.js), so the header doesn't look "elevated" while it's
   still flush against the very top of the page. */
header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

/* Elevation state — added by header-elevation.js once the page has
   scrolled past ~1 viewport height. A translucent, blurred paper (progressive
   enhancement: falls back to solid paper where backdrop-filter is
   unsupported, which is also the correct look, just without the blur) plus
   a soft shadow — "I'm floating above content now", not a restyle. This is
   a STATE change (applied instantly via a class), not an animated
   transition, so it needs no reduced-motion guard (AC#7/§3.1). */
header.wf-header--elevated {
  background: color-mix(in srgb, var(--color-surface) 92%, transparent);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  box-shadow: var(--shadow-sm);
}

/* backdrop-filter unsupported: keep the header fully opaque (paper) rather
   than a translucent color-mix over unknown content — same visual family,
   just without the blur, and every contrast pair below still resolves
   against solid paper (AC#7). */
@supports not (backdrop-filter: blur(1px)) {
  header.wf-header--elevated {
    background: var(--color-surface);
  }
}

.wf-header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  padding-block: var(--wf-space-3);
  gap: var(--wf-space-3);
}

.wf-brand {
  font-family: var(--wf-font);
  font-weight: var(--font-weight-medium);
  font-size: var(--font-h3);
  color: var(--color-text);
  text-decoration: none;
  position: relative;
}

/* Signature underline-grow micro-interaction (§5.1). A ::after bar starts
   scaled to 0 width and grows in from the left on hover/focus — this reads
   as considered, not a color flash or a sudden jump. */
.wf-brand::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 2px;
  background: var(--color-accent-strong);
  transform: scaleX(0);
  transform-origin: left;
}

@media (prefers-reduced-motion: no-preference) {
  .wf-brand::after {
    transition: transform var(--motion-base) var(--ease-out);
  }
}

.wf-brand:hover::after,
.wf-brand:focus-visible::after {
  transform: scaleX(1);
}

header nav[aria-label="Primary"] {
  display: flex;
  align-items: center;
  gap: var(--wf-space-3);
}

.wf-nav-toggle {
  font-family: var(--wf-font);
  font-size: var(--wf-text-base);
  background: var(--color-surface);
  color: var(--color-text);
  /* border-ui not border-strong — a meaningful interactive control boundary,
     must clear 1.4.11 (see .wf-field input/textarea above for the numbers). */
  border: 1px solid var(--color-border-ui);
  border-radius: var(--radius-sm);
  padding: var(--wf-space-2) var(--wf-space-3);
  min-height: 44px;
  min-width: 44px;
  cursor: pointer;
}

#wf-nav-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: none;
  flex-direction: column;
  gap: var(--wf-space-1);
  width: 100%;
}

/* JS-off / no-JS resilience: nav list is always visible without JS.
   nav.js adds .wf-js-enabled to <html> once it runs, and only THEN does
   the toggle get the power to hide the list — so with JS disabled the
   list simply stays visible and usable (AC#13). */
html:not(.wf-js-enabled) #wf-nav-list {
  display: flex;
}

html.wf-js-enabled .wf-nav-toggle {
  display: inline-block;
}

html.wf-js-enabled #wf-nav-list[data-open="true"] {
  display: flex;
}

html.wf-js-enabled #wf-nav-list[data-open="false"] {
  display: none;
}

#wf-nav-list li {
  display: block;
}

#wf-nav-list a {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding-block: var(--wf-space-2);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
  text-decoration: none;
  position: relative;
  width: fit-content;
}

#wf-nav-list a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 8px;
  height: 2px;
  background: var(--color-accent-strong);
  transform: scaleX(0);
  transform-origin: left;
}

@media (prefers-reduced-motion: no-preference) {
  #wf-nav-list a::after {
    transition: transform var(--motion-base) var(--ease-out);
  }
}

#wf-nav-list a:hover::after,
#wf-nav-list a:focus-visible::after {
  transform: scaleX(1);
}

/* Current page: persistent teal-deep underline + color, offset 6px per
   spec §4.1 — NOT just the hover state, and never color alone (the ▸
   marker below is the non-color cue, AC#8). */
#wf-nav-list a[aria-current="page"] {
  color: var(--color-accent-strong);
}

#wf-nav-list a[aria-current="page"]::after {
  bottom: 6px;
  transform: scaleX(1);
}

/* Scroll-spy active state (proposal §3.2) — a scrolled-to section, NOT a
   page change, so this is deliberately a class + aria-current="true"
   (generic "current item"), never aria-current="page". Same visual
   treatment as the true current-page state above (teal-deep + persistent
   underline) so it reads as one consistent "you are here" language, but the
   two are semantically distinct for assistive tech. */
#wf-nav-list a.is-in-view {
  color: var(--color-accent-strong);
}

#wf-nav-list a.is-in-view::after {
  bottom: 6px;
  transform: scaleX(1);
}

.wf-current-marker {
  margin-right: 0.25em;
}

@media (min-width: 48rem) {
  .wf-nav-toggle {
    display: none !important;
  }

  #wf-nav-list,
  html.wf-js-enabled #wf-nav-list {
    display: flex !important;
    flex-direction: row;
    width: auto;
    gap: var(--wf-space-4);
  }
}

/* --------------------------------------------------------------------------
   Footer (inverted — §4.10)
   -------------------------------------------------------------------------- */

footer.on-ink {
  background: var(--color-surface-inv);
  border-top: 1px solid var(--color-border);
  margin-top: var(--wf-space-7);
  padding-block: var(--wf-space-6);
  color: var(--color-text-inv-secondary);
}

@media (min-width: 48rem) {
  footer.on-ink {
    padding-block: var(--wf-space-7);
  }
}

footer.on-ink p {
  color: var(--color-text-inv-secondary);
}

/* Footer-as-destination (§4.5) — the large serif thesis restatement that
   opens the footer, a deliberate closing beat echoing the hero H1. */
.wf-footer-thesis {
  font-family: var(--font-serif);
  font-size: var(--font-h1);
  font-weight: var(--font-serif-weight-hero);
  letter-spacing: var(--tracking-h1);
  line-height: 1.1;
  text-wrap: balance;
  color: var(--color-text-inv);
  max-width: var(--wf-measure);
  margin: 0 0 var(--wf-space-4) 0;
}

/* The existing contact CTA now sits one level down in the hierarchy,
   under the thesis — same copy, no new claim, just a smaller voice. */
.wf-footer-contact-line {
  font-size: var(--font-body-lg);
  margin: 0 0 var(--wf-space-4) 0;
}

/* The colophon — a quiet closing line, smallest voice in the footer,
   sitting under the copyright line it echoes in scale. */
.wf-footer-colophon {
  font-size: var(--font-small);
  color: var(--color-text-inv-secondary);
  margin: var(--wf-space-2) 0 0 0;
  padding-top: var(--wf-space-3);
  border-top: 1px solid var(--color-border-inv);
}

footer nav[aria-label="Footer"] ul {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--wf-space-3) 0;
  display: flex;
  flex-direction: column;
  gap: var(--wf-space-2);
}

footer.on-ink a {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  color: var(--color-accent-inv);
  text-decoration: none;
  position: relative;
  width: fit-content;
}

footer.on-ink a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 6px;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
}

@media (prefers-reduced-motion: no-preference) {
  footer.on-ink a::after {
    transition: transform var(--motion-base) var(--ease-out);
  }
}

footer.on-ink a:hover,
footer.on-ink a:focus-visible {
  color: var(--color-text-inv);
}

footer.on-ink a:hover::after,
footer.on-ink a:focus-visible::after {
  transform: scaleX(1);
}

footer.on-ink .wf-footer-copyright {
  font-size: var(--font-small);
  color: var(--color-text-inv-secondary);
}

@media (min-width: 48rem) {
  footer nav[aria-label="Footer"] ul {
    flex-direction: row;
    gap: var(--wf-space-5);
  }
}

/* --------------------------------------------------------------------------
   Hero (inverted — §4.2)
   -------------------------------------------------------------------------- */

.wf-hero.on-ink {
  background: var(--color-surface-inv);
  padding-block: var(--wf-space-6);
}

@media (min-width: 48rem) {
  .wf-hero.on-ink {
    padding-block: var(--wf-space-7);
  }
}

.wf-hero__eyebrow {
  font-size: var(--font-eyebrow);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-text-inv-eyebrow);
  margin: 0 0 var(--wf-space-3) 0;
}

.wf-hero h1 {
  font-family: var(--font-serif);
  font-size: var(--font-display);
  font-weight: var(--font-serif-weight-hero);
  line-height: 1.05;
  letter-spacing: var(--tracking-display);
  text-wrap: balance;
  color: var(--color-text-inv);
  max-width: 16ch;
  margin: 0 0 var(--wf-space-4) 0;
}

.wf-hero p {
  font-size: var(--font-body-lg);
  color: var(--color-text-inv-secondary);
  max-width: var(--wf-measure);
}

/* --------------------------------------------------------------------------
   Stat strip (§4.3)
   -------------------------------------------------------------------------- */

.wf-stat-strip {
  background: var(--color-surface-alt);
  border-block: 1px solid var(--color-border);
  padding-block: var(--wf-space-5);
}

.wf-stat-grid {
  margin: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--wf-space-3);
}

@media (min-width: 48rem) {
  .wf-stat-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--wf-space-4);
  }
}

.wf-stat-cell {
  background: var(--color-surface);
  /* DOM order is label (dt) then value (dd) — correct for screen readers,
     which read the dt before its dd. Flex + order keeps the big value
     showing first visually, matching the spec's layout (AC#13). */
  display: flex;
  flex-direction: column;
}

.wf-stat-value {
  order: -1;
  font-family: var(--font-serif);
  font-size: var(--font-stat);
  font-weight: var(--font-serif-weight-stat);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-stat);
  line-height: 1.0;
  color: var(--color-text);
  margin: 0 0 var(--wf-space-1) 0;
}

.wf-stat-label {
  margin: 0;
  font-size: var(--font-eyebrow);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--color-text-secondary);
}

/* --------------------------------------------------------------------------
   What I lead on — four proofs (§4.4)
   -------------------------------------------------------------------------- */

.wf-lead-on {
  padding-block: var(--wf-space-6);
}

@media (min-width: 48rem) {
  .wf-lead-on {
    padding-block: var(--wf-space-7);
  }
}

/* De-carded (Direction 2.0 §4.1.3): retires the 4-up rounded-shadow-card
   grid (the "default theme" tell) for four hairline-ruled rows — a
   considered index, 2-col editorial list at >=48rem, no drop-shadows, no
   card chrome. Same <h3>/<p> content as before. */
.wf-proof-list {
  list-style: none;
  margin: var(--wf-space-4) 0 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  column-gap: var(--wf-space-6);
}

@media (min-width: 48rem) {
  .wf-proof-list {
    grid-template-columns: repeat(2, 1fr);
    grid-auto-flow: row;
  }
}

.wf-proof-row {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--wf-space-3);
  align-items: baseline;
  padding-block: var(--wf-space-4);
  border-top: 1px solid var(--color-border);
}

.wf-proof-row:last-child {
  border-bottom: 1px solid var(--color-border);
}

@media (min-width: 48rem) {
  /* The bottom rule only needs to appear once per column, on each
     column's own last row, not doubled where the 2-up grid wraps. */
  .wf-proof-row:nth-last-child(-n+2) {
    border-bottom: 1px solid var(--color-border);
  }
}

.wf-proof-row__number {
  grid-row: 1 / span 2;
  font-family: var(--font-serif);
  font-size: var(--font-h3);
  font-weight: var(--font-serif-weight-hero);
  font-variant-numeric: tabular-nums;
  color: var(--color-rail-number);
  line-height: 1;
}

.wf-proof-row h3 {
  grid-column: 2;
  font-size: var(--font-h3);
  font-weight: var(--font-weight-medium);
  margin: 0 0 var(--wf-space-2) 0;
}

.wf-proof-row p {
  grid-column: 2;
  margin: 0;
  color: var(--color-text-secondary);
}

/* --------------------------------------------------------------------------
   The terracotta band (Direction 2.0 §2/§4.1.4/§10 #3-#4-#6) — a THIRD
   ground (not paper, not ink): full-bleed, structural, warm. Carries the
   people-leadership/promise line in paper text (4.92:1 — the whole reason
   this uses terracotta-INK #A8442F and not the lighter terracotta spark,
   which only clears 3.73:1/3.99:1, large-text only). Ink text is never
   used for this band's running copy (only 3.03:1, large/UI-only, §10 #4).
   -------------------------------------------------------------------------- */
.wf-band {
  background: var(--color-surface-band);
  padding-block: var(--wf-space-6);
}

@media (min-width: 48rem) {
  .wf-band {
    padding-block: var(--wf-space-7);
  }
}

.wf-band__line {
  margin: 0;
  max-width: var(--wf-measure);
  font-family: var(--font-serif);
  font-size: var(--font-h2);
  font-weight: var(--font-serif-weight-quote);
  font-style: italic;
  letter-spacing: var(--tracking-quote);
  line-height: 1.3;
  text-wrap: balance;
  color: var(--color-text-band);
}

/* Focus ring on the band ground: PAPER, not teal-muted (2.43:1, fails) or
   terracotta-ink (invisible on its own ground) — proposal §10 #6. Scoped
   to the band so it never leaks onto paper/ink focus rings elsewhere. */
.wf-band :focus-visible {
  outline-color: var(--color-focus-band);
}

/* --------------------------------------------------------------------------
   Cases grid + card (§4.5)
   -------------------------------------------------------------------------- */

.wf-cases {
  padding-block: var(--wf-space-6);
}

@media (min-width: 48rem) {
  .wf-cases {
    padding-block: var(--wf-space-7);
  }
}

/* Full-bleed case moment (§4.1.5): the strongest asset (the first case in
   src/_data/cases.js — the accessibility program) leads as a wide feature
   row spanning every column; the remaining cases follow in a tighter grid
   underneath. At >=64rem the feature spans all 3 columns; below that the
   grid is already single/2-up and the feature card is simply the first,
   full-width row — no separate mobile treatment needed. */
.wf-case-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--wf-space-4);
  margin-top: var(--wf-space-4);
}

@media (min-width: 48rem) {
  .wf-case-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 64rem) {
  .wf-case-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.wf-case-card--feature {
  grid-column: 1 / -1;
}

@media (min-width: 48rem) {
  /* The feature card gets its own internal two-column layout (title/blurb
     left, meta right) so it reads as a wide row, not just a stretched
     square card — the "alternate wide/tight" rhythm the spec calls for. */
  .wf-case-card--feature {
    display: grid;
    grid-template-columns: 2fr 1fr;
    column-gap: var(--wf-space-5);
    align-items: start;
    padding-block: var(--wf-space-5);
  }

  .wf-case-card--feature .wf-badge {
    grid-column: 1 / -1;
  }

  .wf-case-card--feature h3 {
    font-family: var(--font-serif);
    font-size: var(--font-h2);
    font-weight: var(--font-serif-weight-head);
    letter-spacing: var(--tracking-h2);
  }

  .wf-case-card--feature .wf-case-card__meta {
    grid-column: 2;
    grid-row: 2 / span 2;
    border-top: none;
    padding-top: 0;
    margin-top: 0;
  }

  .wf-case-card--feature .wf-case-card__cta {
    grid-column: 1;
  }
}

.wf-case-card {
  display: flex;
  flex-direction: column;
  gap: var(--wf-space-2);
}

.wf-case-card h3 {
  font-size: var(--font-h3);
  font-weight: var(--font-weight-medium);
  margin: 0;
}

.wf-case-card h3 a {
  color: var(--color-text);
  text-decoration: none;
  /* The whole title text is the link (per spec §4.5) — no stretched-click
     trick, so the badge and meta row underneath stay their own, separate,
     unambiguous hit targets. */
}

.wf-case-card h3 a:hover,
.wf-case-card h3 a:focus-visible {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.wf-case-card p {
  margin: 0;
  color: var(--color-text);
}

.wf-case-card__meta {
  display: flex;
  flex-direction: column;
  gap: var(--wf-space-1);
  color: var(--color-text-secondary);
  font-size: var(--font-small);
  border-top: 1px solid var(--color-border);
  padding-top: var(--wf-space-2);
  margin-top: auto;
}

.wf-case-card__cta {
  font-weight: var(--font-weight-medium);
  color: var(--color-accent-strong);
}

/* Badge (§4.5, §4.11) — pill chip, text-only meaning, never color-coded */
.wf-badge {
  display: inline-block;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-pill);
  color: var(--color-accent-strong);
  font-size: var(--font-eyebrow);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  padding: 4px 12px;
  margin: 0 0 var(--wf-space-2) 0;
  align-self: flex-start;
}

/* --------------------------------------------------------------------------
   Placeholder-block mechanic (§6) — "artifact pending", designed not
   unfinished. Replaces the old dashed grey box entirely (AC#2).
   -------------------------------------------------------------------------- */

.wf-placeholder {
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface-alt);
  padding: var(--wf-space-5);
  margin-block: var(--wf-space-4);
  /* Inline-in-prose default: a case page can stack up to four of these
     (one per section) — 16/6 keeps them present but quiet, so the reading
     column still leads. The taller 16/10 ratio is reserved for the
     proof/artifacts grid below (two-up, more room to breathe). */
  min-height: 7rem;
  aspect-ratio: 16 / 6;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.wf-artifact-grid .wf-placeholder {
  margin-block: 0;
  min-height: 12rem;
  aspect-ratio: 16 / 10;
}

.wf-placeholder__label {
  color: var(--color-text-secondary);
  font-family: var(--wf-font);
  font-size: var(--font-eyebrow);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  text-align: center;
  margin: 0;
  max-width: 32ch;
}

/* Quiet mark above the label — a thin teal-deep hairline rule (§6) */
.wf-placeholder__label::before {
  content: "";
  display: block;
  width: 40px;
  height: 1px;
  background: var(--color-accent-strong);
  margin: 0 auto var(--wf-space-2) auto;
}

.wf-artifact-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--wf-space-4);
  margin-bottom: var(--wf-space-4);
}

@media (min-width: 64rem) {
  .wf-artifact-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* --------------------------------------------------------------------------
   System-art plates (Direction 2.0 §5) — the six bespoke inline SVG
   diagrams that replace the six "Coming Soon" placeholder blocks named in
   the proposal's shot-list (§5.2). Presented as numbered museum plates:
   the SVG itself (role="img" + <title>/<desc> for its accessible name/
   description) inside a <figure>, with a <figcaption> carrying the
   "Fig. 0N — Title. Caption." museum-label text (guardian-passed, from the
   writer's copy-selections memo §4). One consistent visual grammar across
   all six (§5.1): ink structure, teal-deep = the active/flow path,
   terracotta-ink = the single human/critical node, Roboto tracked-caps
   labels, flat line-art (no gradients/shadows), hairline-outlined nodes at
   --radius-sm. Responsive via viewBox + max-width:100%.
   -------------------------------------------------------------------------- */
.wf-plate {
  margin-block: var(--wf-space-5);
  padding: var(--wf-space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-alt);
}

.wf-plate svg {
  display: block;
  width: 100%;
  height: auto;
  max-width: 100%;
}

.wf-plate figcaption {
  margin-top: var(--wf-space-3);
  padding-top: var(--wf-space-3);
  border-top: 1px solid var(--color-border);
  font-size: var(--font-small);
  color: var(--color-text-secondary);
}

.wf-plate figcaption strong {
  color: var(--color-text);
  font-weight: var(--font-weight-medium);
}

/* SVG internals — shared token-driven styles so no diagram hard-codes a
   raw hex. Scoped to .wf-plate so these classes can't leak into unrelated
   SVG usage elsewhere on the site. */
.wf-plate .wf-plate-stroke-structure { stroke: var(--color-ink); fill: none; }
.wf-plate .wf-plate-stroke-flow      { stroke: var(--color-teal-deep); fill: none; }
.wf-plate .wf-plate-node-fill        { fill: var(--color-paper); stroke: var(--color-ink); }
.wf-plate .wf-plate-node-critical    { fill: var(--color-paper); stroke: var(--color-terracotta-ink); }
.wf-plate .wf-plate-node-critical-fill { fill: var(--color-terracotta-ink); stroke: var(--color-terracotta-ink); }
/* Font-size floor (critic P4 pass, legibility fix): SVG text `font-size`
   is a length in the SVG's OWN user-coordinate system, not a fixed CSS
   pixel — because every plate scales via `width:100%` against a 640-720
   wide viewBox, a small user-unit value shrinks along with the whole
   diagram at narrow viewports. .wf-plate is capped at the page's 66ch
   reading measure, so its worst-case rendered width is ~295px at a
   375px viewport (container 343px minus the plate's own 2x24px padding)
   — a scale factor of 295/~700 (viewBox widths range 640-800 across the
   six plates) ≈ 0.42-0.46. Solving for an ~11px CSS-equivalent floor at
   that worst case requires a 24 user-unit font-size (24 * 0.46 ≈ 11px);
   see the developer's report for the full six-plate table. 24px is now
   the ONE shared base for both label classes (previously 11px on
   .wf-plate-label, and NO font-size at all on .wf-plate-label-critical,
   which silently fell back to the SVG default and was the worst
   offender) — never go below this without recomputing the whole table. */
.wf-plate .wf-plate-label,
.wf-plate .wf-plate-label-critical {
  font-family: var(--wf-font);
  font-size: 24px;
  font-weight: var(--font-weight-medium);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.wf-plate .wf-plate-label {
  fill: var(--color-chrome);
}

.wf-plate .wf-plate-label-critical {
  fill: var(--color-terracotta-ink);
}

/* --------------------------------------------------------------------------
   Case detail page (§4.6)

   Top rhythm (header rule -> h1) now comes from .wf-page (single source —
   layout addendum §1/§2, AC#2) so .wf-case only owns its OWN bottom
   padding here; do not reintroduce a padding-block-start on this rule.
   -------------------------------------------------------------------------- */

.wf-case {
  padding-block-end: var(--wf-space-6);
}

@media (min-width: 48rem) {
  .wf-case {
    padding-block-end: var(--wf-space-7);
  }
}

.wf-case h1 {
  font-family: var(--font-serif);
  font-size: var(--font-h1);
  font-weight: var(--font-serif-weight-hero);
  line-height: 1.1;
  letter-spacing: var(--tracking-h1);
  text-wrap: balance;
  /* NOTE: this max-width is NOT the real measure constraint — `ch` here
     resolves against --font-h1 (much larger than 1rem), so this value is
     wider than the intended 66ch-of-body-text. It's harmless because the
     .wf-page grid track (minmax(0, 66ch), resolved correctly at .wf-page's
     own 1rem) is what actually constrains this h1's rendered width — kept
     here only so this rule still behaves reasonably if .wf-case h1 is ever
     used somewhere without a .wf-page grid ancestor. Do not rely on this
     line for the real cap; see the .wf-page comment above for why. */
  max-width: var(--wf-measure);
  margin: 0 0 var(--wf-space-3) 0;
}

.wf-throughline {
  max-width: var(--wf-measure);
  font-style: italic;
  font-weight: var(--font-weight-regular);
  color: var(--color-text-secondary);
  border-left: 2px solid var(--color-spark);
  padding-left: var(--wf-space-3);
}

.wf-meta-row {
  display: flex;
  flex-direction: column;
  gap: var(--wf-space-2);
  margin: 0 0 var(--wf-space-4) 0;
  padding: var(--wf-space-3) 0;
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

.wf-meta-item {
  display: flex;
  gap: var(--wf-space-1);
}

.wf-meta-item dt {
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}

.wf-meta-item dd {
  margin: 0;
  color: var(--color-text-secondary);
}

@media (min-width: 48rem) {
  .wf-meta-row {
    flex-direction: row;
    gap: var(--wf-space-5);
  }

  .wf-meta-item {
    flex-direction: column;
    gap: 0;
  }
}

.wf-outcome-recap {
  max-width: var(--wf-measure);
  margin-bottom: var(--wf-space-5);
  background: var(--color-surface-alt);
}

.wf-outcome-recap__label {
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  font-size: var(--font-eyebrow);
  letter-spacing: 0.14em;
  color: var(--color-accent-strong);
  margin: 0 0 var(--wf-space-2) 0;
}

.wf-outcome-recap ul {
  margin: 0;
  font-size: var(--font-body-lg);
  color: var(--color-text);
}

/* .wf-prose keeps its TYPOGRAPHY role only (h2 sizing, link styles,
   :first-child margin reset) — the page-level 66ch measure cap lives on
   .wf-page's own grid-track sizing (see above), in effect at every width,
   not just >=64rem (layout addendum §2.4). Applying max-width here was the
   root cause of the original centered-column bug: this wrapper also
   carried .wf-container's margin-inline:auto, so the whole reading column
   floated centered inside the 72rem frame instead of sharing the header
   brand's left edge. */
.wf-prose h2 {
  font-size: var(--font-h2);
  font-weight: var(--font-weight-regular);
  letter-spacing: -0.015em;
  margin-top: var(--wf-space-6);
}

.wf-prose > :first-child,
.wf-prose h2:first-of-type {
  margin-top: 0;
}

.wf-prose a {
  color: var(--color-accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}

@media (prefers-reduced-motion: no-preference) {
  .wf-prose a {
    transition: text-underline-offset var(--motion-base) var(--ease-out),
                text-decoration-thickness var(--motion-base) var(--ease-out);
  }
}

.wf-prose a:hover,
.wf-prose a:focus-visible {
  text-underline-offset: 5px;
  text-decoration-thickness: 2px;
}

.wf-chip-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--wf-space-2);
}

.wf-chip {
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-pill);
  color: var(--color-accent-strong);
  font-size: var(--font-eyebrow);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  padding: 4px 12px;
}

/* Prev/next as a designed footer moment (Direction 2.0 §4.2) — a composed
   spread, not two plain links: hairline top, tracked-caps eyebrow, the
   case title in Roboto medium with the site's signature grow-underline
   under the title only (not the eyebrow), generous padding so it reads as
   a closing beat rather than a leftover row. */
.wf-case-nav {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--wf-space-5);
  margin-top: var(--wf-space-7);
  padding-top: var(--wf-space-5);
  border-top: 1px solid var(--color-border);
}

@media (min-width: 48rem) {
  .wf-case-nav {
    grid-template-columns: 1fr 1fr;
    gap: var(--wf-space-6);
  }
}

.wf-case-nav__link {
  display: flex;
  flex-direction: column;
  gap: var(--wf-space-2);
  color: var(--color-text);
  text-decoration: none;
  padding: var(--wf-space-3) 0;
}

.wf-case-nav__link--next {
  text-align: left;
}

@media (min-width: 48rem) {
  .wf-case-nav__link--next {
    text-align: right;
    align-items: flex-end;
  }
}

.wf-case-nav__eyebrow {
  font-size: var(--font-eyebrow);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-accent-strong);
}

.wf-case-nav__title {
  position: relative;
  width: fit-content;
  font-size: var(--font-h3);
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}

.wf-case-nav__title::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 2px;
  background: var(--color-accent-strong);
  transform: scaleX(0);
  transform-origin: left;
}

@media (prefers-reduced-motion: no-preference) {
  .wf-case-nav__title::after {
    transition: transform var(--motion-base) var(--ease-out);
  }
}

.wf-case-nav__link--next .wf-case-nav__title::after {
  transform-origin: right;
}

.wf-case-nav__link:hover .wf-case-nav__title::after,
.wf-case-nav__link:focus-visible .wf-case-nav__title::after {
  transform: scaleX(1);
}

/* --------------------------------------------------------------------------
   About page (§4.8)
   -------------------------------------------------------------------------- */

.wf-pillar-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--wf-space-4);
  margin-top: var(--wf-space-4);
  max-width: none;
}

@media (min-width: 48rem) {
  .wf-pillar-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

.wf-pillar-card h3 {
  font-size: var(--font-h3);
  font-weight: var(--font-weight-medium);
  margin-top: 0;
}

.wf-credentials-list {
  columns: 1;
  margin: var(--wf-space-4) 0 0 0;
  padding-left: 1.25rem;
  color: var(--color-text);
}

.wf-credentials-list li::marker {
  color: var(--color-accent-strong);
}

@media (min-width: 48rem) {
  .wf-credentials-list {
    columns: 2;
    column-gap: var(--wf-space-5);
  }
}

.wf-credentials-list li {
  break-inside: avoid;
  margin-bottom: var(--wf-space-2);
}

/* Pull-quote — Noto Serif's real, separately-drawn italic (not a synthetic
   CSS slant), per proposal §1.4. Used once on About (the turnaround-method
   line) and once per case page (§4.2, added in the P2 rail/rhythm pass). */
.wf-pullquote {
  font-family: var(--font-serif);
  font-size: var(--font-h2);
  font-weight: var(--font-serif-weight-quote);
  font-style: italic;
  letter-spacing: var(--tracking-quote);
  line-height: 1.3;
  text-wrap: balance;
  color: var(--color-text);
  border-left: 2px solid var(--color-spark);
  padding-left: var(--wf-space-3);
  margin-block: var(--wf-space-5);
  max-width: var(--wf-measure);
}

/* --------------------------------------------------------------------------
   Contact page + form (§4.4/§4.9) — composed as a closing invitation, not
   a lone form card. Below 64rem: a single stacked column (invitation,
   then form) inside .wf-page's existing 66ch track — unchanged reading
   order. At >=64rem: .wf-contact-frame spans BOTH of .wf-page's tracks
   (same technique the About rail sections and the contact block use) and
   subdivides into its own two columns — invitation left, form right — so
   the page reads as one composed spread instead of a centered form card.
   -------------------------------------------------------------------------- */
.wf-contact-frame {
  border-left: 2px solid var(--color-spark);
  padding-left: var(--wf-space-4);
}

@media (min-width: 64rem) {
  .wf-contact-frame {
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: minmax(0, var(--wf-measure)) 1fr;
    column-gap: var(--wf-space-6);
    align-items: start;
  }

  .wf-contact-invite {
    grid-column: 1;
  }

  .wf-contact-form {
    grid-column: 2;
  }
}

.wf-contact-invite__h1 {
  font-family: var(--font-serif);
  font-size: var(--font-h1);
  font-weight: var(--font-serif-weight-hero);
  letter-spacing: var(--tracking-h1);
  line-height: 1.1;
  text-wrap: balance;
  margin: 0 0 var(--wf-space-3) 0;
}

.wf-contact-invite__lead {
  font-size: var(--font-body-lg);
  max-width: var(--wf-measure);
  margin: 0 0 var(--wf-space-4) 0;
}

/* No-JS default: hide the form, show only the mailto fallback (progressive
   enhancement — see contact.js). Once JS runs, nav.js's mechanism adds
   "wf-js-enabled" to <html> and the form is revealed. */
.wf-contact-form {
  display: none;
}

html.wf-js-enabled .wf-contact-form {
  display: block;
  max-width: var(--wf-measure);
  margin-block: var(--wf-space-4);
}

@media (min-width: 64rem) {
  html.wf-js-enabled .wf-contact-form {
    margin-block-start: 0;
  }
}

.wf-field {
  margin-bottom: var(--wf-space-4);
}

.wf-field label {
  display: block;
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
  margin-bottom: var(--wf-space-2);
}

.wf-field input,
.wf-field textarea {
  width: 100%;
  font-family: var(--wf-font);
  font-size: var(--font-body);
  color: var(--color-text);
  background: var(--color-surface);
  /* border-ui (sand-deep, #8A7549) not border-strong: this is a load-bearing
     control boundary, so it must clear the 1.4.11 non-text-contrast floor
     (3.68:1 on paper / 3.57:1 on surface-alt) — border-strong only passes
     1.65:1 / 1.61:1 and is reserved for decorative pill borders now. */
  border: 1px solid var(--color-border-ui);
  border-radius: var(--radius-sm);
  padding: var(--wf-space-2) var(--wf-space-3);
  min-height: 44px;
}

@media (prefers-reduced-motion: no-preference) {
  .wf-field input,
  .wf-field textarea {
    transition: border-color var(--motion-fast) var(--ease-out);
  }
}

.wf-field input:focus-visible,
.wf-field textarea:focus-visible {
  border-color: var(--color-focus);
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

.wf-field textarea {
  min-height: 8rem;
  resize: vertical;
}

.wf-field input[aria-invalid="true"],
.wf-field textarea[aria-invalid="true"] {
  border: 2px solid var(--color-spark);
}

.wf-field-error {
  color: var(--color-error);
  font-weight: var(--font-weight-medium);
  margin: var(--wf-space-2) 0 0 0;
}

.wf-field-error::before {
  content: "⚠ ";
}

/* --------------------------------------------------------------------------
   Buttons — "sophisticated, not cheap" (§4.9.1)

   .wf-button is the shared base (the hero's "View the work" CTA uses it
   directly); .wf-submit is the contact-form submit button and now just
   extends the same base so both stay visually identical without duplicating
   rules. .wf-button--on-ink is the variant for buttons sitting on an
   inverted (ink) section, e.g. the hero.
   -------------------------------------------------------------------------- */

.wf-button,
.wf-submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--wf-font);
  font-size: var(--font-body);
  font-weight: var(--font-weight-medium);
  text-decoration: none;
  color: var(--color-paper);
  background: var(--color-accent-strong);
  border: none;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm);
  padding: 12px 24px;
  min-height: 44px;
  min-width: 44px;
  cursor: pointer;
}

@media (prefers-reduced-motion: no-preference) {
  .wf-button,
  .wf-submit {
    transition: background-color var(--motion-base) var(--ease-out),
                transform var(--motion-base) var(--ease-out),
                box-shadow var(--motion-base) var(--ease-out);
  }
}

.wf-button:hover,
.wf-submit:hover {
  background: #245358; /* hair-darker teal per spec §4.9.1 — a one-off mix, not a reusable token */
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
  color: var(--color-paper);
}

.wf-button:active,
.wf-submit:active {
  transform: translateY(0);
  box-shadow: var(--shadow-sm);
}

.wf-button:disabled,
.wf-submit:disabled {
  cursor: not-allowed;
  opacity: 0.55;
  transform: none;
}

/* On-ink variant (§4.9.1 "Primary on ink, hero"): fill flips to paper, text
   to ink — same shape and motion, inverted palette so it reads on a dark
   ground. Focus-visible ring already flips to terracotta-ink via the
   .on-ink :focus-visible rule near the top of this file. */
.wf-button--on-ink {
  color: var(--color-ink);
  background: var(--color-paper);
  margin-top: var(--wf-space-4);
}

.wf-button--on-ink:hover {
  background: var(--color-text-inv-eyebrow); /* ash — a hair darker than paper, still reads as "paper family" */
  color: var(--color-ink);
}

/* --------------------------------------------------------------------------
   Button :visited hardening (fixes a live contrast bug, 2026-07).

   Root cause: `.on-ink a:visited { color: var(--color-accent-inv); }` (near
   the top of this file) assumes any link inside an `.on-ink` section sits
   directly on the ink ground, so ash-on-ink text is correct there. That's
   true for prose/nav links, but a BUTTON paints its own background (paper),
   so it is not "on ink" locally even though it lives inside `.on-ink` —
   the ancestor rule doesn't know that and was winning on :visited, painting
   ash text on the paper button (~1.7:1, fails AA badly).

   Fix: buttons get their own explicit :visited (and :visited:hover) colors
   so no ancestor link-color rule — generic `a`, `a:visited`, or
   `.on-ink a:visited` — can ever reach them.

   Specificity note (why the `a.` prefix below matters): CSS specificity is
   compared column by column — (ids, classes/attrs/pseudo-classes, type
   selectors) — and only moves to the next column on an exact tie in the
   one before it. `.on-ink a:visited` is (0 ids, 2 classes/pseudo, 1 type)
   because it counts BOTH `.on-ink` and `:visited` as the "class" column AND
   the bare `a` as a type selector. A same-looking button rule written as
   just `.wf-button--on-ink:visited` is only (0, 2, 0) — it ties on the
   class column and then LOSES on the type column, so `.on-ink a:visited`
   would still win even though the button selector reads as "more
   specific" in plain English. Writing the selector as `a.wf-button--on-ink`
   adds that same type-selector column back, producing (0, 2, 1) — a true
   tie with `.on-ink a:visited` — and CSS breaks ties by source order, so
   as long as this block stays BELOW `.on-ink a:visited` in the file (it
   does), it wins. This is the exact class of mistake the automated cascade
   check (tools/a11y/check-contrast.mjs) exists to catch. -------------- */
.wf-button:visited,
.wf-submit:visited {
  color: var(--color-paper);
}

.wf-button:visited:hover,
.wf-submit:visited:hover {
  color: var(--color-paper);
}

a.wf-button--on-ink:visited {
  color: var(--color-ink);
}

a.wf-button--on-ink:visited:hover {
  color: var(--color-ink);
}

.wf-form-status {
  margin: var(--wf-space-3) 0 0 0;
  font-weight: var(--font-weight-medium);
  color: var(--color-text);
}

.wf-form-status:empty {
  margin: 0;
}

.wf-fallback {
  max-width: var(--wf-measure);
}

.wf-fallback a {
  color: var(--color-accent-strong);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.wf-fallback a:hover,
.wf-fallback a:focus-visible {
  text-underline-offset: 5px;
}

/* ==========================================================================
   Print stylesheet (Direction 2.0 P4 §6) — case pages render as printed
   documents: no nav/header chrome, no footer, no interactive/decorative
   furniture (skip link, nav toggle, form, sticky positioning, grain,
   scroll-reveal states), rail markers fold back to inline headings (the
   same collapse behavior the <64rem responsive rule already defines — print
   is, in effect, always "the narrow layout"), and the system-art diagrams
   print intact (SVG renders natively in print, no extra rule needed).
   Ink sections (hero/footer/band) print as plain black-on-white text so
   ink toner isn't spent recreating a screen-only background wash.
   ========================================================================== */
@media print {
  /* Chrome that only makes sense on a live, interactive page. */
  header,
  footer,
  .wf-skiplink,
  .wf-nav-toggle,
  .wf-contact-form,
  .wf-fallback,
  .wf-case-nav {
    display: none !important;
  }

  /* Sticky positioning is meaningless once paginated — let every sticky
     element (header, rail markers) fall back to normal flow so nothing
     is clipped mid-page or repeated oddly across page breaks. */
  * {
    position: static !important;
  }

  /* No grain, no scroll-reveal pre-states — print is a static document;
     content must be fully opaque/untransformed on every page. */
  body::before,
  main::before,
  .wf-hero.on-ink::before,
  footer.on-ink::before,
  .wf-band::before {
    display: none !important;
  }

  [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
  }

  /* Rail markers (the ::before/::after generated numeral + tether) are a
     screen-only >=64rem enhancement; hide them in print so the heading's
     own in-flow text is the only thing that prints — same collapse
     behavior the <64rem responsive rule already guarantees, applied here
     unconditionally regardless of the paper size a print target reports. */
  .wf-page--about > section > h2::before,
  .wf-prose > h2::before,
  .wf-page--about > section > h2::after,
  .wf-prose > h2::after {
    display: none !important;
  }

  .wf-page--about > section > h2,
  .wf-prose > h2 {
    font-family: var(--wf-font) !important;
    font-size: var(--font-h2) !important;
    font-weight: var(--font-weight-regular) !important;
    text-transform: none !important;
    letter-spacing: normal !important;
    color: var(--color-text) !important;
    max-width: none !important;
  }

  /* Every section prints on a plain white/black page — the ink hero,
     footer, and terracotta band all fold to plain body text so print
     doesn't try to fill a page with a dark or warm background fill. */
  .wf-hero.on-ink,
  .wf-band {
    background: none !important;
    color: #000 !important;
  }

  .wf-hero h1,
  .wf-hero p,
  .wf-band__line {
    color: #000 !important;
  }

  /* Full-bleed containers regain normal page margins in print. */
  .wf-container,
  .wf-page {
    max-width: none !important;
    padding: 0 !important;
    display: block !important;
  }

  a {
    color: #000 !important;
    text-decoration: underline;
  }

  /* Print URLs after external links only (critic P4 pass: scoped away
     from mailto: links, which aren't "external" in the navigation sense
     and whose address is already fully visible as the link text itself
     on this site) — a small, considered touch, not a requirement. */
  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 0.85em;
    color: #333;
  }
}
