/* ==========================================================================
   BEN W. ROSE — benwrose.com
   Two views: an introduction, and the long version.
   Ground: TEX-1249 warm paper-fibre. Portrait: MOT-1304 dual-skin cross-fade.
   Rose is the accent and never the ground.
   ========================================================================== */

:root {
  --paper:      #F7F2EA;   /* warm off-white, ~95% L / 40° / 5% S — TEX-1249 */
  --paper-2:    #EFE7DB;
  --ink:        #0F0C0B;
  --ink-dim:    #4A423E;
  --ink-faint:  #746C67;

  --rose-900:   #5E1B28;
  --rose-700:   #8E2C3E;   /* 7.4:1 on paper */
  --rose-300:   #DCA098;
  --rose-100:   #F2D8CF;
  --rose:       var(--rose-700);

  --display: "Barlow Condensed", "Arial Narrow", sans-serif;
  --body:    "Barlow", system-ui, -apple-system, sans-serif;
  --label:   "IBM Plex Mono", ui-monospace, monospace;

  /* MOT-1704 — re-authored at mobile, not scaled */
  --dur-fast:   .35s;
  --dur-base:   .5s;
  --dur-reveal: .75s;
  --ease-standard: cubic-bezier(.22, 1, .36, 1);
  --ease-reveal:   cubic-bezier(.19, 1, .22, 1);
  --ease-depart:   cubic-bezier(.55, .06, .68, .19);
}
@media (max-width: 768px) {
  :root {
    --dur-fast: .44s; --dur-base: .62s; --dur-reveal: .95s;
    --ease-standard: cubic-bezier(.26, 1, .34, 1);
  }
}
@media (prefers-reduced-motion: reduce) {
  :root { --dur-fast: .01s; --dur-base: .01s; --dur-reveal: .01s; }
}

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

/* TEX-1249 — a tiled 300px fibre plate, not a live filter. The warmth is the
   load-bearing half; the fibre is what stops ink masses reading as PNGs. */
body {
  background-color: var(--paper);
  background-image: url("assets/paper.png");
  background-repeat: repeat;
  color: var(--ink);
  font-family: var(--body);
  font-size: 1rem; line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}
body::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(140% 110% at 50% 36%, transparent 70%, rgb(0 0 0 / .018) 100%);
}

img, svg { display: block; max-width: 100%; }
img { image-orientation: from-image; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
a { color: inherit; }
hr { display: none; }
::selection { background: var(--rose-300); color: var(--rose-900); }
:focus-visible { outline: 2px solid var(--rose); outline-offset: 3px; }

.skip {
  position: absolute; left: 1rem; top: -4rem; z-index: 200;
  background: var(--ink); color: var(--paper); padding: .75rem 1.25rem;
  font-family: var(--label); font-size: .75rem; letter-spacing: .1em;
  text-transform: uppercase;
  transition: top var(--dur-fast) var(--ease-standard);
}
.skip:focus { top: 1rem; }

.wrap { width: min(1240px, 100% - 3rem); margin-inline: auto; }
@media (max-width: 640px) { .wrap { width: min(1240px, 100% - 2rem); } }

/* ── Type ───────────────────────────────────────────────────────────────── */
.label {
  font-family: var(--label); font-size: .625rem; font-weight: 500;
  letter-spacing: .2em; text-transform: uppercase; color: var(--ink-faint);
}

/* ==========================================================================
   NAV — two buttons, centred. Mail icon right.
   ========================================================================== */
.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  padding: 1.125rem clamp(1rem, 3vw, 2.5rem);
  border-bottom: 1px solid rgba(0,0,0,0);
  transition: padding var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard),
              background var(--dur-fast) var(--ease-standard);
}
.nav[data-scrolled="true"] {
  padding-block: .8125rem;
  border-color: rgba(26,22,20,.12);
  background: rgba(247,242,234,.88);
  backdrop-filter: blur(14px) saturate(1.1);
}
@supports not (backdrop-filter: blur(1px)) {
  .nav[data-scrolled="true"] { background: rgba(247,242,234,.98); }
}
.nav > .mail { grid-column: 3; justify-self: end; }

.nav__switch {
  grid-column: 2; justify-self: center;
  position: relative; display: flex; gap: .25rem;
  padding: .25rem; border: 1px solid rgba(26,22,20,.18); border-radius: 999px;
  background: rgba(247,242,234,.72);
  backdrop-filter: blur(10px);
}
.nav__switch button {
  position: relative; z-index: 1;
  padding: .5rem clamp(.875rem, 3vw, 1.375rem); border-radius: 999px;
  font-family: var(--label); font-size: .625rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase; white-space: nowrap;
  color: var(--ink-dim);
  transition: color var(--dur-fast) var(--ease-standard);
}
.nav__switch button[aria-current="true"] { color: var(--paper); }
.nav__switch button:not([aria-current="true"]):hover { color: var(--ink); }
.nav__ind {
  position: absolute; z-index: 0; top: .25rem; bottom: .25rem; left: .25rem;
  border-radius: 999px; background: var(--ink);
  transition: transform var(--dur-base) var(--ease-standard),
              width var(--dur-base) var(--ease-standard);
}
@media (prefers-reduced-motion: reduce) { .nav__ind { transition: none; } }

.mail {
  display: inline-grid; place-content: center;
  width: 44px; height: 44px; flex: none;
  background: var(--ink); color: var(--paper); border-radius: 50%;
  transition: background var(--dur-fast) var(--ease-standard),
              transform var(--dur-fast) var(--ease-standard);
}
.mail svg { width: 17px; height: 17px; }
.mail:hover  { background: var(--rose); }
.mail:active { background: var(--rose-900); transform: scale(.95); }
.mail--lg { width: 64px; height: 64px; }
.mail--lg svg { width: 23px; height: 23px; }

/* ==========================================================================
   VIEWS — home departs left, about arrives from the right. MOT-1307.
   ========================================================================== */
.view[hidden] { display: none; }
@keyframes viewOutLeft  { to   { opacity: 0; transform: translateX(-8%); } }
@keyframes viewOutRight { to   { opacity: 0; transform: translateX(8%); } }
@keyframes viewInRight  { from { opacity: 0; transform: translateX(8%); } }
@keyframes viewInLeft   { from { opacity: 0; transform: translateX(-8%); } }
.view[data-anim="out-left"]  { animation: viewOutLeft  .42s var(--ease-depart) both; }
.view[data-anim="out-right"] { animation: viewOutRight .42s var(--ease-depart) both; }
.view[data-anim="in-right"]  { animation: viewInRight  .58s var(--ease-standard) .14s both; }
.view[data-anim="in-left"]   { animation: viewInLeft   .58s var(--ease-standard) .14s both; }
/* No scroll lock during the swap. This used to be overflow: hidden, which
   froze the page for the ~720ms the animation ran: scroll down in that
   window and nothing moved, which read as the page hanging. The horizontal
   overflow from the +/-8% slide is already handled by the overflow-x: clip
   on html, body, which is all this rule was actually needed for. */
/* The outgoing view leaves the flow for the length of the swap. In flow, the
   incoming view stacked BELOW it: the document briefly grew to the height of
   both views and then collapsed back, which jumped every scroll-progress job
   (the ground, the river) twice per swap, and the entrance animation played
   off screen below the fold. Out of flow they occupy the same box, which is
   what the cross-slide was always meant to be. */
body[data-swapping] .view[data-anim^="out-"] {
  position: absolute; top: 0; left: 0; width: 100%;
}
/* Scroll anchoring otherwise tries to hold the reader's place when the
   outgoing view leaves the flow, and fights the jump to the top. */
.view { overflow-anchor: none; }
@media (prefers-reduced-motion: reduce) { .view[data-anim] { animation: none; } }

/* ── Section heads ──────────────────────────────────────────────────────── */
.sec__head .label { display: flex; align-items: center; gap: 1rem; }

/* ── What I do ─────────────────────────────────────────────────────────── */
.craft { padding-block: clamp(3rem, 8vw, 6rem) clamp(2rem, 5vw, 3.5rem); }
.craft__list { display: grid; }
.craft__row {
  position: relative;
  display: grid; gap: .25rem;
  padding-block: clamp(1.25rem, 3vw, 2rem);
  
}
.craft__row:last-child {  }
.craft__row::before {
  content: ""; position: absolute; left: 0; top: -1px; height: 1px; width: 100%;
  background: var(--rose); transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-reveal) var(--ease-standard);
}
.craft__row:hover::before { transform: scaleX(1); }
.craft__row h3 {
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.6rem, 4.4vw, 2.5rem); line-height: .98;
}
.craft__row p { color: var(--ink-dim); max-width: 46ch; }
@media (min-width: 761px) {
  .craft__row { grid-template-columns: minmax(0,.9fr) minmax(0,1.4fr); gap: 2rem; align-items: baseline; }
}

/* ── Pitch ──────────────────────────────────────────────────────────────── */
/* ==========================================================================
   ABOUT
   ========================================================================== */
/* Mirrors the hero exactly: portrait left at the same scale, vertical title
   right, copy running full width beneath. */

/* Vertical rose title on the right — the hero's move, repeated once, in the
   accent. Rose is the surname; this is the one place it gets to be display. */

/* The mask reveal is opt-in: JS adds .is-masked, so if JS never runs the
   title is simply visible rather than permanently clipped. */

/* Same scale as the hero portrait */

.bio__copy {
  grid-column: 1 / -1; grid-row: 2;
  display: grid; gap: 1rem; margin-top: 1rem;
}
.bio__copy p { color: var(--ink-dim); max-width: 60ch; }
.bio__lead {
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.5rem, 4.4vw, 2.5rem); line-height: 1.02;
  color: var(--ink) !important; max-width: 22ch !important;
}

@media (max-width: 760px) {
  
  
}
@media (prefers-reduced-motion: reduce) {  }

/* ── Work ───────────────────────────────────────────────────────────────── */
.work { padding-block: clamp(2rem, 5vw, 3rem); }

/* ── Background grid ────────────────────────────────────────────────────── */
.ground { padding-block: clamp(3rem, 7vw, 5rem); margin-block: clamp(3rem, 7vw, 5rem); }

/* ── How I work ─────────────────────────────────────────────────────────── */
.creed { padding-block: clamp(3rem, 7vw, 5rem); }
.creed ul { display: grid; gap: .125rem; }
.creed li { list-style: none; }
.creed span {
  font-family: var(--display); font-weight: 600; text-transform: uppercase;
  font-size: clamp(1.3rem, 4.4vw, 2.5rem); line-height: 1.16;
  display: inline-flex; gap: .5em; align-items: baseline;
}
.creed em { color: var(--rose-300); font-style: normal; }

/* ── Contact ────────────────────────────────────────────────────────────── */

/* ── Reveal — three behaviours by weight, not one blanket fade ──────────── */
.rv { opacity: 0; transform: translateY(20px); }
.rv[data-in] {
  opacity: 1; transform: none;
  transition: opacity var(--dur-reveal) var(--ease-reveal),
              transform var(--dur-reveal) var(--ease-reveal);
}
.work__card.rv { transform: translateY(38px); }
.work__card.rv[data-in] { transition: opacity .95s var(--ease-reveal), transform .95s var(--ease-reveal); }
.craft__row.rv, .creed li.rv, .ground__grid > div.rv { transform: translateY(12px); }
.craft__row.rv[data-in], .creed li.rv[data-in], .ground__grid > div.rv[data-in] {
  transition: opacity .52s var(--ease-reveal), transform .52s var(--ease-reveal);
}
.creed li.rv:nth-child(1)[data-in] { transition-delay: 0s; }
.creed li.rv:nth-child(2)[data-in] { transition-delay: .055s; }
.creed li.rv:nth-child(3)[data-in] { transition-delay: .11s; }
.creed li.rv:nth-child(4)[data-in] { transition-delay: .165s; }
.creed li.rv:nth-child(5)[data-in] { transition-delay: .22s; }
.creed li.rv:nth-child(6)[data-in] { transition-delay: .275s; }
.rv[data-in].d1 { transition-delay: .08s; }
.rv[data-in].d2 { transition-delay: .16s; }
.rv[data-in].d3 { transition-delay: .24s; }
@media (prefers-reduced-motion: reduce) {
  .rv, .rv[data-in] { opacity: 1; transform: none; transition: none; }
}

/* Character rise on titles — base + i × 22ms */
[data-rise] .word { display: inline-block; white-space: nowrap; }
[data-rise] .ch { display: inline-block; opacity: 0; transform: translateY(.4em); }
[data-rise][data-in] .ch {
  opacity: 1; transform: none;
  transition: opacity .5s var(--ease-reveal), transform .6s var(--ease-reveal);
  transition-delay: calc(var(--i) * 22ms);
}
@media (prefers-reduced-motion: reduce) { [data-rise] .ch { opacity: 1; transform: none; } }

/* ==========================================================================
   ADDITIONS
   ========================================================================== */

/* ── Hero bar: sits in its own grid row UNDER the portrait, never over it ─ */

/* ── Built: three cards, small, above the fold-and-a-bit ───────────────── */
.built { padding-block: clamp(2.5rem, 6vw, 4rem) clamp(1.5rem, 4vw, 2.5rem); }

/* ── City plate: bottom-aligned, low opacity, behind the closing blocks ── */
/* .closer no longer carries a city plate; the footer owns it. This rule was
   left mangled by an earlier edit and was applying opacity:.17 to .foot,
   which is why the whole footer washed out to light grey. */
.closer { position: relative; }

.closer > *, .foot > * { position: relative; z-index: 1; }

/* ── Slats: LAY-819 flex-redistribute, applied to photos. Click a slat and
      it takes the room; the others give it up. Container never changes. ── */
.slats {
  display: flex; gap: 6px;
  height: clamp(300px, 52vh, 520px);
  width: 100%;
}
.slat {
  position: relative; flex: 1 1 0; min-width: 44px;
  overflow: hidden; padding: 0;
  
  transition: flex-grow var(--dur-base) cubic-bezier(.2,.8,.25,1);
}
/* 3.6 rather than the card's 2.2: these are photographs, not tabs, and the
   open one has to be legible as an image. Divergence, logged. */
.slat[aria-selected="true"] { flex-grow: 3.6; }

.slat__img {
  position: absolute; inset: 0;
  background-image: var(--img);
  background-size: cover; background-position: center;
  filter: saturate(.72) contrast(1.05);
  transition: filter var(--dur-base) var(--ease-standard),
              transform var(--dur-reveal) var(--ease-standard);
}
.slat[aria-selected="true"] .slat__img { filter: saturate(1) contrast(1.04); }
.slat:hover .slat__img { transform: scale(1.03); }

.slat__cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: .875rem 1rem;
  font-family: var(--label); font-size: .5625rem; letter-spacing: .18em;
  text-transform: uppercase; text-align: left; color: #fff;
  white-space: nowrap; overflow: hidden;
  background: linear-gradient(transparent, rgb(26 22 20 / .72));
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--dur-base) var(--ease-standard),
              transform var(--dur-base) var(--ease-standard);
}
.slat[aria-selected="true"] .slat__cap { opacity: 1; transform: none; }
.slat:focus-visible { outline: 2px solid var(--rose); outline-offset: 2px; }

@media (max-width: 640px) {
  .slats { height: clamp(260px, 46vh, 380px); gap: 4px; }
  .slat[aria-selected="true"] { flex-grow: 5; }
  .slat__cap { font-size: .5rem; letter-spacing: .14em; padding: .75rem; }
}
@media (prefers-reduced-motion: reduce) {
  .slat, .slat__img, .slat__cap { transition: none; }
}

/* ── Education grid: structured as a definition list, not loose blocks ─── */

/* ── Creed: bigger. It is the loudest block on the About page. ─────────── */
.creed span { font-size: clamp(1.6rem, 5.6vw, 3.25rem); line-height: 1.1; }

/* ── With every rule and band removed, SPACE is the only separator left.
      Vertical rhythm therefore has to vary a lot between blocks, or the page
      turns into one undifferentiated column (tell #19). ─────────────────── */
.built  { padding-block: clamp(3.5rem, 9vw, 6rem) clamp(1.5rem, 4vw, 2.5rem); }
.craft  { padding-block: clamp(1.5rem, 4vw, 2.5rem) clamp(4rem, 11vw, 7rem); }
.work   { padding-block: clamp(3rem, 8vw, 5rem) clamp(4rem, 10vw, 6.5rem); }
.ground { padding-block: clamp(2rem, 5vw, 3rem) clamp(4.5rem, 12vw, 8rem); margin-block: 0; }
.reel   { padding-block: clamp(1.5rem, 4vw, 2.5rem) clamp(5rem, 13vw, 8.5rem); }
.creed  { padding-block: clamp(2rem, 5vw, 3rem) clamp(5rem, 13vw, 8rem); }
.bio    { padding-block: clamp(6rem, 13vw, 9rem) clamp(2.5rem, 6vw, 4rem); }

.craft__row { padding-block: clamp(1rem, 2.4vw, 1.5rem); }

/* The section that was carrying a band now carries a size change instead */

/* ==========================================================================
   STAGE — the portrait is hoisted out of the views, so a view swap cannot
   move it. Only the vertical title, the line under it, and the content
   below ever animate.
   ========================================================================== */
.stage {
  position: relative;
  display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: end;
  padding: 5rem clamp(1rem,3vw,2.5rem) 0;
  min-height: 92svh;
  overflow: clip;
}
.stage::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: repeating-linear-gradient(90deg,
    rgba(26,22,20,.05) 0 2px, rgba(26,22,20,0) 2px 46px);
  mask-image: linear-gradient(90deg,#000 0%,#000 50%,transparent 88%);
}
.stage__portrait {
  grid-column: 1; grid-row: 1; z-index: 1;
  justify-self: start; align-self: end;
  width: min(70vw, 600px);
  margin-left: clamp(-2.5rem,-4vw,-1rem);
}
.stage__portrait img {
  width: 100%; height: auto;
  mask-image: linear-gradient(to bottom,#000 93%,transparent 100%);
}

.stage__title { grid-column: 2; grid-row: 1; z-index: 2; align-self: center; justify-self: end; }
.ttl {
  writing-mode: vertical-rl;
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(3rem, 12.5vh, 8.5rem); line-height: .88; white-space: nowrap;
}
.ttl i { font-style: normal; color: var(--rose); }
.ttl--rose { color: var(--rose); }
.ttl[hidden] { display: none; }

@media (max-width: 620px) { .stage__place { margin-left: 0; } }
@media (max-width: 760px) {
  .stage { padding-inline: clamp(.75rem,3vw,2.5rem); }
  .stage__portrait { width: min(84vw,480px); margin-left: -1.75rem; }
  .ttl { font-size: clamp(2.4rem,9.5vh,5rem); }
}

.ttl { clip-path: inset(0 0 100% 0); }
.stage[data-ready] .ttl { clip-path: inset(0 0 0 0); transition: clip-path .95s var(--ease-reveal) .12s; }
.stage__portrait { opacity: 0; transform: translateY(20px); }
.stage[data-ready] .stage__portrait {
  opacity: 1; transform: none;
  transition: opacity .9s var(--ease-reveal) .3s, transform .9s var(--ease-reveal) .3s;
}
.stage[data-ready] .stage__bar {
  opacity: 1; transform: none;
  transition: opacity .7s var(--ease-reveal) .6s, transform .7s var(--ease-reveal) .6s;
}
@media (prefers-reduced-motion: reduce) {
  .ttl { clip-path: none !important; }
.stage__portrait { opacity: 1 !important; transform: none !important; }
}

/* ── Intro + differentiator: centred, against the left-aligned lists ───── */
.edge {
  padding-block: clamp(2rem,6vw,4rem) clamp(3rem,8vw,5rem);
  display: grid; gap: .375rem; justify-items: center; text-align: center;
}

/* ── Closer ────────────────────────────────────────────────────────────── */
.closer {
  padding-block: clamp(4rem,11vw,7rem) clamp(4rem,11vw,7rem);
  display: grid; gap: 1.25rem; justify-items: center; text-align: center;
}
.closer__lead {
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(2.8rem,11vw,6.5rem); line-height: .86;
}
.closer__body { color: var(--ink-dim); max-width: 38ch; }
.closer .link-go { margin-top: .5rem; }

/* ── Bio ───────────────────────────────────────────────────────────────── */
.bio { padding-block: clamp(1.5rem,4vw,2.5rem) clamp(3rem,8vw,5rem); display: grid; gap: 1.25rem; }
.bio__lead {
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.6rem,4.6vw,2.6rem); line-height: 1.02; max-width: 22ch;
}
.bio__copy { display: grid; gap: 1rem; }
.bio__copy p { color: var(--ink-dim); max-width: 60ch; }

/* ── Education, highlighted and centred ────────────────────────────────── */
.edu {
  padding-block: clamp(3rem,8vw,5rem) clamp(3rem,8vw,5rem);
  display: grid; gap: .625rem; justify-items: center; text-align: center;
}
.edu__school {
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(2rem,7vw,4rem); line-height: .94;
}
.edu__degree {
  font-family: var(--label); font-size: .6875rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-dim);
}
.edu__facts {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: clamp(1.25rem,4vw,3rem); margin-top: 1.25rem;
}
.edu__facts span {
  display: grid; gap: .25rem;
  font-family: var(--label); font-size: .5625rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-faint);
}
.edu__facts b {
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.1rem,2.8vw,1.5rem); line-height: 1; color: var(--ink);
  letter-spacing: 0;
}
.edu__note { color: var(--ink-dim); max-width: 46ch; margin-top: .75rem; font-size: .9375rem; }

/* ── Writing rows ──────────────────────────────────────────────────────── */
.writing { display: grid; gap: .25rem; margin-top: clamp(3rem,8vw,5rem); }

/* ── Footer ────────────────────────────────────────────────────────────── */
.foot {
  position: relative; overflow: clip;
  background: var(--ink); color: var(--paper);
  margin-top: clamp(3rem,8vw,5rem);
  padding-block: clamp(3rem,8vw,5rem) clamp(1.5rem,3vw,2rem);
}
.foot__city { position: absolute; inset: auto 0 0 0; z-index: 0; pointer-events: none; line-height: 0; }
.foot__city img {
  width: 100%; height: clamp(200px,32vw,420px);
  object-fit: cover; object-position: bottom center; display: block;
  opacity: .18; filter: invert(1) sepia(1) saturate(.3) brightness(1.4);
}
.foot > * { position: relative; z-index: 1; }
.foot__in {
  display: grid; gap: clamp(1.75rem,5vw,3rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%,200px),1fr));
}
.foot__block { display: grid; gap: .5rem; align-content: start; }
.foot__k {
  font-family: var(--label); font-size: .5625rem; letter-spacing: .2em;
  text-transform: uppercase; color: rgb(247 242 234 / .45);
}
.foot__mail {
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.2rem,3.4vw,1.9rem); line-height: 1; text-decoration: none;
  transition: color var(--dur-fast) var(--ease-standard);
}
.foot__mail:hover { color: var(--rose-300); }
.foot__links { display: grid; gap: .375rem; }
.foot__links a {
  font-family: var(--label); font-size: .6875rem; letter-spacing: .16em;
  text-transform: uppercase; text-decoration: none; color: rgb(247 242 234 / .74);
  width: max-content;
  transition: color var(--dur-fast) var(--ease-standard);
}
.foot__links a:hover { color: var(--paper); }
.foot__meta {
  font-family: var(--label); font-size: .6875rem; letter-spacing: .16em;
  text-transform: uppercase; color: rgb(247 242 234 / .55);
}
.foot__rule {
  display: flex; flex-wrap: wrap; gap: .5rem 2rem; justify-content: space-between;
  margin-top: clamp(2.5rem,7vw,4rem); padding-top: 1.25rem;
  border-top: 1px solid rgb(247 242 234 / .16);
}
.foot :focus-visible { outline-color: var(--paper); }

/* ── Rhythm. Space is the only separator, so it has to vary. ───────────── */
.built  { padding-block: 0 clamp(1.5rem,4vw,2.5rem); }
.craft  { padding-block: clamp(1.5rem,4vw,2.5rem) 0; }
.creed  { padding-block: clamp(2rem,5vw,3rem) clamp(3rem,8vw,5rem); }
.reel   { padding-block: clamp(1.5rem,4vw,2.5rem) clamp(2rem,5vw,3rem); }
.ground { padding-block: 0 clamp(3rem,8vw,5rem); margin-block: 0; }
.work   { padding-block: 0 clamp(2rem,5vw,3rem); }

/* ── Buttons: restored. The rule that sized these was destroyed by an earlier
      edit, which is why the arrows rendered at container size. ─────────── */
.link-go {
  display: inline-flex; align-items: center; gap: .625rem;
  width: max-content; padding: .8125rem 1.25rem; min-height: 44px;
  border: 1px solid rgba(26,22,20,.24); border-radius: 999px;
  font-family: var(--label); font-size: .625rem; letter-spacing: .18em;
  text-transform: uppercase;
  transition: background var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.link-go svg {
  width: 16px; height: 12px; flex: none;
  transition: transform var(--dur-fast) var(--ease-standard);
}
.link-go:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.link-go:hover svg { transform: translateX(4px); }
.link-go:active { background: var(--rose-900); border-color: var(--rose-900); }

/* The mail control is a fixed circle and must never be stretchable by a
   grid or flex parent. */
.mail, .mail--lg { flex: 0 0 auto; align-self: center; justify-self: center; }
.mail     { width: 44px; height: 44px; min-width: 44px; min-height: 44px; }
.mail--lg { width: 64px; height: 64px; min-width: 64px; min-height: 64px; }

/* ── Craft rows: each one pops and turns rose as it reaches the middle of
      the viewport. The active row also opens its skill set. Together these
      slow the scroll down, which is the point. ─────────────────────────── */
.craft__row {
  position: relative;
  transition: color var(--dur-base) var(--ease-standard),
              transform var(--dur-base) var(--ease-standard);
}
.craft__row h3 { transition: color var(--dur-base) var(--ease-standard); }

.craft__row[data-live] { transform: translateX(clamp(.4rem, 1vw, 1rem)); }
.craft__row[data-live] h3 { color: var(--rose); }

.craft__skills {
  display: grid; grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-base) cubic-bezier(.215,.61,.355,1);
  grid-column: 2;
}
.craft__skills > * { min-height: 0; }
.craft__row[data-live] .craft__skills,
.craft__row:hover .craft__skills,
.craft__row:focus-visible .craft__skills { grid-template-rows: 1fr; }

.craft__skills li {
  list-style: none; display: inline-block;
  font-family: var(--label); font-size: .5625rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-dim);
  border: 1px solid rgba(26,22,20,.2);
  padding: .3125rem .5rem; margin: .75rem .25rem 0 0;
  opacity: 0; transform: translateY(4px);
  transition: opacity var(--dur-fast) var(--ease-standard),
              transform var(--dur-fast) var(--ease-standard);
}
.craft__row[data-live] .craft__skills li,
.craft__row:hover .craft__skills li { opacity: 1; transform: none; }
.craft__row[data-live] .craft__skills li:nth-child(n+1) { transition-delay: calc(var(--n,0) * 32ms); }

@media (prefers-reduced-motion: reduce) {
  .craft__row, .craft__row h3, .craft__skills, .craft__skills li { transition: none; }
  .craft__row[data-live] { transform: none; }
}

/* ── Hero tagline: fills the dead space on the right at desktop widths.
      Hidden below 1100px, where there is no room and the hero is not empty. */
.stage__tag { display: none; }
@media (min-width: 1100px) {
  .stage__tag {
    display: block;
    grid-column: 1 / -1; grid-row: 1; z-index: 2;
    align-self: start; justify-self: end;
    max-width: 22ch; margin-top: 1rem; padding-right: clamp(6rem, 11vw, 10rem);
    font-family: var(--body); font-style: italic; font-weight: 400;
    font-size: clamp(1rem, 1.35vw, 1.25rem); line-height: 1.5;
    color: var(--ink-dim); text-align: right; text-wrap: balance;
  }
}

/* ── Footer skyline: pin to the bottom and render the ink as paper.
      invert(1) on a rose plate produces a mint-grey slab, which is what was
      showing as a light block across the footer. brightness(0) invert(1)
      flattens every inked pixel to white and leaves transparency alone. ── */
.foot__city { position: absolute; left: 0; right: 0; bottom: 0; top: auto; }
.foot__city img {
  filter: brightness(0) invert(1);
  opacity: .14;
  object-position: bottom center;
}

/* The closing paragraph carries the whole ask, so it is set well above body
   size at desktop rather than left as small print under the headline. */
.closer__body {
  font-size: clamp(1.05rem, 1.9vw, 1.55rem);
  line-height: 1.45; max-width: 46ch; color: var(--ink-dim);
}

/* The 0fr -> 1fr collapse needs ONE child to act on, so the chips live in a
   ul inside the grid. Without the wrapper each chip was its own grid row and
   the row never actually collapsed, which is why the list left a large gap. */
.craft__skills { display: grid; grid-template-rows: 0fr; grid-column: 2;
  transition: grid-template-rows var(--dur-base) cubic-bezier(.215,.61,.355,1); }
.craft__skills > ul { min-height: 0; overflow: hidden; list-style: none;
  margin: 0; padding: 0; }
.craft__row[data-live] .craft__skills,
.craft__row:hover .craft__skills,
.craft__row:focus-within .craft__skills { grid-template-rows: 1fr; }
.craft__skills li { margin: .75rem .25rem 0 0; }
@media (max-width: 760px) { .craft__skills { grid-column: 1; } }

/* ── Rose duotone on the work imagery, matching the baked portrait.
      grayscale supplies the luminance, the rose plate supplies hue and
      saturation via mix-blend-mode: color. Lifts on hover so the visitor can
      actually read the site being shown. ──────────────────────────────── */
/* the nightbill plate is already rose, so it must not be tinted again */

@media (prefers-reduced-motion: reduce) {
  .built__shot img, .work__shot img, .built__shot::after, .work__shot::after { transition: none; }
}

/* ── Hero tagline, set properly: two comfortable lines, rose rule above,
      clear of the vertical name. ─────────────────────────────────────── */
@media (min-width: 1100px) {
  .stage__tag {
    max-width: 15ch;
    margin-top: 1.75rem;
    padding-right: clamp(7rem, 12vw, 11rem);
    font-size: clamp(1.05rem, 1.5vw, 1.4rem);
    line-height: 1.35;
    color: var(--ink);
    letter-spacing: -.005em;
  }
  .stage__tag::before {
    content: ""; display: block; width: 40px; height: 2px;
    background: var(--rose); margin: 0 0 .875rem auto;
  }
}
@media (min-width: 1500px) { .stage__tag { max-width: 18ch; } }

/* Chips must shrink-wrap. They were inheriting block layout and rendering as
   full-width bars. Flex-wrap on the ul is more robust than inline-block. */
.craft__skills > ul { display: flex; flex-wrap: wrap; gap: .375rem; }
.craft__skills li {
  display: inline-flex; width: auto; flex: 0 0 auto;
  margin: 0; padding: .3125rem .5625rem;
  white-space: nowrap;
}
.craft__skills > ul { padding-top: .875rem; }

/* Footer was 444px tall at 390px wide around 269px of content: the padding
   and the city plate were both sized off desktop assumptions. */
@media (max-width: 860px) {
  .foot { padding-block: clamp(2rem, 7vw, 3rem) clamp(1rem, 3vw, 1.5rem);
          margin-top: clamp(2rem, 7vw, 3rem); }
  .foot__in { gap: 1.5rem; }
  .foot__rule { margin-top: 1.75rem; padding-top: 1rem; }
  .foot__city img { height: clamp(110px, 30vw, 200px); opacity: .12; }
}

/* ── TEX-1006 Isoline contour field ────────────────────────────────────
   Nested hairlines at constant stroke weight, running off all four edges.
   Apparent tone comes ONLY from line spacing, never from alpha or a greyed
   stroke, so the ink stays full-strength rose and the interval is widened
   to hold coverage near 7% where the type sits. */
.craft { position: relative; }
.topo {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  color: var(--rose);
}
.craft__list { position: relative; z-index: 1; }

/* ── Hero tagline: each part on its own line, set beside the portrait rather
      than stranded against the right edge. Home view only. ────────────── */
.stage__tag { display: none; }
.stage__tag[hidden] { display: none !important; }
@media (min-width: 1100px) {
  .stage__tag {
    display: grid; gap: .5rem;
    position: absolute; z-index: 2;
    top: clamp(8rem, 15vh, 11rem);
    left: min(64vw, 560px);
    max-width: 20ch;
    text-align: left;
    padding: 0; margin: 0;
  }
  .stage__tag::before {
    content: ""; width: 44px; height: 2px; background: var(--rose);
    margin: 0 0 .375rem 0;
  }
  .stage__tag span {
    font-family: var(--body); font-style: italic; font-weight: 400;
    font-size: clamp(1.05rem, 1.4vw, 1.35rem); line-height: 1.3;
    color: var(--ink); letter-spacing: -.005em;
  }
  .stage__tag span:last-child { color: var(--ink-dim); margin-top: .25rem; }
}

/* Writing rows sit centred as a block, so they read as a coda to the work
   grid rather than a misaligned third column. */
.writing { width: max-content; max-width: 100%; margin-inline: auto; }
@media (max-width: 640px) {
  .writing { width: 100%; }
  .writing__row { grid-template-columns: 64px minmax(0,1fr); }
  .writing__row img { width: 64px; height: 44px; }
}

/* Work and built card text centres under its thumbnail at desktop, so the
   three columns read as a set rather than three ragged left edges. */
@media (min-width: 761px) {
  .work__card, .built__card { text-align: center; justify-items: center; }
  .work__card p, .built__card p { max-width: 30ch; margin-inline: auto; }
}

/* Writing rows: up a step so they hold their own next to the work grid. */
@media (max-width: 640px) {
  .writing__row { grid-template-columns: 76px minmax(0,1fr); gap: .25rem 1rem; }
  .writing__row img { width: 76px; height: 52px; }
}

/* ── Slats: same rose duotone as the work imagery, lifting on hover, plus a
      progress bar that shows the auto-advance timer running. ──────────── */
.slat__img { filter: grayscale(1) contrast(1.06); }
.slat::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: var(--rose-900); mix-blend-mode: color;
  pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-standard);
}
.slat:hover::after, .slat:focus-visible::after { opacity: 0; }
.slat:hover .slat__img, .slat:focus-visible .slat__img { filter: grayscale(0) contrast(1.02); }
.slat__cap { z-index: 2; }

/* Timer rail. Runs only on the open slat, resets on every change, and is
   removed the moment the visitor takes control. Linear, because it reports
   elapsed time and easing it would misreport progress. */
.slat__timer {
  position: absolute; left: 0; bottom: 0; height: 2px; z-index: 3;
  width: 100%; transform: scaleX(0); transform-origin: left;
  background: var(--rose-100);
  pointer-events: none;
}
.slat[aria-selected="true"] .slat__timer { animation: slatTick var(--tick, 3.6s) linear forwards; }
.slats[data-taken] .slat__timer,
.slats[data-paused] .slat__timer { animation-play-state: paused; }
.slats[data-taken] .slat__timer { opacity: 0; }
@keyframes slatTick { from { transform: scaleX(0); } to { transform: scaleX(1); } }

@media (prefers-reduced-motion: reduce) {
  .slat__timer { display: none; }
  .slat__img, .slat::after { transition: none; }
}

/* No rule above the tagline; it shows on both views. */
.stage__tag::before { content: none !important; display: none !important; }

/* The Business Strategy Game result reads as one continuous sentence rather
   than a fact chip plus an orphaned footnote. */
.edu__note {
  max-width: 62ch; font-size: clamp(.9375rem, 1.4vw, 1.0625rem);
  line-height: 1.5; margin-top: 1.25rem; text-wrap: pretty;
}

/* ── TEX-1006 page-wide ────────────────────────────────────────────────
   The contour field now runs behind the whole page rather than one section.
   Divergence from the card, logged: it says tone must come from spacing and
   never from alpha, but the owner asked for a lower-opacity wash, so this
   drops to .38 and widens the interval as well. The mask is what makes it
   "cut off in places" - the field dissolves and returns instead of running
   edge to edge at a constant density. */
.topo {
  position: fixed; inset: 0; z-index: 0;
  width: 100vw; height: 100vh;
  pointer-events: none;
  color: var(--rose);
  opacity: .38;
  -webkit-mask-image:
    radial-gradient(60% 45% at 18% 22%, #000 0 38%, transparent 78%),
    radial-gradient(52% 38% at 82% 48%, #000 0 30%, transparent 74%),
    radial-gradient(70% 50% at 44% 84%, #000 0 34%, transparent 80%),
    radial-gradient(40% 30% at 92% 12%, #000 0 26%, transparent 72%);
  mask-image:
    radial-gradient(60% 45% at 18% 22%, #000 0 38%, transparent 78%),
    radial-gradient(52% 38% at 82% 48%, #000 0 30%, transparent 74%),
    radial-gradient(70% 50% at 44% 84%, #000 0 34%, transparent 80%),
    radial-gradient(40% 30% at 92% 12%, #000 0 26%, transparent 72%);
  -webkit-mask-composite: source-over;
  mask-composite: add;
}
/* everything the page draws sits above the field */
main, .stage, .body, .foot, .nav { position: relative; }
main { z-index: 1; }
.nav { z-index: 100; }
.craft { position: relative; }
.craft__list { position: relative; z-index: 1; }

/* The city plate was at .14 and effectively invisible against the ink. */
.foot__city img { opacity: .3; }
@media (max-width: 860px) { .foot__city img { opacity: .26; } }

/* More contour, less presence. Density doubles while opacity drops well
   below where it competed with the type. The mask now covers far more of the
   page so the field reads as continuous terrain with occasional clearings,
   rather than four isolated patches. */
.topo {
  opacity: .17;
  -webkit-mask-image:
    radial-gradient(78% 62% at 14% 18%, #000 0 62%, transparent 96%),
    radial-gradient(72% 58% at 88% 40%, #000 0 58%, transparent 94%),
    radial-gradient(86% 66% at 46% 88%, #000 0 60%, transparent 96%),
    radial-gradient(64% 52% at 96% 84%, #000 0 54%, transparent 92%),
    radial-gradient(58% 48% at 6% 66%, #000 0 50%, transparent 90%);
  mask-image:
    radial-gradient(78% 62% at 14% 18%, #000 0 62%, transparent 96%),
    radial-gradient(72% 58% at 88% 40%, #000 0 58%, transparent 94%),
    radial-gradient(86% 66% at 46% 88%, #000 0 60%, transparent 96%),
    radial-gradient(64% 52% at 96% 84%, #000 0 54%, transparent 92%),
    radial-gradient(58% 48% at 6% 66%, #000 0 50%, transparent 90%);
}

/* Type-safe zone. The card forbids body copy over a field above ~10%
   coverage, so the craft rows carry a soft paper halo that clears the lines
   immediately behind the words without putting a visible box on the page. */
.craft__row h3,
.craft__row p {
  text-shadow: 0 0 10px var(--paper), 0 0 4px var(--paper), 0 0 2px var(--paper);
}
.craft__skills li { background: color-mix(in srgb, var(--paper) 88%, transparent); }

/* ══ Craft rows: ONE colour mechanism, not two ═════════════════════════
   Scroll position owns the rose state. Hover only opens the skills. Having
   both set the colour is what made Engineering look stuck red after the
   pointer left: it was still the live row, so the colour never returned. */
.craft__row:hover h3 { color: inherit; }
.craft__row[data-live] h3 { color: var(--rose); }
.craft__row:hover:not([data-live]) { transform: none; }

/* ══ Field: quieter again, and the type gets a real clearing ═══════════ */
.topo { opacity: .09; }

.craft__row h3, .craft__row p {
  text-shadow: 0 0 14px var(--paper), 0 0 8px var(--paper),
               0 0 4px var(--paper), 0 0 2px var(--paper);
}
.craft__skills li {
  background: var(--paper);
  border-color: rgba(26,22,20,.28);
  color: var(--ink);
}

/* ══ Slats open on hover, no click required ════════════════════════════ */
.slats:hover .slat[aria-selected="true"]:not(:hover) { flex-grow: 1; }
.slat:hover { flex-grow: 3.6; }
.slat:hover .slat__cap { opacity: 1; transform: none; }
@media (max-width: 640px) { .slat:hover { flex-grow: 5; } }

/* The canvas is now twice the viewport tall so it has room to travel. */
.topo { height: 200vh; will-change: transform; }

/* ══ TYPOGRAPHY AUDIT FIXES ═══════════════════════════════════════════
   Measured at 1440px. Several label sizes had drifted to 9px, which is
   below anything readable, and two footer tokens were carrying their
   contrast on opacity rather than on a real colour. */

/* 9px -> 11px. Mono at 9px with .14em tracking is decorative, not legible. */
.craft__skills li { font-size: .6875rem; letter-spacing: .1em; padding: .375rem .625rem; }

/* 13px -> 14.5px. This is body copy, not a caption. */

/* Footer: real colours instead of alpha, and nothing under 10.5px. */
.foot__k     { font-size: .65625rem; color: rgb(196 188 180); letter-spacing: .18em; }
.foot__links a { font-size: .75rem; color: rgb(226 219 210); }
.foot__links a:hover { color: var(--paper); }
.foot__meta  { font-size: .71875rem; color: rgb(196 188 180); }

/* Measure: the two widest blocks were running past 50ch. */
.craft__row p, .closer__body { max-width: 44ch; }

/* Last of the sub-10.5px text. Nav labels and photo captions were the only
   remaining offenders; both now clear the floor. */
.nav__switch button { font-size: .6875rem; }
.slat__cap { font-size: .6875rem; letter-spacing: .12em; padding: 1rem; }
@media (max-width: 640px) { .slat__cap { font-size: .625rem; letter-spacing: .1em; padding: .75rem; } }

/* ══════════════════════════════════════════════════════════════════════
   THE RIVER
   One continuous form running the full document height. The bed is static;
   the current is a dash segment scrubbed to scroll position, so scrolling
   moves you down it. Linear throughout: scroll has no beginning or end to
   ease, and easing it would make the page fight the wheel.
   ══════════════════════════════════════════════════════════════════════ */
.river {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  overflow: visible;
}
#riverBed {
  fill: none;
  stroke: var(--rose);
  stroke-width: .55;
  opacity: .16;
  vector-effect: non-scaling-stroke;
}
#riverFlow {
  fill: none;
  stroke: var(--rose);
  stroke-width: 1.4;
  opacity: .55;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
}

/* ── The figure drifting alongside the river ─────────────────────────── */
.drift {
  position: relative;
  width: min(38vw, 300px);
  margin: clamp(1rem, 4vw, 3rem) 0 clamp(2rem, 6vw, 4rem) auto;
  margin-right: clamp(-3rem, -5vw, -1rem);   /* breaks past the container */
}
.drift img { width: 100%; height: auto; display: block; }
.drift figcaption {
  font-family: var(--label); font-size: .6875rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-faint);
  margin-top: .75rem; text-align: right;
}
@media (max-width: 700px) {
  .drift { width: min(56vw, 220px); margin-right: -1.5rem; }
}

/* ── Wave-edge handoffs. Sections hand off through a curve rather than a
      hard cut, which is what keeps the scroll feeling continuous. ────── */
.wave {
  display: block; width: 100%; height: clamp(40px, 6vw, 90px);
  margin-block: clamp(-2rem, -4vw, -1rem) 0;
  pointer-events: none;
}
.wave path { fill: var(--paper-2); }

/* ── Reduced motion: the river stops flowing but stays visible. ──────── */
@media (prefers-reduced-motion: reduce) {
  #riverFlow { stroke-dasharray: none !important; stroke-dashoffset: 0 !important; opacity: .3; }
}

/* ══ ERROR HANDLING ═══════════════════════════════════════════════════
   Every image gets a defined failed state. Without this a broken src
   collapses to a bare alt string floating in the layout, which is exactly
   the "random floating text" failure mode. */
img[data-failed] {
  display: grid; place-content: center;
  min-height: 6rem;
  font-family: var(--label); font-size: .6875rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-faint);
  background: var(--paper-2);
  outline: 1px solid rgba(26,22,20,.12); outline-offset: -1px;
}
/* A card whose image failed keeps its shape instead of collapsing. */
.slat[data-failed] .slat__img {
  background: var(--paper-2);
  background-image: none !important;
}
.slat[data-failed]::after { content: none; }

/* Offline / failed-font fallback: the display face has a metric-compatible
   stack so a font failure changes weight, not layout. */
.js-fontfail .ttl,
.js-fontfail .section-title { letter-spacing: -.01em; }

/* justify-items:center made every grid child shrink-wrap, which collapsed the
   thumbnails to zero width. Centre the TEXT, let the shot fill the column. */
@media (min-width: 761px) {
  .work__card, .built__card { justify-items: stretch; text-align: center; }
}

/* The river is sized to the document in JS; body is not a positioned
   ancestor with a resolvable height, so height:100% would only ever give
   one viewport. */
.river { height: auto; }

/* Timer cadence matches the 3.2s interval. */
.slat[aria-selected="true"] .slat__timer { animation-duration: 3.2s; }

/* ══ WEB INTERFACE GUIDELINES ═════════════════════════════════════════ */
:root { color-scheme: light; }

/* Screen-reader-only live region for the reel. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Any figure that reports a count uses tabular figures so it cannot jitter. */
.slat__cap, .edu__facts, .foot__meta { font-variant-numeric: tabular-nums; }

/* Toggle state must be exposed visually as well as in the accessibility tree. */
.slat[aria-pressed="true"] { flex-grow: 3.6; }
@media (max-width: 640px) { .slat[aria-pressed="true"] { flex-grow: 5; } }
.slat[aria-pressed="true"] .slat__cap { opacity: 1; transform: none; }
.slat[aria-pressed="true"] .slat__img { filter: saturate(1) contrast(1.04); }
.slat[aria-pressed="true"] .slat__timer { animation: slatTick 3.2s linear forwards; }
.slat[aria-pressed="true"]::after { opacity: 0; }

/* Guidelines: animate transform and opacity only. */
.slat { transition: flex-grow var(--dur-base) cubic-bezier(.2,.8,.25,1); }

/* ── Narrative turns. Short centred beats between blocks, set larger than
      body but smaller than a headline, so the page reads as a continuous
      argument rather than a stack of sections. ─────────────────────────── */
.turn {
  padding-block: clamp(2.5rem, 7vw, 4.5rem);
  display: grid; justify-items: center; text-align: center;
}
.turn p {
  font-family: var(--display); font-weight: 600; text-transform: uppercase;
  font-size: clamp(1.25rem, 3.2vw, 2rem); line-height: 1.15;
  max-width: 24ch; color: var(--ink);
}

/* Final calibration. The field is an undercurrent: noticed on a second look,
   never competing with type. Phones get less because the type is
   proportionally larger against the field there. */
.topo { opacity: .085; }
@media (max-width: 860px) { .topo { opacity: .055; } }

/* Type-safe clearing, strengthened. The card forbids body copy over a field
   above ~10% coverage; the halo guarantees the words sit on clear paper
   without drawing a visible box. */
.craft__row h3, .craft__row p, .turn p {
  text-shadow: 0 0 16px var(--paper), 0 0 10px var(--paper),
               0 0 5px var(--paper), 0 0 2px var(--paper);
}
.craft__skills li { background: var(--paper); }

/* ══ SLATS: corrected ═════════════════════════════════════════════════
   Three faults were compounding:
   1. mix-blend-mode:color with a saturated plate forces every pixel to that
      hue at full chroma, which produced hot magenta rather than the muted
      rose of the baked portrait. The plate is now desaturated and partial.
   2. Six equal slats at 414px gave ~60px each, so nothing read as open.
   3. nowrap + overflow:hidden clipped the caption to "JOSHUA".
   ══════════════════════════════════════════════════════════════════════ */

/* 1. Tint matched to the portrait ramp, not to the accent. */
.slat::after {
  background: #6E4147;          /* desaturated rose, close to the baked ramp */
  opacity: .62;
}
.slat__img { filter: grayscale(1) contrast(1.04) brightness(1.02); }
.slat[aria-pressed="true"]::after { opacity: 0; }
.slat[aria-pressed="true"] .slat__img { filter: grayscale(0) contrast(1.03); }

/* 2. The open slat has to dominate, and closed ones shrink to a spine. */
.slats { gap: 4px; }
.slat { min-width: 30px; flex: 1 1 0; }
.slat[aria-pressed="true"] { flex-grow: 6; }
@media (min-width: 861px) {
  .slats { gap: 6px; }
  .slat { min-width: 44px; }
  .slat[aria-pressed="true"] { flex-grow: 4.2; }
}

/* 3. Caption only on the open slat, and allowed to sit on two lines. */
.slat__cap {
  white-space: normal; overflow: visible;
  opacity: 0; pointer-events: none;
  max-width: 100%; line-height: 1.35;
}
.slat[aria-pressed="true"] .slat__cap { opacity: 1; }
@media (max-width: 640px) {
  .slat__cap { font-size: .625rem; letter-spacing: .1em; padding: .625rem .5rem; }
  .slats { height: clamp(280px, 50vh, 420px); }
}

/* will-change on a 2.6MP canvas pins a large GPU layer for the whole session.
   The canvas only moves on scroll, so the hint is not worth the memory. */
.topo { will-change: auto; }

/* ══ HERO: topography, not stripes ════════════════════════════════════
   The vertical band ground came from the portrait reference. The contour
   field now runs behind the hero like everywhere else, so the page has one
   continuous terrain rather than two different backdrops. */
.stage::before { content: none; }

/* ══ THE RIVER, made profound ═════════════════════════════════════════
   It is the spine of the page, so it is allowed real weight. The bed is the
   channel; the current is the thing travelling down it. */
#riverBed {
  stroke-width: 1.6;
  opacity: .22;
}
#riverFlow {
  stroke-width: 4;
  opacity: .62;
}
@media (max-width: 860px) {
  #riverBed  { stroke-width: 1.2; opacity: .2; }
  #riverFlow { stroke-width: 2.8; opacity: .55; }
}

/* The failed-image fallback must never paint behind a transparent cutout.
   A blanket `img { background }` put a visible plate behind the portrait and
   the body figure, so the placeholder is scoped to the failed state only. */
img { background: none; }
img[data-failed] { background: var(--paper-2); }
/* background-COLOR, not the shorthand. The shorthand reset background-image
   to none, and since .slat__img carries its photograph as a background image,
   every slat in the reel rendered as an empty plate. */
.slat__img {
  background-color: var(--paper-2);   /* photographic tiles only, never cutouts */
}
.stage__portrait img, .drift img { background: none !important; }

/* ══ No layout shift on a view swap ═══════════════════════════════════
   The two views differ in height, so the scrollbar could appear on one and
   not the other, shifting every centred element horizontally. Reserving the
   gutter permanently makes the swap seamless. */
/* Superseded below: the scrollbar is removed outright rather than reserved. */

/* The stage is shared and must not reflow when the title text changes
   length, so its row heights are fixed rather than content-driven. */
.stage { grid-template-rows: 1fr auto; }
.stage__title { min-height: 1px; }

/* ══ River travels WITH the view ══════════════════════════════════════
   Previously it was pinned to the document, so it scrolled away and any
   given screen showed only a fragment. Fixed to the viewport at twice its
   height and translated on scroll, the channel is always present and the
   water keeps moving past you. That is the difference between a decorative
   line and the feeling of travelling down something. */
.river {
  position: fixed;
  inset: 0 0 auto 0;
  height: 200vh;
  will-change: transform;
}

/* ══════════════════════════════════════════════════════════════════════════
   PASS: THE RIVER RUN
   One ground, one river, and pit stops that alternate banks down its curve.
   Everything below supersedes the accreted layers above it.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── No scrollbar. Two views of different heights meant the bar appeared on
      one and not the other, and every centred element jumped sideways on the
      swap. Removing it is the only fix that costs nothing at load. ──────── */
html {
  scrollbar-gutter: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
html::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* ── Nothing may leave the frame sideways. `.topo` was 100vw, which is wider
      than the viewport whenever a scrollbar is present, and that alone made
      the whole page draggable left and right on a phone. ────────────────── */
html, body { overflow-x: clip; max-width: 100%; }
.topo, .river { width: 100%; left: 0; right: 0; }
.rockdefs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ══ THE GROUND — Inspiration (2), TEX-1005 ═══════════════════════════════
   Filled nested bands rather than hairlines, drawn once at three viewport
   heights and translated on scroll, so it reads as one enormous poster the
   page is travelling down rather than a texture that redraws. Divergence
   from TEX-1006, logged: that card forbids tone from alpha, but a legible
   page needs the field held well under the type. */
.topo {
  position: fixed; inset: 0 0 auto 0; z-index: 0;
  height: 300vh;
  pointer-events: none;
  opacity: .62;
  will-change: transform;
  -webkit-mask-image: none;
  mask-image: none;
}
@media (max-width: 860px) { .topo { opacity: .5; } }

/* ══ THE RIVER — one line, bigger ═════════════════════════════════════════
   Bed and current are the same stroke at the same weight, so what you see is
   a single channel with a lit length travelling down it, never two lines. */
#riverBed  { stroke-width: 6; opacity: .13; stroke-linecap: round; }
#riverFlow { stroke-width: 6; opacity: .70; stroke-linecap: round; }
@media (max-width: 860px) {
  #riverBed  { stroke-width: 4.5; opacity: .13; }
  #riverFlow { stroke-width: 4.5; opacity: .66; }
}

/* ── Stray rose rules. The river is the only red line on the page. ─────── */
.craft__row::before { content: none !important; }

/* ══ NAV — folder left, switch centre, mail right ════════════════════════ */
.nav { grid-template-columns: 1fr auto 1fr; }
.nav > .disc { grid-column: 1; justify-self: start; }
.disc {
  display: inline-grid; place-content: center;
  width: 44px; height: 44px; min-width: 44px; min-height: 44px; flex: 0 0 auto;
  background: var(--ink); color: var(--paper); border-radius: 50%;
  transition: background var(--dur-fast) var(--ease-standard),
              transform var(--dur-fast) var(--ease-standard);
}
.disc svg { width: 18px; height: 18px; }
.disc:hover  { background: var(--rose); }
.disc:active { background: var(--rose-900); transform: scale(.95); }
.nav__switch a {
  position: relative; z-index: 1; display: inline-flex; align-items: center;
  padding: .5rem clamp(.875rem, 3vw, 1.375rem); border-radius: 999px;
  font-family: var(--label); font-size: .6875rem; font-weight: 500;
  letter-spacing: .18em; text-transform: uppercase; white-space: nowrap;
  text-decoration: none; color: var(--ink-dim);
  transition: color var(--dur-fast) var(--ease-standard);
}
.nav__switch a:hover { color: var(--ink); }

/* ══ STAGE — the strap line and the city are gone, so the row they sat in
      goes with them and the portrait keeps its own footing. ═════════════ */
.stage {
  grid-template-rows: 1fr;
  min-height: 86svh;
  padding-block: 5rem clamp(1.5rem, 4vw, 3rem);
}
.stage[data-ready] .stage__portrait {
  transition: opacity .9s var(--ease-reveal) .25s, transform .9s var(--ease-reveal) .25s;
}

/* ══ PIT STOPS ════════════════════════════════════════════════════════════
   Joshua Tree monzogranite, one per beat, alternating banks so the eye is
   handed down the river curve instead of returning to a single left edge.
   LAY-918's principle without its literal silhouette handoff. */
.stop {
  position: relative; isolation: isolate;
  width: min(1240px, 100% - 3rem); margin-inline: auto;
  padding-block: clamp(4rem, 11vw, 8rem);
  display: grid; gap: .5rem;
  justify-items: center; text-align: center;
}
.stop > *:not(.stop__rock) { position: relative; z-index: 1; }
.stop__rock {
  position: absolute; z-index: 0; top: 50%;
  width: min(86vw, 560px); height: auto;
  translate: 0 -50%;
  fill: var(--rose); opacity: .09;
  pointer-events: none;
}
.stop__rock use { fill: inherit; }

.stop__stmt {
  font-size: clamp(1.15rem, 2.5vw, 1.7rem); line-height: 1.45;
  max-width: 34ch; text-wrap: pretty;
}
.stop__small  { color: var(--ink-dim); max-width: 34ch; }
.stop__punch {
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(2.6rem, 10vw, 6rem); line-height: .9; color: var(--rose);
}
.stop__after {
  color: var(--ink-dim); max-width: 34ch; margin-top: .75rem;
  font-size: clamp(.9375rem, 1.5vw, 1.0625rem);
}
.stop__turn {
  font-family: var(--display); font-weight: 600; text-transform: uppercase;
  font-size: clamp(1.35rem, 3.4vw, 2.2rem); line-height: 1.14;
  max-width: 24ch;
}

/* Left and right banks. Below 861px the river is narrow and the offsets
   would only squeeze the measure, so everything returns to centre. */
@media (min-width: 861px) {
  .stop--l { justify-items: start; text-align: left;  padding-left: 4%;  padding-right: 32%; }
  .stop--r { justify-items: end;   text-align: right; padding-right: 4%; padding-left: 32%; }
  .stop--l .stop__rock { left: -2%; }
  .stop--r .stop__rock { right: -2%; }
  .stop--r .stop__stmt, .stop--r .stop__small,
  .stop--r .stop__after, .stop--r .stop__turn { margin-left: auto; }
}
@media (max-width: 860px) {
  .stop { padding-block: clamp(3rem, 9vw, 5rem); }
  .stop__rock { left: 50%; translate: -50% -50%; width: min(94vw, 460px); opacity: .075; }
}

/* Type sits on clear paper wherever the field runs behind it. */
.stop__stmt, .stop__small, .stop__after, .stop__turn,
.bio__copy p, .bio__lead, .edu__note, .drift figcaption {
  text-shadow: 0 0 16px var(--paper), 0 0 10px var(--paper),
               0 0 5px var(--paper), 0 0 2px var(--paper);
}

/* ══ EDUCATION — rose, because it is where he graduated ══════════════════ */
.edu__school { color: var(--rose); }
.edu__note b { font-weight: 500; color: var(--ink); }

/* ══ EXPERIENCE ═══════════════════════════════════════════════════════════ */
@media (min-width: 861px) {
  .exp__row { grid-template-columns: 12rem minmax(0, 1fr); gap: 2rem; }
}

/* The side thing, kept visibly in proportion to the roles above it. */

/* ══ THE DRIFTING FIGURE ═════════════════════════════════════════════════ */
.drift { width: min(34vw, 260px); }
.drift figcaption {
  color: var(--ink-dim); font-size: .6875rem; letter-spacing: .16em;
  margin-top: .5rem;
}
@media (max-width: 700px) { .drift { width: min(50vw, 200px); margin-right: -1rem; } }

/* ══ CREED — the first three carry the belief, so they carry the size ═══ */
.creed ul { gap: .25rem; }
.creed li:not(.key) span { font-size: clamp(1.15rem, 3.6vw, 2rem); color: var(--ink-dim); }
.creed li.key span { font-size: clamp(1.7rem, 6vw, 3.5rem); color: var(--ink); font-weight: 700; }
.creed li.key em { color: var(--rose); }
.creed li.key + li:not(.key) { margin-top: 1.25rem; }

/* ══ WRITING ROWS — each is a link into the archive ══════════════════════ */
@media (max-width: 640px) {
  .writing__row a { grid-template-columns: 76px minmax(0, 1fr); gap: .25rem 1rem; }
}

/* ══ THE END — a longer pause, and one deliberate arrival ════════════════
   Everything else on the page rises 12-38px. The closing block scales as
   well as rises and takes a beat longer, so the scroll has an ending rather
   than just running out. */
.fin {
  padding-top: clamp(6rem, 16vw, 11rem);
  opacity: 0; transform: translateY(44px) scale(.965);
}
.fin[data-in] {
  opacity: 1; transform: none;
  transition: opacity 1.05s var(--ease-reveal), transform 1.25s var(--ease-reveal);
}
.closer { padding-block: clamp(2rem, 5vw, 3rem) clamp(6rem, 15vw, 10rem); }
.reel   { padding-block: clamp(2rem, 5vw, 3rem) clamp(6rem, 15vw, 10rem); }
@media (prefers-reduced-motion: reduce) {
  .fin { opacity: 1 !important; transform: none !important; transition: none !important; }
}

/* ══ ROCKS — the reel becomes a vertical stratum at phone width ══════════
   Six vertical slats at 390px gave every closed one a 30px spine, which is
   an edge rather than a photograph. Stacked top to bottom they get the full
   width, so the open one is legible and the closed ones read as bedding
   planes pressed together. */
@media (max-width: 760px) {
  .slats {
    flex-direction: column;
    height: clamp(430px, 66svh, 580px);
    gap: 3px;
  }
  .slat {
    min-width: 0; min-height: 34px; width: 100%;
    border-radius: 30px 38px 32px 24px / 24px 28px 38px 32px;
  }
  .slat:nth-child(2n) { border-radius: 34px 24px 28px 38px / 38px 32px 24px 28px; }
  .slat:nth-child(3n) { border-radius: 24px 32px 38px 30px / 30px 38px 28px 24px; }
  .slat[aria-pressed="true"] { flex-grow: 7; }
  .slat__cap { text-align: center; padding: .75rem .875rem; }
  .slat__timer { height: 3px; }
}

/* ══ THE ARCHIVE PLACEHOLDER ════════════════════════════════════════════ */
.soon__main { display: grid; align-content: center; min-height: 78svh; padding-top: 5rem; }
.soon__k {
  font-family: var(--label); font-size: .6875rem; letter-spacing: .2em;
  text-transform: uppercase; color: var(--ink-faint);
}
.soon__go { margin-top: 1.5rem; }
.soon .nav__switch { background: rgba(247, 242, 234, .72); }

/* ══ Reduced motion: the ground and the river hold still but stay ═══════ */
@media (prefers-reduced-motion: reduce) {
  .topo, .river { transform: none !important; }
}

/* ══ CORRECTIONS AFTER THE FIRST RUN ══════════════════════════════════════ */

/* The figure sat in its own full-bleed band, which opened a ~600px void
   between the education block and the roles, and pushed its own caption off
   the right edge of the screen. It now sits inside the page measure and
   bleeds by a controlled amount that keeps the caption on the page. */
.drift {
  width: min(1240px, 100% - 3rem);
  margin: clamp(.5rem, 2vw, 1.5rem) auto clamp(1rem, 3vw, 2rem);
  display: grid; justify-items: end;
  padding-right: 0;
}
.drift img { width: min(30vw, 240px); height: auto; }
.drift figcaption { text-align: right; }
@media (max-width: 700px) {
  .drift { width: min(1240px, 100% - 2rem); margin-right: auto; }
  .drift img { width: min(46vw, 190px); }
}

/* The band either side of the figure was doing the same job twice. */
.edu { padding-block: clamp(2.5rem, 7vw, 4rem) clamp(1.5rem, 4vw, 2.5rem); }

/* The halftone plate was cropped to the base of the buildings and run at .3,
   so it read as static rather than as a skyline. Pull the crop up to the
   rooflines and hold it well under the type. */
.foot__city img {
  height: clamp(120px, 22vw, 300px);
  object-position: center 22%;
  opacity: .16;
}
@media (max-width: 860px) { .foot__city img { height: clamp(90px, 26vw, 160px); opacity: .13; } }

/* Reel: the caption plate needs to clear the rounded corners at phone width
   so it does not sit outside the stone it belongs to. */
@media (max-width: 760px) {
  .slat__cap {
    left: 0; right: 0; bottom: 0;
    border-radius: inherit;
    background: linear-gradient(transparent, rgb(26 22 20 / .78) 62%);
  }
}

/* ══ SECOND PASS ══════════════════════════════════════════════════════════ */

/* The boulder read as a smudge because it was fill-only. An outline is what
   makes granite legible as granite, and it also ties it to the contour field
   it is sitting on. */
.stop__rock {
  opacity: 1;
  fill: var(--rose); fill-opacity: .06;
  stroke: var(--rose); stroke-opacity: .26; stroke-width: 1.1;
  vector-effect: non-scaling-stroke;
}
.stop__rock use { fill: inherit; stroke: inherit; }
@media (max-width: 860px) {
  .stop__rock { fill-opacity: .05; stroke-opacity: .2; }
}

/* The discipline rows had a 500px gulf between the name and its description,
   which read as two unrelated columns rather than one row. */
@media (min-width: 761px) {
  .craft__row {
    grid-template-columns: minmax(0, .42fr) minmax(0, 1fr);
    gap: clamp(1.5rem, 3vw, 2.5rem);
  }
}

/* ══ THIRD PASS ═══════════════════════════════════════════════════════════ */

/* The boulders are <use> instances, and a <use> shadow tree is unreachable
   from an outside selector. Only inherited properties cross the boundary, so
   the paths paint with currentColor and this sets what that colour is. */
.stop__rock { color: var(--rose); fill: none; stroke: none; opacity: 1; }
@media (max-width: 860px) { .stop__rock { opacity: .82; } }

/* The archive placeholder centres, since there is no river running past it
   to justify a bank. */
.soon .stop { padding-block: clamp(3rem, 8vw, 5rem); }
/* NO width override here. The rebuilt .stop__rock fills its .stop__in via
   `inset: 0; width: 100%`. Pinning a fixed width re-anchored it to left:0,
   so the stone sat ~60px left of the text it was meant to sit behind and
   "In progress." ran out past its right edge. Let it fill, like every other
   stop on the site. */
.soon .stop__in { max-width: min(90vw, 560px); }

/* .link-go is an <a> on that page and was picking up the default underline. */
.link-go { text-decoration: none; }

/* ══ WEB INTERFACE GUIDELINES — AUDIT FIXES ═══════════════════════════════
   Measured at 390px and 1440px on both views. */

/* Nothing under 10.5px. Three tokens had drifted back below the floor. */
.edu__facts span { font-size: .6875rem; letter-spacing: .14em; }
.link-go { font-size: .6875rem; }
.slat__cap { font-size: .6875rem; }
@media (max-width: 640px) { .slat__cap { font-size: .6875rem; letter-spacing: .1em; } }

/* The caption plate began fully transparent, so the first line of white type
   could land on a bright part of the photograph. It now reaches full strength
   before the text starts. */
.slat__cap {
  background: linear-gradient(rgb(26 22 20 / 0), rgb(26 22 20 / .82) 45%, rgb(26 22 20 / .9));
  padding-top: 2.25rem;
}

/* Footer links were 19px tall. Every target clears 24px now, and the row
   spacing is reduced to keep the block the same height. */
.foot__links { gap: 0; }
.foot__links a { padding-block: .375rem; }
.foot__mail { display: inline-block; padding-block: .25rem; width: max-content; }

/* Touch: no 300ms delay, no default blue flash, and full-bleed surfaces
   respect the notch and the home indicator. Mobile is the design target. */
a, button, .slat, .craft__row { touch-action: manipulation; }
a, button { -webkit-tap-highlight-color: rgb(142 44 62 / .16); }
.nav  { padding-top: max(1.125rem, env(safe-area-inset-top)); }
.nav[data-scrolled="true"] { padding-top: max(.8125rem, env(safe-area-inset-top)); }
.foot { padding-bottom: max(clamp(1.5rem, 3vw, 2rem), env(safe-area-inset-bottom)); }
/* Applied to .wrap and .stage only. Putting it on .stop would have the same
   specificity as .stop--l / .stop--r and would silently cancel the bank
   offsets, which is a worse bug than the one being fixed. */
.wrap {
  padding-left: max(0px, env(safe-area-inset-left));
  padding-right: max(0px, env(safe-area-inset-right));
}
.stage {
  padding-inline:
    max(clamp(1rem, 3vw, 2.5rem), env(safe-area-inset-left))
    max(clamp(1rem, 3vw, 2.5rem), env(safe-area-inset-right));
}
@media (max-width: 760px) {
  .stage {
    padding-inline:
      max(clamp(.75rem, 3vw, 2.5rem), env(safe-area-inset-left))
      max(clamp(.75rem, 3vw, 2.5rem), env(safe-area-inset-right));
  }
}

/* Display type sets in balanced blocks rather than leaving a single orphan. */
.stop__turn, .closer__lead, .bio__lead, .edu__school, .stop__punch { text-wrap: balance; }

/* The ground is painted after first paint, so it fades in rather than popping
   into an already-settled page. */
.topo { opacity: 0; transition: opacity .9s var(--ease-reveal); }
.topo[data-drawn] { opacity: .62; }
@media (max-width: 860px) { .topo[data-drawn] { opacity: .5; } }
@media (prefers-reduced-motion: reduce) { .topo { transition: none; } }

/* ══════════════════════════════════════════════════════════════════════════
   PIT STOPS, REBUILT
   The rock is no longer decoration sitting near the words. It is stretched to
   the padded box of the words themselves, so the text is always inside the
   stone at every width and in every language. preserveAspectRatio="none" is
   what makes that possible, and the distortion is a feature here: no two
   stops end up the same shape.
   ══════════════════════════════════════════════════════════════════════════ */
.stop {
  display: grid;
  padding-block: clamp(3.5rem, 10vw, 7rem);
  gap: 0;
}
.stop__in {
  position: relative; isolation: isolate;
  display: grid; gap: .5rem;
  justify-items: center; text-align: center;
  width: max-content; max-width: 100%;
  padding: clamp(2.75rem, 7vw, 4.5rem) clamp(2.5rem, 7vw, 5rem);
}
.stop__in > *:not(.stop__rock):not(.pebble) { position: relative; z-index: 2; }

.stop__rock {
  position: absolute; z-index: 0; inset: 0;
  width: 100%; height: 100%;
  color: var(--rose);
  pointer-events: none;
}

/* Scatter. Each stone is placed from its own custom properties, so no two
   sit at the same offset and none of them is a rotation of its neighbour. */
.pebble {
  position: absolute; z-index: 1;
  left: var(--x); top: var(--y);
  width: var(--s); height: auto; aspect-ratio: 100 / 70;
  color: var(--rose);
  fill: currentColor; fill-opacity: .075;
  stroke: currentColor; stroke-opacity: .3; stroke-width: 1.1;
  vector-effect: non-scaling-stroke;
  pointer-events: none;
}
@media (max-width: 860px) {
  /* At 390px the outriggers would sit off the page, so only the ones that
     stay inside the frame survive. */
  .pebble { width: calc(var(--s) * .66); }
  .stop__in { padding: clamp(2.25rem, 8vw, 3.25rem) clamp(1.75rem, 7vw, 3rem); }
}

/* ── Banks. The stop block itself moves; the rock moves with it, because the
      rock is now part of the block rather than a backdrop behind it. ────── */
.stop--l { justify-items: start; }
.stop--r { justify-items: end; }
@media (min-width: 861px) {
  .stop--l { padding-left: 3%;  padding-right: 22%; }
  .stop--r { padding-right: 3%; padding-left: 22%; }
  .stop--l .stop__rock { transform: scaleX(-1); }   /* mirrored, never cloned */
}
@media (max-width: 860px) {
  .stop { justify-items: center; padding-inline: 0; }
  .stop--l, .stop--r { padding-inline: 0; }
  .stop__in { width: auto; }
}

/* ── They fall into place ────────────────────────────────────────────────
      The stone arrives first and the scatter follows it down, base + i×90ms.
      No overshoot and no bounce: doctrine forbids it, and a boulder that
      springs reads as rubber rather than granite. ─────────────────────── */
.stop__rock, .pebble { opacity: 0; transform: translate3d(0, -34px, 0); }
.stop--l .stop__rock { transform: scaleX(-1) translate3d(0, -34px, 0); }
.pebble { transform: translate3d(0, -46px, 0) rotate(var(--r, 0deg)); }

.stop[data-in] .stop__rock {
  opacity: 1; transform: none;
  transition: opacity .45s linear, transform .8s cubic-bezier(.3, .72, .28, 1);
}
.stop--l[data-in] .stop__rock {
  transform: scaleX(-1);
}
.stop[data-in] .pebble {
  opacity: 1; transform: rotate(var(--r, 0deg));
  transition: opacity .4s linear, transform .7s cubic-bezier(.3, .72, .28, 1);
  transition-delay: calc(.18s + var(--i, 0) * 90ms);
}
@media (prefers-reduced-motion: reduce) {
  .stop__rock, .pebble {
    opacity: 1 !important; transition: none !important;
    transform: rotate(var(--r, 0deg)) !important;
  }
  .stop--l .stop__rock { transform: scaleX(-1) !important; }
}

/* ── Measures, now that the words are inside a stone ─────────────────── */
.stop__stmt  { max-width: 30ch; }
.stop__small { max-width: 30ch; }
.stop__after { max-width: 30ch; }
.stop__turn  { max-width: 20ch; }
.stop--r .stop__stmt, .stop--r .stop__small,
.stop--r .stop__after, .stop--r .stop__turn { margin-left: 0; }
.stop--l, .stop--r { text-align: center; }
.stop--l .stop__in, .stop--r .stop__in { text-align: center; }

/* The archive placeholder gets the same treatment, centred. */
.soon .stop { justify-items: center; padding-inline: 0; }

/* ══ FIXES ════════════════════════════════════════════════════════════════ */

/* An earlier layer centred the rock with `translate: 0 -50%`. `inset: 0` in
   the rebuild does not touch the translate property, so every stone was
   still being lifted half its own height and the words fell out the bottom. */
.stop__rock { translate: none; }

/* The creed lines all carry the same weight of belief, so they all carry the
   same size. The first three are marked by colour, not by scale. */
.creed li span,
.creed li.key span,
.creed li:not(.key) span { font-size: clamp(1.35rem, 4.4vw, 2.5rem); }
.creed li:not(.key) span { color: var(--ink-dim); font-weight: 600; }
.creed li.key span { color: var(--ink); font-weight: 700; }
.creed li.key em { color: var(--rose); }
.creed li:not(.key) em { color: var(--rose-300); }
.creed li.key + li:not(.key) { margin-top: 1rem; }

/* ══════════════════════════════════════════════════════════════════════════
   EVERYTHING IN A ROCK
   The stone stopped being a backdrop and became the container. Every block
   that carries a claim now sits inside one, stretched to its own padded box
   so the words can never fall out at any width.
   ══════════════════════════════════════════════════════════════════════════ */

:root { --rose-400: #DE6273; }   /* bright enough to read as red on the ink footer */

/* ── Hero tagline: bigger, and set against the vertical name rather than
      stranded in the middle of the field. ────────────────────────────── */
.stage__tag { display: none; }
@media (min-width: 1100px) {
  .stage__tag {
    display: grid; gap: .45rem;
    position: absolute; z-index: 2;
    left: auto; right: clamp(8.5rem, 12vw, 13rem);
    top: 50%; translate: 0 -50%;
    max-width: 16ch;
    text-align: right;
    margin: 0; padding: 0;
  }
  .stage__tag span {
    font-family: var(--body); font-style: italic; font-weight: 400;
    font-size: clamp(1.3rem, 1.85vw, 1.75rem); line-height: 1.25;
    color: var(--ink); letter-spacing: -.008em;
    text-wrap: balance;
  }
  .stage__tag span:last-child { color: var(--ink-dim); margin-top: .35rem; }
}
@media (min-width: 1500px) { .stage__tag { max-width: 18ch; } }

/* ── Shared stone-container mechanics ──────────────────────────────────── */
.orock__rock, .erock__rock {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  color: var(--rose);
  translate: none;
  pointer-events: none;
}

/* ── The sites, directly under the opening statement ──────────────────── */
.sites { padding-block: clamp(1rem, 3vw, 2rem) clamp(2rem, 6vw, 3.5rem); }
.sites__grid {
  display: grid; gap: clamp(1.25rem, 3vw, 2rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
  list-style: none; margin: 0; padding: 0;
}
.srock { position: relative; }
.srock a {
  position: relative; display: grid; gap: .45rem;
  justify-items: center; text-align: center; text-decoration: none;
  padding: clamp(1.75rem, 3.5vw, 2.5rem) clamp(1.25rem, 2.5vw, 1.75rem);
}
.srock a > *:not(.srock__rock) { position: relative; z-index: 1; }
.srock__shot {
  display: block; width: 100%; aspect-ratio: 16/10;
  border-radius: 16px;                 /* rounded, as asked */
  overflow: hidden; isolation: isolate;
  background-color: var(--paper-2);
  margin-bottom: .5rem;
}
.srock__shot img {
  width: 100%; height: 100%; object-fit: cover; object-position: top center;
  filter: grayscale(1) contrast(1.06);
  transition: transform var(--dur-reveal) var(--ease-standard),
              filter var(--dur-base) var(--ease-standard);
}
.srock__shot::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: var(--rose-900); mix-blend-mode: color; pointer-events: none;
  transition: opacity var(--dur-base) var(--ease-standard);
}
.srock__shot--mark {
  background: var(--rose-900); display: grid; place-items: center;
}
.srock__shot--mark::after { content: none; }
.srock__shot--mark span {
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(1.3rem, 3.2vw, 1.9rem); color: var(--rose-100);
}
.srock a:hover .srock__shot::after { opacity: 0; }
.srock a:hover .srock__shot img { filter: grayscale(0) contrast(1.02); transform: scale(1.03); }
.srock__name {
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.15rem, 2.4vw, 1.4rem); line-height: 1;
  transition: color var(--dur-fast) var(--ease-standard);
}
.srock a:hover .srock__name { color: var(--rose); }
.srock__note { font-size: .875rem; line-height: 1.45; color: var(--ink-dim); max-width: 26ch; }

/* The setup line sits on open paper, so the stone below it lands harder. */

/* ── The offer. One sentence per stone. ────────────────────────────────── */
.offer { padding-block: clamp(1rem, 3vw, 2rem) clamp(2rem, 6vw, 3.5rem); }
.offer__grid {
  display: grid; gap: clamp(1.25rem, 3vw, 2rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  list-style: none; margin: 0; padding: 0;
}
.orock { position: relative; }
.orock p {
  position: relative; z-index: 1;
  padding: clamp(2.25rem, 4.5vw, 3.25rem) clamp(1.75rem, 3.5vw, 2.5rem);
  margin: 0; text-align: center; color: var(--ink-dim);
  font-size: clamp(.9375rem, 1.4vw, 1.0625rem); line-height: 1.5;
  text-wrap: pretty;
  text-shadow: 0 0 14px var(--paper), 0 0 7px var(--paper), 0 0 3px var(--paper);
}

/* ── Experience, one role per stone, alternating banks ─────────────────── */
.erock { position: relative; margin-block: clamp(1.25rem, 3.5vw, 2.25rem); }
.erock__in {
  position: relative; z-index: 1;
  display: grid; gap: .3rem;
  padding: clamp(2.25rem, 4.5vw, 3.25rem) clamp(2rem, 4.5vw, 3.25rem);
}
/* One typographic pattern for all four, so swim coaching cannot drift from
   the roles above it. */
.erock__k {
  font-family: var(--label); font-size: .6875rem; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink-faint);
}
.erock h3 {
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.35rem, 3vw, 1.85rem); line-height: 1.02;
  margin-top: .1rem;
}
.erock__p {
  color: var(--ink-dim); font-size: .9375rem; line-height: 1.55;
  max-width: 46ch; margin-top: .35rem;
  text-shadow: 0 0 14px var(--paper), 0 0 7px var(--paper), 0 0 3px var(--paper);
}
.erock__go { display: flex; flex-wrap: wrap; align-items: center; gap: .875rem; margin-top: .875rem; }
@media (min-width: 861px) {
  .erock { width: min(100%, 620px); }
  .erock--l { margin-right: auto; }
  .erock--r { margin-left: auto; }
  .erock--l .erock__rock { transform: scaleX(-1); }
}

/* The Web Design stone and the figure travel as a pair. */
@media (min-width: 861px) {
  .pair {
    display: grid; align-items: center;
    grid-template-columns: minmax(0, 1fr) minmax(0, .62fr);
    gap: clamp(1rem, 3vw, 2.5rem);
  }
  .pair .erock { width: 100%; margin-block: 0; }
}

/* ── The figure: bigger, further left, oval, and dissolving ───────────── */
.drift { width: 100%; margin: 0; display: grid; justify-items: center; }
.drift img { width: min(78%, 340px); height: auto; }
.drift figcaption {
  text-align: center; margin-top: .25rem;
  color: var(--ink-dim); font-family: var(--label);
  font-size: .6875rem; letter-spacing: .16em; text-transform: uppercase;
}
@media (max-width: 860px) {
  .drift { margin-block: clamp(1rem, 4vw, 2rem); }
  .drift img { width: min(62%, 260px); }
}

/* ── Small controls ────────────────────────────────────────────────────── */
.link-go--sm {
  padding: .5rem .875rem; min-height: 36px;
  font-size: .625rem; letter-spacing: .14em;
}
.link-go--sm svg { width: 13px; height: 10px; }

/* ══ CRAFT ROWS: every skill visible, always ══════════════════════════════
   The chips used to be behind a 0fr collapse that opened on hover or on the
   scroll-live row. Hiding the substance until the visitor finds the right
   row is the opposite of advertising it, so they now sit open the whole
   time and simply light up as the row passes the middle of the screen. */
.craft__skills {
  display: flex; flex-wrap: wrap; gap: .375rem;
  list-style: none; margin: .875rem 0 0; padding: 0;
  grid-template-rows: none;
  grid-column: 2;
}
.craft__skills li {
  display: inline-flex; flex: 0 0 auto; white-space: nowrap;
  /* Pebbles, not pills. Each corner gets its own horizontal and vertical
     radius so no two chips are the same shape, which is the same idea as the
     rock plates behind the cards: still fully round, just not machine-made.
     Vertical radii stay at or under half the ~31px height so the sides read
     as fully rounded; the horizontal radii run wider, which is what tilts
     each corner off true. Six variants cycle, and nth-child restarts per
     list, so the repeat never lines up across the three groups. */
  border-radius: 26px 14px 22px 18px / 15px 12px 15px 13px;
  margin: 0; padding: .375rem .75rem;
  font-family: var(--label); font-size: .6875rem; letter-spacing: .1em;
  text-transform: uppercase;
  background: var(--paper);
  border: 1px solid rgba(26,22,20,.2);
  color: var(--ink-dim);
  opacity: 1; transform: none;
  transition: color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard),
              transform var(--dur-fast) var(--ease-standard);
}
/* Lit by scroll position, one row at a time. */
.craft__row[data-live] .craft__skills li {
  color: var(--ink); border-color: var(--rose-300);
}
/* The other five pebbles. */
.craft__skills li:nth-child(6n+2) { border-radius: 14px 28px 16px 24px / 12px 15px 13px 15px; }
.craft__skills li:nth-child(6n+3) { border-radius: 20px 16px 28px 14px / 15px 13px 12px 15px; }
.craft__skills li:nth-child(6n+4) { border-radius: 16px 26px 14px 24px / 13px 15px 15px 12px; }
.craft__skills li:nth-child(6n+5) { border-radius: 28px 15px 22px 17px / 15px 12px 14px 15px; }
.craft__skills li:nth-child(6n)   { border-radius: 15px 22px 17px 30px / 12px 15px 13px 15px; }

/* Popped by the pointer, on the chip itself. */
.craft__skills li:hover {
  color: var(--rose); border-color: var(--rose);
  transform: translateY(-2px);
}
@media (max-width: 760px) { .craft__skills { grid-column: 1; } }
@media (prefers-reduced-motion: reduce) { .craft__skills li { transition: none; } }

/* ══ CREED: the whole list pops as it passes ══════════════════════════════
   Same mechanism as the craft rows. The line nearest the middle of the
   screen is the live one, so the block reads as something moving past you
   rather than six lines that arrived together. */
.creed li span {
  transition: color var(--dur-base) var(--ease-standard),
              transform var(--dur-base) var(--ease-standard);
}
.creed li em { transition: color var(--dur-base) var(--ease-standard); }
.creed li:not([data-live]) span { color: var(--ink-dim); }
.creed li[data-live] span { color: var(--ink); transform: translateX(clamp(.35rem, .9vw, .9rem)); }
.creed li[data-live] em { color: var(--rose); }
.creed li:not([data-live]) em { color: var(--rose-300); }
@media (prefers-reduced-motion: reduce) {
  .creed li span, .creed li em { transition: none; }
  .creed li[data-live] span { transform: none; }
}

/* ══ REEL: rounded at every width, not just on a phone ═══════════════════ */
.slat {
  border-radius: 26px 34px 30px 22px / 22px 26px 34px 30px;
}
.slat:nth-child(2n) { border-radius: 32px 22px 26px 34px / 34px 30px 22px 26px; }
.slat:nth-child(3n) { border-radius: 22px 30px 34px 28px / 28px 34px 26px 22px; }
.slat__cap { border-radius: inherit; }
.slats { gap: 6px; }

/* ══ FOOTER: the name is the way home ════════════════════════════════════ */

/* ══ SKELETON ════════════════════════════════════════════════════════════
   A defined loading state, so a slow image is a shape that is clearly
   arriving rather than a hole in the page. Transform-only sweep. */
[data-skel] { position: relative; overflow: hidden; background-color: var(--paper-2); }
[data-skel]::before {
  content: ""; position: absolute; inset: 0; z-index: 2;
  background: linear-gradient(90deg, transparent, rgb(255 255 255 / .55), transparent);
  transform: translateX(-100%);
  animation: skelSweep 1.35s linear infinite;
  pointer-events: none;
}
[data-skel] img { opacity: 0; transition: opacity .4s linear; }
[data-skel][data-loaded] img { opacity: 1; }
[data-skel][data-loaded]::before { content: none; }
@keyframes skelSweep { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) {
  [data-skel]::before { animation: none; opacity: .4; transform: none; }
  [data-skel] img { transition: none; }
}

/* ══ SITES: no stone ═════════════════════════════════════════════════════
   The three screenshots are the only literal images on the home view, and a
   stone behind each one turned the row into a single smear rather than three
   pieces of work. They stand on clear paper with a rounded plate. */
.srock a { padding: 0; gap: .5rem; }
.srock__shot { margin-bottom: .625rem; }

/* The offer stones need more headroom: their silhouettes narrow toward the
   top corners, so the first line was running out to the edge. */
.orock p { padding-block: clamp(3rem, 5.5vw, 4rem) clamp(2.5rem, 5vw, 3.5rem); }

/* Tagline: two comfortable lines rather than three cramped ones, and pulled
   back off the vertical name. */
@media (min-width: 1100px) {
  .stage__tag { max-width: 19ch; right: clamp(9.5rem, 13vw, 15rem); }
}
@media (min-width: 1500px) { .stage__tag { max-width: 21ch; } }

/* ══════════════════════════════════════════════════════════════════════════
   BLOCK STONES
   The silhouettes were domes, so their inscribed rectangle was small and the
   first and last lines of every block sat outside the drawn shape even though
   they were inside its bounding box. The new outlines are rounded organic
   BLOCKS, and the padding is now a percentage of the stone so the clearance
   holds at every size.
   ══════════════════════════════════════════════════════════════════════════ */
.stop__in { padding: clamp(2.75rem, 6.5%, 5.5rem) clamp(2.5rem, 7%, 5.5rem); }
.erock__in { padding: 8.5% 9%; }
.orock p   { padding: 11% 10%; }
@media (max-width: 860px) {
  .stop__in { padding: clamp(2.25rem, 8%, 3.5rem) clamp(1.75rem, 8%, 3rem); }
  .erock__in { padding: 9% 8%; }
  .orock p { padding: 12% 9%; }
}

/* ══ CREED: one line, one stone ═══════════════════════════════════════════ */
.creed ul {
  display: grid; gap: clamp(1.25rem, 3.2vw, 2.5rem);
  list-style: none; margin: 0; padding: 0;
}
.crock { position: relative; width: min(100%, 780px); list-style: none; }
.crock__rock {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  color: var(--rose); translate: none; pointer-events: none;
}
.crock--l .crock__rock { transform: scaleX(-1); }
.crock span {
  position: relative; z-index: 1;
  display: flex; gap: .45em; align-items: baseline;
  padding: 6.5% 7.5%;
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.3rem, 3.6vw, 2.1rem); line-height: 1.1;
  color: var(--ink-dim);
  text-shadow: 0 0 14px var(--paper), 0 0 7px var(--paper), 0 0 3px var(--paper);
  transition: color var(--dur-base) var(--ease-standard);
}
.crock em { font-style: normal; color: var(--rose-300); flex: none;
            transition: color var(--dur-base) var(--ease-standard); }
/* Lit one at a time by scroll position, same painter as the craft rows. */
.crock[data-live] span { color: var(--ink); }
.crock[data-live] em   { color: var(--rose); }
@media (prefers-reduced-motion: reduce) {
  .crock span, .crock em { transition: none; }
}
/* the old flat-list rules no longer apply to anything */
.creed li.key span, .creed li:not(.key) span { font-size: inherit; }

/* ══ OUTRO — the name and the section links, on paper ════════════════════
   Same rose as the hero, on the same ground as the hero, which is the only
   way that colour stays legible. On the ink footer #8E2C3E measures 2.5:1
   and would have failed even the large-text threshold. */
.outro {
  padding-block: clamp(3rem, 8vw, 5.5rem) clamp(2.5rem, 6vw, 4rem);
  display: grid; gap: 1.25rem; justify-items: start;
}
/* These four render as <button> on index.html and as <a href> on work.html.
   Nothing on the site uses an intentional underline, so the UA default only
   ever shows up as a bug -- visibly on .outro__name, invisibly under the
   icon-only links. Reset it once, here, rather than per page. */
.skip, .disc, .mail, .ghlink, .outro__name { text-decoration: none; }

.outro__name {
  font-family: var(--display); font-weight: 800; text-transform: uppercase;
  font-size: clamp(2.75rem, 11vw, 7rem); line-height: .86;
  color: var(--rose);
  padding: 0; text-align: left; display: block;
  transition: color var(--dur-fast) var(--ease-standard);
}
.outro__name i { font-style: normal; color: var(--ink); }
.outro__name:hover   { color: var(--rose-900); }
.outro__name:hover i { color: var(--rose); }
.outro__name:active  { color: var(--rose-900); }

.outro__go { display: flex; flex-wrap: wrap; gap: .5rem; }
.outro__go button, .outro__go a {
  font-family: var(--label); font-size: .6875rem; letter-spacing: .16em;
  text-transform: uppercase; text-decoration: none;
  padding: .625rem 1rem; min-height: 40px;
  display: inline-flex; align-items: center;
  border: 1px solid var(--rose); border-radius: 999px;
  color: var(--rose); background: none;
  transition: background var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard);
}
.outro__go button:hover, .outro__go a:hover { background: var(--rose); color: var(--paper); }
.outro__go button:active, .outro__go a:active { background: var(--rose-900); border-color: var(--rose-900); }

/* ══ HERO TAGLINE — the display face, set small ══════════════════════════ */
@media (min-width: 1100px) {
  .stage__tag { gap: .1rem; max-width: 13ch; right: clamp(9rem, 12.5vw, 14rem); }
  .stage__tag span {
    font-family: var(--display); font-style: normal; font-weight: 700;
    text-transform: uppercase; letter-spacing: .005em;
    font-size: clamp(1.45rem, 2.1vw, 2.15rem); line-height: 1.04;
    color: var(--ink);
  }
  .stage__tag span:last-child {
    font-weight: 600; color: var(--ink-dim);
    font-size: clamp(1.2rem, 1.7vw, 1.7rem); line-height: 1.08;
    margin-top: .5rem;
  }
}
@media (min-width: 1500px) { .stage__tag { max-width: 14ch; } }

/* ══ FOOTER — more of the city, and the name block is gone from it ═══════ */
@media (min-width: 861px) {
  .foot { padding-block: clamp(4.5rem, 10vw, 8rem) clamp(2rem, 4vw, 3rem); }
  .foot__city img { height: clamp(220px, 34vw, 560px); opacity: .24; object-position: center 38%; }
}
@media (max-width: 860px) {
  .foot__city img { height: clamp(110px, 30vw, 200px); opacity: .16; }
}

/* ══ WRITING — down the river, one piece per stone ════════════════════════
   Three tight rows stacked in the centre read as a footnote. Staggered across
   the banks at the same scale as the other stones, they read as three more
   stops on the way down. */
.writing {
  display: grid; gap: clamp(1.25rem, 3.5vw, 2.5rem);
  width: 100%; max-width: none; margin: clamp(2rem, 6vw, 3.5rem) 0 0;
  padding: 0; list-style: none;
}
@media (max-width: 640px) {
  .wrock a { grid-template-columns: 76px minmax(0, 1fr); gap: .2rem 1rem; padding: 9% 8%; }
}

/* ══ CREED STONES: sized to the line ══════════════════════════════════════
   A fixed 780px stone around a six-word line is a lozenge, not a boulder.
   Each stone now hugs its own sentence, so the six of them come out at six
   different widths and the stack reads as scattered rock rather than a menu. */
.crock { width: max-content; max-width: 100%; }
.crock span {
  padding: 1.5rem 2.25rem;
  font-size: clamp(1.15rem, 3.2vw, 1.9rem);
}
@media (max-width: 640px) {
  .crock { width: auto; }
  .crock span { padding: 1.25rem 1.5rem; font-size: clamp(1.05rem, 4.4vw, 1.4rem); }
}
.creed ul { gap: clamp(1.5rem, 3.6vw, 2.75rem); }

/* ══ OFFER: skimmable ═════════════════════════════════════════════════════
   Three paragraphs of even body copy is three paragraphs nobody reads. A caps
   line carries the skim and one sentence carries the detail, which is the
   pattern every portfolio that survives a five-second scan actually uses. */
.orock p { padding: 0; }
.orock__in {
  position: relative; z-index: 1;
  display: grid; gap: .5rem; justify-items: center;
  padding: 12% 10%;
  text-align: center;
}
.orock__in b {
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.25rem, 2.8vw, 1.7rem); line-height: 1.02;
  color: var(--ink); max-width: 14ch;
  text-shadow: 0 0 14px var(--paper), 0 0 7px var(--paper), 0 0 3px var(--paper);
}
.orock__in p {
  /* One step darker than the usual body dim, and a wider paper halo. These
     three lines are the only body copy on the page that sits directly on the
     contour ground rather than on flat paper, so the bands and the river run
     straight through the letterforms. --ink-dim is legible on paper and was
     losing to that texture. */
  margin: 0; color: #332D2A; max-width: 26ch;
  font-size: .9375rem; line-height: 1.45; text-wrap: pretty;
  text-shadow: 0 0 18px var(--paper), 0 0 10px var(--paper),
               0 0 5px var(--paper), 0 0 2px var(--paper);
}

/* ══ HERO ENTRANCE, MADE UNBREAKABLE ═════════════════════════════════════
   The portrait and the name used to DEFAULT to invisible and rely on a
   transition fired by [data-ready] to bring them back. Any condition that
   stalls that transition leaves the hero permanently blank, which is the
   worst failure this page has. The resting state is now visible and the
   entrance is an animation that plays over it: if the animation never runs,
   the hero simply appears. `both` keeps the from-state during the delay, so
   the choreography is unchanged when everything works. */
.stage__portrait { opacity: 1; transform: none; }
.ttl { clip-path: none; }

.stage[data-ready] .stage__portrait {
  animation: heroRise .9s var(--ease-reveal) .25s both;
  transition: none;
}
.stage[data-ready] .ttl {
  animation: heroWipe .95s var(--ease-reveal) .12s both;
  transition: none;
}
@keyframes heroRise { from { opacity: 0; transform: translateY(20px); } }
@keyframes heroWipe { from { clip-path: inset(0 0 100% 0); } }

@media (prefers-reduced-motion: reduce) {
  .stage[data-ready] .stage__portrait,
  .stage[data-ready] .ttl { animation: none; }
}

/* ══ OFFER STONES: less lozenge, more boulder ════════════════════════════ */
.orock__in { padding: 15% 11%; gap: .6rem; }
.orock__in b { max-width: 18ch; font-size: clamp(1.2rem, 2.5vw, 1.55rem); }
.orock__in p { max-width: 30ch; }
@media (max-width: 860px) { .orock__in { padding: 13% 9%; } }

/* ══ NO-MOTION GROUND STATE ══════════════════════════════════════════════
   Set by the frozen-timeline failsafe in script.js. Everything that would
   have animated in is simply present, with the one transform each element
   genuinely needs for layout kept intact. */
[data-nomotion] .rv,
[data-nomotion] .fin,
[data-nomotion] .stage__portrait,
[data-nomotion] .stop__rock,
[data-nomotion] .crock__rock,
[data-nomotion] .wrock__rock,
[data-nomotion] .erock__rock,
[data-nomotion] .orock__rock,
[data-nomotion] .pebble,
[data-nomotion] .ttl,
[data-nomotion] [data-rise] .ch {
  opacity: 1 !important;
  transform: none !important;
  clip-path: none !important;
  animation: none !important;
  transition: none !important;
}
[data-nomotion] .stop--l .stop__rock,
[data-nomotion] .crock--l .crock__rock,
[data-nomotion] .wrock--l .wrock__rock,
[data-nomotion] .erock--l .erock__rock { transform: scaleX(-1) !important; }
[data-nomotion] .pebble { transform: rotate(var(--r, 0deg)) !important; }
[data-nomotion] .topo { opacity: .62 !important; }
@media (max-width: 860px) { [data-nomotion] .topo { opacity: .5 !important; } }

/* Two controls had slipped back under the 10.5px floor. */
.link-go--sm { font-size: .6875rem; letter-spacing: .12em; padding: .5rem .9375rem; min-height: 38px; }

/* The nightbill plate inherited ink from the page, so anything that fell
   through to the wrapper would have been dark type on a dark ground. */
.srock__shot--mark { color: var(--rose-100); }

/* ══════════════════════════════════════════════════════════════════════════
   THE CAIRN
   Six stones that fall as you scroll and settle into a pile. Scroll-scrubbed
   and therefore linear by construction — the visitor is driving it, and an
   easing curve would make the stones fight the wheel. Nothing overshoots and
   nothing bounces: granite does not spring.

   The RESTING state is the settled pile. The fall is subtracted from it via
   --fall (1 = still in the air, 0 = landed), so if the script never runs the
   pile is simply already stacked.
   ══════════════════════════════════════════════════════════════════════════ */
.creed { padding-block: clamp(2rem, 5vw, 3rem) clamp(4rem, 10vw, 7rem); }
.pile {
  display: grid; justify-items: center;
  gap: 0; list-style: none; margin: 0; padding: 0;
}
.crock {
  --fall: 0;
  position: relative;
  width: max-content; max-width: 100%;
  margin: 0;
  will-change: transform;
  transform:
    translate3d(var(--dx, 0), calc(var(--fall) * -1 * var(--drop, 260px)), 0)
    rotate(calc(var(--rest, 0deg) + var(--fall) * var(--tilt, 0deg)));
  opacity: calc(1 - var(--fall) * 1.35);
}
/* Each stone rests ON the one above it, and the overlap stays inside the
   padding so no line of type is ever covered. */
.crock + .crock { margin-top: clamp(-1.6rem, -2.4vw, -1rem); }
.crock__rock { z-index: 0; }
.crock span { position: relative; z-index: 1; padding: 1.7rem 2.25rem; }

@media (max-width: 640px) {
  .crock { width: auto; max-width: 100%; }
  .crock span { padding: 1.35rem 1.5rem; }
  .crock + .crock { margin-top: -1rem; }
}

/* No script, no scroll, or a frozen timeline: the pile is already stacked. */
@media (prefers-reduced-motion: reduce) {
  .crock { --fall: 0 !important; transform: rotate(var(--rest, 0deg)); }
}
[data-nomotion] .crock {
  opacity: 1 !important;
  transform: rotate(var(--rest, 0deg)) !important;
}

/* ══ WRITING — three squares in a row, then the way into the archive ═════ */
.writing {
  display: grid; gap: clamp(1rem, 2.5vw, 1.75rem);
  grid-template-columns: repeat(3, minmax(0, 1fr));
  list-style: none; margin: clamp(2rem, 6vw, 3.5rem) 0 0; padding: 0;
  width: 100%; max-width: none;
}
.wcard { position: relative; }
.wcard a {
  display: grid; gap: .45rem; justify-items: center; text-align: center;
  text-decoration: none;
}
.wcard__shot {
  display: block; width: 100%; aspect-ratio: 1 / 1;
  border-radius: 18px; overflow: hidden; isolation: isolate;
  background-color: var(--paper-2); margin-bottom: .5rem;
}
.wcard__shot img {
  width: 100%; height: 100%; object-fit: cover; object-position: center;
  filter: grayscale(1) contrast(1.06) sepia(.35) hue-rotate(-15deg) saturate(1.5);
  transition: filter var(--dur-base) var(--ease-standard),
              transform var(--dur-reveal) var(--ease-standard);
}
.wcard a:hover .wcard__shot img { filter: none; transform: scale(1.04); }
.wcard b {
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(1.05rem, 2.2vw, 1.35rem); line-height: 1.04;
  transition: color var(--dur-fast) var(--ease-standard);
}
.wcard a:hover b { color: var(--rose); }
.wcard__note { font-size: .875rem; line-height: 1.4; color: var(--ink-dim); max-width: 26ch; }

.writing__go { display: flex; justify-content: center; margin-top: clamp(1.75rem, 4vw, 2.75rem); }
.link-go__folder { width: 17px; height: 17px; flex: none; }

@media (max-width: 640px) {
  .writing { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .875rem; }
  .wcard:first-child { grid-column: 1 / -1; }
  .wcard:first-child .wcard__shot { aspect-ratio: 16 / 10; }
  .wcard__shot { border-radius: 14px; }
}

/* The setup line for "Mine don't." carries the whole argument, so it is set
   as a statement rather than as small print. */
.stop__small {
  font-family: var(--display); font-weight: 600; text-transform: uppercase;
  font-size: clamp(1.3rem, 3.2vw, 2rem); line-height: 1.14;
  color: var(--ink); max-width: 22ch;
}

/* ══════════════════════════════════════════════════════════════════════════
   FOOTER, IN TWO REGISTERS
   The name and the section links sit in a paper band at the top of the
   footer; contact, elsewhere and status sit on the ink below it. That is the
   only way the name keeps the EXACT hero rose (#8E2C3E) and stays legible:
   it measures 7.4:1 on paper and 2.5:1 on the ink, which would fail even the
   large-text threshold. One footer, two grounds, no stray band on the page.
   ══════════════════════════════════════════════════════════════════════════ */
.foot {
  background: none; color: inherit;
  overflow: visible; padding: 0;
  margin-top: clamp(3rem, 8vw, 5rem);
}
.foot__head {
  display: grid; gap: 1.25rem; justify-items: start;
  padding-block: clamp(2rem, 5vw, 3.25rem) clamp(2rem, 5vw, 3rem);
}
.foot__dark {
  position: relative; overflow: clip;
  background: var(--ink); color: var(--paper);
  padding-block: clamp(3rem, 8vw, 5.5rem) clamp(1.5rem, 3vw, 2rem);
  padding-bottom: max(clamp(1.5rem, 3vw, 2rem), env(safe-area-inset-bottom));
}
.foot__dark > * { position: relative; z-index: 1; }
.foot__dark .foot__city { z-index: 0; }
@media (min-width: 861px) {
  .foot__dark { padding-block: clamp(4.5rem, 10vw, 8rem) clamp(2rem, 4vw, 3rem); }
}

/* the name and the pills, now living inside the footer */
.foot__head .outro__name { margin: 0; }
.outro { display: none; }   /* the floating version is gone */

@media (max-width: 640px) {
  .foot__head { justify-items: start; }
}

/* The plate got both taller and stronger at once and took the footer over.
   It is a backdrop: present on a second look, never the first thing read. */
.foot__city img { opacity: .15; }
@media (min-width: 861px) {
  .foot__dark { padding-block: clamp(3rem, 6vw, 4.5rem) clamp(2rem, 4vw, 3rem); }
  .foot__city img { height: clamp(200px, 26vw, 400px); opacity: .17; object-position: center 30%; }
}
@media (max-width: 860px) {
  .foot__city img { height: clamp(110px, 28vw, 190px); opacity: .13; }
}

/* ══ CAIRN, CORRECTED ═════════════════════════════════════════════════════
   A falling stone travels UP from where it will land, so mid-fall it sits on
   top of the stones already settled and reads as a collision rather than as
   a drop. Fading it out much harder means it is effectively invisible until
   it is nearly home, and the only thing you see is the landing. */
.crock { opacity: calc(1 - var(--fall) * 2.6); }

/* Chunkier: at 1.7rem of vertical padding a one-line stone is a lozenge.
   Real cairn stones are thick relative to their length. */
.crock span { padding: 2.15rem 2.6rem; }
.crock + .crock { margin-top: clamp(-2.1rem, -3vw, -1.4rem); }
@media (max-width: 640px) {
  .crock span { padding: 1.6rem 1.6rem; }
  .crock + .crock { margin-top: -1.35rem; }
}

/* A frozen timeline also stalls smooth scrolling, so a nav click could leave
   the page where it was. Under the failsafe, scrolling is instant. */
html[data-nomotion] { scroll-behavior: auto !important; }

/* Sit them closer so the stack reads as one pile rather than six separate
   stones on a line. The overlap stays inside the padding, so no line of type
   is ever covered by the stone above it. */
.crock + .crock { margin-top: clamp(-2.9rem, -4vw, -2rem); }
@media (max-width: 640px) { .crock + .crock { margin-top: -1.75rem; } }

/* The one role that carries a job title as well as a period. */
.erock__role {
  font-family: var(--label); font-size: .6875rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--rose);
  margin-top: .15rem;
}

/* GitHub as its own mark, sized down so it reads as a footnote to the role. */
.ghlink {
  display: inline-grid; place-content: center;
  width: 30px; height: 30px; min-width: 30px; min-height: 30px;
  border-radius: 50%; color: var(--ink-faint);
  transition: color var(--dur-fast) var(--ease-standard),
              transform var(--dur-fast) var(--ease-standard);
}
.ghlink svg { width: 19px; height: 19px; display: block; }
.ghlink:hover  { color: var(--ink); transform: translateY(-2px); }
.ghlink:active { color: var(--rose); transform: none; }

/* A 16:9 plate cropped to a square loses its edges, and the Fred piece keeps
   its title there. Per-card focal point rather than a blanket centre crop. */
.wcard__shot img { object-position: var(--focal, center); }

/* ══════════════════════════════════════════════════════════════════════════
   FOOTER: THE NAME MOVES ONTO THE INK
   Measured on #0F0C0B: the hero rose #8E2C3E lands at 2.39:1, which is
   unreadable and fails even the 3:1 large-text floor. These are the SAME hue
   — hue and saturation are untouched, only the value is lifted, and only as
   far as legibility requires. 112px/800 name at 3.21:1, 11px pills at 4.78:1.
   ══════════════════════════════════════════════════════════════════════════ */
:root { --rose-ink: #AF364D; --rose-ink-sm: #CB586D; }

.foot { margin-top: clamp(3rem, 8vw, 5rem); }
.foot__dark { padding-top: clamp(2.5rem, 6vw, 4rem); }
.foot__head {
  display: grid; gap: 1.25rem; justify-items: start;
  padding-block: 0 clamp(2.5rem, 6vw, 4rem);
}
.foot__head .outro__name { color: var(--rose-ink); }
.foot__head .outro__name i { color: var(--paper); }
.foot__head .outro__name:hover   { color: var(--paper); }
.foot__head .outro__name:hover i { color: var(--rose-ink); }

.foot__head .outro__go button,
.foot__head .outro__go a {
  border-color: var(--rose-ink-sm); color: var(--rose-ink-sm); background: none;
}
.foot__head .outro__go button:hover,
.foot__head .outro__go a:hover { background: var(--rose-ink-sm); color: var(--ink); }
.foot__head .outro__go button:active,
.foot__head .outro__go a:active { background: var(--rose-300); border-color: var(--rose-300); }
.foot__head :focus-visible { outline-color: var(--paper); }

/* ══ CAIRN: bigger on desktop ════════════════════════════════════════════ */
@media (min-width: 861px) {
  .crock span {
    font-size: clamp(1.6rem, 3.1vw, 2.75rem);
    padding: 2.6rem 3.2rem;
  }
  .crock + .crock { margin-top: clamp(-3.6rem, -4.6vw, -2.4rem); }
  .creed { padding-block: clamp(2.5rem, 6vw, 4rem) clamp(5rem, 12vw, 8rem); }
}

/* ══════════════════════════════════════════════════════════════════════════
   FOOTER: SMALLER, AND A SKYLINE THAT SURVIVES BEING BIG
   The plate was a 570px source upscaled to 2400 — every edge was mush and the
   halftone dots turned into blobs. It is rebuilt as a silhouette: the mask is
   upscaled and re-thresholded, so the edges are sub-pixel clean at any size,
   and cropped to the roofline instead of the solid mass below it.
   ══════════════════════════════════════════════════════════════════════════ */
.foot__dark { padding-block: clamp(2.25rem, 5vw, 3.25rem) clamp(1.25rem, 2.5vw, 1.75rem); }
.foot__head { padding-block: 0 clamp(1.75rem, 4vw, 2.5rem); }
.foot__in   { gap: clamp(1.5rem, 4vw, 2.5rem); }
.foot__rule { margin-top: clamp(1.75rem, 4vw, 2.5rem); padding-top: 1rem; }

.foot__city img {
  height: clamp(120px, 17vw, 260px);
  object-fit: cover; object-position: center bottom;
  opacity: .13;
}
@media (min-width: 861px) {
  .foot__dark { padding-block: clamp(2.5rem, 5vw, 3.5rem) clamp(1.5rem, 3vw, 2rem); }
  .foot__city img { height: clamp(150px, 18vw, 280px); opacity: .14; }
}
@media (max-width: 860px) {
  .foot__city img { height: clamp(90px, 22vw, 150px); opacity: .11; }
}

/* Neither of these needs a promoted layer. A fixed element three viewports
   tall with will-change:transform is a very large composited surface, and
   re-rastering it is exactly what a fast scroll cannot afford. */
.topo, .river { will-change: auto; }
.topo { height: 200vh; }

/* The band is now the roofline itself, so it is anchored near its own top —
   the Chrysler spire is the whole reason to use this plate and a bottom
   anchor cuts it off. */
.foot__city img {
  height: clamp(120px, 20vw, 300px);
  object-fit: cover; object-position: center 8%;
  opacity: .16;
}
@media (max-width: 860px) { .foot__city img { height: clamp(88px, 24vw, 150px); opacity: .13; } }

/* ══ HERO: less dead air above the portrait ═════════════════════════════
   The stage was 86svh with the figure bottom-aligned, so everything between
   the nav and the top of his head was empty. */
.stage {
  min-height: 74svh;
  padding-block: 4.25rem clamp(1.25rem, 3vw, 2.25rem);
}
@media (max-width: 760px) { .stage { min-height: 70svh; padding-top: 4rem; } }

/* ══ FOOTER BUG ══════════════════════════════════════════════════════════
   `.foot__dark > *` set position:relative on every direct child, which
   included the skyline plate and knocked it out of absolute positioning and
   into normal flow. It was adding its own 288px to the footer height and
   opening the gap between the links and the name. */
.foot__dark > .foot__city {
  position: absolute; inset: auto 0 0 0; z-index: 0;
  line-height: 0; pointer-events: none;
}
.foot__city img { object-position: center 58%; opacity: .17; }

/* ══ HERO SPACING: mobile only ══════════════════════════════════════════
   Desktop keeps its full stage; the dead air above the figure was a phone
   problem, so the correction belongs at phone width. */
.stage { min-height: 86svh; padding-block: 5rem clamp(1.5rem, 4vw, 3rem); }
@media (max-width: 760px) {
  .stage { min-height: 64svh; padding-block: 3.75rem clamp(1rem, 3vw, 1.75rem); }
}

@media (max-width: 760px) {
  .stage { min-height: 52svh; padding-block: 3.5rem clamp(.75rem, 2vw, 1.25rem); }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE SKYLINE, DRAWN
   The photographic plate was a 570px source. Silhouetting it got the edges
   clean, but a 570px original stretched across a 1440px footer has no detail
   left to give, and its provenance was never confirmed. This is drawn: two
   paths, resolution-free, a couple of kilobytes, no network request, and it
   speaks the same flat-shape language as the boulders and the contour field.
   ══════════════════════════════════════════════════════════════════════════ */
.foot__sky {
  display: block; width: 100%;
  height: clamp(110px, 17vw, 240px);
  color: var(--paper);
}
.foot__sky-b { fill: currentColor; fill-opacity: .16; }
.foot__sky-w { fill: currentColor; fill-opacity: .30; }
@media (max-width: 860px) {
  .foot__sky { height: clamp(80px, 22vw, 130px); }
  .foot__sky-b { fill-opacity: .14; }
  .foot__sky-w { fill-opacity: .26; }
}

/* Quieter, and short enough that the roofline sits under the link columns
   rather than running through them. */
.foot__sky { height: clamp(96px, 13vw, 190px); }
.foot__sky-b { fill-opacity: .11; }
.foot__sky-w { fill-opacity: .21; }
@media (max-width: 860px) {
  .foot__sky { height: clamp(72px, 18vw, 110px); }
  .foot__sky-b { fill-opacity: .10; }
  .foot__sky-w { fill-opacity: .18; }
}

/* ══════════════════════════════════════════════════════════════════════════
   EXPERIENCE STONES KEEP THEIR BANKS BELOW 861px
   The alternation was gated behind the desktop breakpoint, so every stone
   below it spanned the full column and the block became the one part of the
   page that does not follow the curve. The stones now take a smaller bite of
   the width at every size and swap sides; the inset is what reads as the
   river, so it has to survive the phone.
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 860px) {
  .erock            { width: 87%; }
  .erock--l         { margin-right: auto; margin-left: 0; }
  .erock--r         { margin-left: auto; margin-right: 0; }
  .erock--l .erock__rock { transform: scaleX(-1); }
}
/* At phone width 13% of the column is only ~50px, which is not enough offset
   to read as alternation, so the bite gets deeper rather than shallower and
   the type sizes down to pay for it. */
@media (max-width: 640px) {
  .erock     { width: 91%; }
  .erock__in { padding: 11% 8.5%; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE CAIRN: MORE DISORDER, MORE PRESENCE
   Two separate complaints. Disorder was authored inside MOT-2407's stated
   ranges (x-drift +/-14%, rest +/-4deg) and at those values six stones of
   similar mass still read as a centred column with a wobble. The drift now
   runs to +/-22deg-equivalent offsets, the overlap varies per stone instead
   of being one constant, and alternate stones mirror their silhouette.

   Presence is the harder one, because fill-opacity is capped at ~.10 by
   CRD-2406 -- past that the stone stops being a container and becomes a
   panel, which is tell #17 wearing a costume. So the fill stays where it is
   and the weight comes from the three levers that are not capped: a darker
   currentColor, a drop shadow that separates overlapping stones, and type at
   full ink rather than ink-dim.
   ══════════════════════════════════════════════════════════════════════════ */
.pile { --dscale: 1; --rscale: 1; }
.crock {
  transform:
    translate3d(calc(var(--dx, 0) * var(--dscale)),
                calc(var(--fall) * -1 * var(--drop, 260px)), 0)
    rotate(calc((var(--rest, 0deg) + var(--fall) * var(--tilt, 0deg)) * var(--rscale)));
}
/* Per-stone overlap. A single constant made the vertical rhythm regular even
   while the horizontal one was not, and regularity in one axis is enough to
   give the whole pile away. */
.crock + .crock { margin-top: var(--lap, -1.6rem); }

/* currentColor crosses the <use> boundary; fill-opacity does not. Darkening
   the ink is therefore the only way to add weight without touching the
   shared defs, which the offer and experience stones also draw from. */
/* Two shadows, not one. The tight pass is a contact shadow -- it is what puts
   each stone ON the one beneath rather than merely overlapping it, and it is
   the whole reason the pile reads as depth instead of as a stack of decals.
   The wide pass is ambient and carries the mass. A single mid-radius shadow
   does neither job. */
.crock__rock {
  color: var(--rose-900);
  filter: drop-shadow(0 2px 3px rgb(94 27 40 / .20))
          drop-shadow(0 12px 24px rgb(94 27 40 / .13));
}
.crock--flip .crock__rock { transform: scaleX(-1); }
/* The failsafe blanket-resets transform on every rock, so the mirror has to be
   restated there the same way .crock--l already is, or half the silhouettes
   snap back and the variety disappears exactly when the script has failed. */
[data-nomotion] .crock--flip .crock__rock { transform: scaleX(-1) !important; }
.crock span { color: var(--ink); }
.crock[data-live] span { color: var(--rose-900); }

/* The drift is a percentage of each stone's own width, and on a phone the
   stones are nearly the full column -- 22% of that walks straight off the
   screen. Scaled down here rather than re-authored per stone, and the
   rotation comes down with it because a 5deg tilt on a full-width stone
   lifts its corners much further than on a short one. */
@media (max-width: 860px) { .pile { --dscale: .58; --rscale: .78; } }
@media (max-width: 640px) { .pile { --dscale: .34; --rscale: .58; } }

@media (prefers-reduced-motion: reduce) {
  .crock__rock { filter: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   FOOTER, CONDENSED
   The dark plate was carrying a 7rem name, a 2.5rem gap under it, four link
   rows at .375rem gutters and its own block padding on top of the section
   margin above it. Nothing in it needed that much room -- it is a sign-off,
   not a section.
   ══════════════════════════════════════════════════════════════════════════ */
.foot { margin-top: clamp(1.5rem, 4vw, 2.5rem); }
.foot__dark { padding-block: clamp(1.5rem, 3vw, 2.25rem) clamp(.875rem, 1.75vw, 1.25rem); }
.foot__head { padding-block: 0 clamp(1rem, 2.25vw, 1.5rem); }
.foot__head .outro__name { font-size: clamp(2.25rem, 7.5vw, 4.5rem); line-height: .82; }
.foot__head .outro__go { margin-top: .75rem; }
.foot__head .outro__go button,
.foot__head .outro__go a { padding: .5rem .8rem; min-height: 34px; font-size: .625rem; }

.foot__in   { gap: clamp(1rem, 2.5vw, 1.75rem); row-gap: clamp(1.25rem, 3vw, 1.75rem); }
.foot__block { gap: .3rem; }
.foot__links { gap: 0; }
.foot__links a { padding-block: .2rem; }
.foot__mail { padding-block: .125rem; }
.foot__rule { margin-top: clamp(1rem, 2.5vw, 1.5rem); padding-top: .75rem; }

/* The roofline was tall enough to need clearance under the link columns.
   Shorter, it can sit behind them, which is what buys most of the height --
   but it is now behind type rather than below it, so it has to come down in
   density too. Light buildings under light text eat the contrast the meta row
   depends on, and that row is the smallest type on the page. */
.foot__sky { height: clamp(64px, 8.5vw, 120px); }
.foot__sky-b { fill-opacity: .07; }
.foot__sky-w { fill-opacity: .14; }
@media (max-width: 860px) {
  .foot__sky-b { fill-opacity: .06; }
  .foot__sky-w { fill-opacity: .12; }
}

@media (max-width: 860px) {
  .foot__dark { padding-block: clamp(1.25rem, 3vw, 1.75rem) clamp(.75rem, 2vw, 1rem); }
  .foot__head { padding-block: 0 clamp(.875rem, 2.5vw, 1.25rem); }
  .foot__sky  { height: clamp(52px, 13vw, 84px); }
}

/* ══════════════════════════════════════════════════════════════════════════
   REEL STOP CONTROL — WCAG 2.2.2
   The reel auto-advances every 3.2s and previously could only be delayed,
   never stopped. This is the stop. It sits above the strip, right-aligned
   under the section, in the mono label face so it reads as instrumentation
   rather than as a CTA -- it must be findable without competing with the
   photographs or with the one real call to action on the page.
   ══════════════════════════════════════════════════════════════════════════ */
.slat-stop {
  display: flex; align-items: center; gap: .5rem;
  margin: 0 0 .6rem auto;
  padding: .4rem .7rem;
  min-height: 32px;
  background: none; border: 1px solid transparent; border-radius: 999px;
  font-family: var(--label); font-size: .625rem; font-weight: 500;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard);
}
.slat-stop:hover { color: var(--ink); border-color: var(--rose-700); }
.slat-stop:focus-visible { outline: 2px solid var(--rose-700); outline-offset: 2px; }

/* Two bars when running (the affordance is "pause"), one triangle when
   halted ("play"). Drawn rather than iconified so it inherits currentColor
   and needs no extra request. */
.slat-stop__ico {
  width: 9px; height: 11px; flex: none;
  border-left: 3px solid currentColor; border-right: 3px solid currentColor;
}
.slat-stop[aria-pressed="true"] .slat-stop__ico {
  width: 0; height: 0;
  border-left: 9px solid currentColor; border-right: 0;
  border-top: 6px solid transparent; border-bottom: 6px solid transparent;
}

/* prefers-reduced-motion already stops the reel in script, so the control
   would be a button that does nothing. Hide it rather than lie. */
@media (prefers-reduced-motion: reduce) {
  .slat-stop { display: none; }
}

/* ══ DESKTOP CORRECTION ═══════════════════════════════════════════════════
   Every size on this page is a clamp with a vw middle term, and the caps
   were tuned so they are reached at roughly 1120px. Past that the vw term
   keeps climbing, the clamp keeps returning its MAX, and the layout stops
   responding entirely: a 1280px laptop and a 1920px monitor get identical
   112px section padding and identical 32px setup lines, while the viewport
   they sit in has grown by half. Measured at 1920x1032 the page ran 5.2
   screens tall, and a stop block was 224px of padding around 281px of
   content, so a single short line owned most of a screen.

   This block lowers the CAPS only, above the 1100px breakpoint the file
   already uses for the hero tagline. Nothing below desktop is touched: the
   vw terms still do all the work there, and every value here still carries
   a range so it keeps scaling rather than freezing again. */
@media (min-width: 1100px) {
  /* Outer air around each statement block: 112px -> 80px. */
  .stop { padding-block: clamp(4.5rem, 6.5vw, 5rem); }
  /* And the air inside the rock plate: 72px -> 56px. The horizontal padding
     is left alone; the rock needs its width to read as a rock. */
  .stop__in { padding-block: clamp(2.75rem, 5vw, 3.5rem); }

  /* Dead space between the closing line and the footer: 160px -> 104px. */
  .closer { padding-bottom: clamp(5rem, 8vw, 6.5rem); }

  /* Secondary type. The display sizes are the site's voice and are left as
     they are; these four are running copy that had drifted to 1.5-2x body
     size with nothing above them to justify it. */
  .stop__stmt   { font-size: clamp(1.15rem, 2.5vw, 1.45rem); }  /* 27.2 -> 23.2 */
  .stop__small  { font-size: clamp(1.3rem, 3.2vw, 1.8rem); }    /* 32   -> 28.8 */
  .stop__turn   { font-size: clamp(1.35rem, 3.4vw, 2rem); }     /* 35.2 -> 32   */
  .closer__body { font-size: clamp(1.05rem, 1.9vw, 1.35rem); }  /* 24.8 -> 21.6 */
}

/* ══ DESKTOP, ROUND TWO ═══════════════════════════════════════════════════
   Same principle as the block above and the same 1100px gate: the caps were
   reached at laptop width and never moved again, so on a large screen the
   work and the skills read as small cards floating in a lot of paper. These
   raise the caps instead of lowering them. Nothing below 1100px changes. */
@media (min-width: 1100px) {

  /* ── The portrait is off the left edge now. It sat at left: 0, so the
        window cropped his shoulder and the figure read as stuck to the
        frame rather than standing in it. */
  .stage__portrait { margin-left: clamp(2rem, 4vw, 4.5rem); }

  /* ── The three site cards. They were 392px wide inside the 1240px
        measure, which is smaller than the screenshots they contain. The
        work is the point of the page, so the row gets its own wider
        measure than the running text. */
  .wrap.sites { width: min(1560px, 100% - 4rem); }
  .sites__grid { gap: clamp(1.5rem, 2.5vw, 2.25rem); }
  .srock__name { font-size: clamp(1.05rem, 1.5vw, 1.3rem); }
  .srock__note { font-size: clamp(.9375rem, 1.1vw, 1.0625rem); }

  /* ── Craft rows: the headings, the line under each, and the chips. */
  .craft__row h3 { font-size: clamp(1.6rem, 4.4vw, 3rem); }      /* 40 -> 48 */
  .craft__row p  { font-size: clamp(1rem, 1.5vw, 1.25rem); }     /* 16 -> 20 */
  .craft__skills li {
    font-size: .8125rem; letter-spacing: .11em;                  /* 11 -> 13 */
    padding: .4375rem .875rem;
  }
  /* The pebbles are cut from px radii measured against the old ~31px chip.
     A taller chip with the same radii reads as a rounded rectangle, so the
     vertical radii scale with it and the corners stay round. */
  .craft__skills li                  { border-radius: 30px 16px 25px 20px / 17px 14px 17px 15px; }
  .craft__skills li:nth-child(6n+2)  { border-radius: 16px 32px 18px 27px / 14px 17px 15px 17px; }
  .craft__skills li:nth-child(6n+3)  { border-radius: 23px 18px 32px 16px / 17px 15px 14px 17px; }
  .craft__skills li:nth-child(6n+4)  { border-radius: 18px 30px 16px 27px / 15px 17px 17px 14px; }
  .craft__skills li:nth-child(6n+5)  { border-radius: 32px 17px 25px 19px / 17px 14px 16px 17px; }
  .craft__skills li:nth-child(6n)    { border-radius: 17px 25px 19px 34px / 14px 17px 15px 17px; }
}

/* ══ NAV: no plate ════════════════════════════════════════════════════════
   Scrolling dropped a near-opaque paper bar with a blur and a hairline
   across the top of the page, so the terrain ran under a solid strip. The
   controls carry their own backgrounds — the switch is a pill, the two
   discs are filled circles — so they stay legible with nothing behind them
   and the ground stays continuous. */
.nav[data-scrolled="true"] {
  background: transparent;
  border-color: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
@supports not (backdrop-filter: blur(1px)) {
  .nav[data-scrolled="true"] { background: transparent; }
}

/* ══ OFFER CARDS ══════════════════════════════════════════════════════════
   These three sit directly on the contour ground, so they need both size
   and separation. Full ink rather than the dimmed grey, and a wider paper
   halo so the bands do not run through the letterforms. */
.orock__in p { color: var(--ink); }
@media (min-width: 1100px) {
  .orock__in b { font-size: clamp(1.2rem, 2.5vw, 1.85rem); }   /* 24.8 -> 29.6 */
  .orock__in p {
    font-size: clamp(.9375rem, 1.35vw, 1.125rem);              /* 15   -> 18   */
    max-width: 30ch;
    text-shadow: 0 0 22px var(--paper), 0 0 14px var(--paper),
                 0 0 7px var(--paper), 0 0 3px var(--paper),
                 0 0 1px var(--paper);
  }
}

/* ══ HERO RIGHT-HAND CLUSTER ══════════════════════════════════════════════
   The vertical name and the tagline both sit against the right edge. Easing
   the stage's right padding walks the whole cluster outward together, and
   the tagline gives up some of its own right padding on top of that so it
   closes the gap to the name instead of floating mid-screen. */
@media (min-width: 1100px) {
  .stage { padding-right: clamp(.75rem, 1.5vw, 1.5rem); }   /* 40px -> 24px */
  /* The tagline is absolutely positioned and offset with `right`, not with
     padding, so `right` is the only thing that moves it. */
  .stage__tag { right: clamp(6.5rem, 9vw, 10rem); }         /* 224px -> 160px */
}

/* ══ BIO COPY ═════════════════════════════════════════════════════════════
   The three paragraphs are the most-read text on the About view and they sat
   at 16px under a 103px title. The measure is set in ch, so it holds the same
   character count as the type grows. */
@media (min-width: 1100px) {
  .bio__copy p { font-size: clamp(1rem, 1.5vw, 1.25rem); }   /* 16 -> 20 */
}

/* ══ CAIRN + WRITING CARDS ════════════════════════════════════════════════
   The cairn stones were meant to be large on desktop -- there is a rule
   above named "CAIRN: bigger on desktop" that sets 2.75rem -- but it has
   never once applied. `.crock span` scores (0,1,1) and loses to
   `.creed li:not(.key) span { font-size: inherit }` at (0,2,2), so every
   stone has been rendering at the inherited 16px regardless of viewport.
   Specificity, not the cascade order, is the reason; the id here is what
   finally outranks it. */
@media (min-width: 1100px) {
  #pile .crock span { font-size: clamp(1.5rem, 2.6vw, 2.5rem); }  /* 16 -> 40 */
  .wcard__note { font-size: clamp(.9375rem, 1.15vw, 1.125rem); }  /* 14 -> 18 */
}

/* ══ GITHUB LINK ══════════════════════════════════════════════════════════
   It was a 30px target sitting immediately beside the "See the work" button,
   reading as a second half of that control. space-between sends it to the
   far edge of the card's inner box so it corners properly, and the target
   grows past the 44px minimum. */
@media (min-width: 1100px) {
  .erock__go { width: 100%; justify-content: space-between; }
  .ghlink {
    width: 44px; height: 44px; min-width: 44px; min-height: 44px;
  }
  .ghlink svg { width: 27px; height: 27px; }

  /* ══ EDUCATION BLOCK ═══════════════════════════════════════════════════
     The degree line and the GPA under CUM LAUDE were both at 11px, which is
     the label floor, directly under a 64px school name. */
  .edu__degree     { font-size: clamp(.6875rem, 1vw, .9375rem); }  /* 11 -> 15 */
  .edu__facts span { font-size: clamp(.6875rem, 1vw, .9375rem); }  /* 11 -> 15 */
  .edu__facts b    { font-size: clamp(1.1rem, 2.8vw, 2rem); }      /* 24 -> 32 */
}

/* ══ DESKTOP TYPE, +9px ═══════════════════════════════════════════════════
   Second pass over everything raised above: another ~9px on each, per the
   brief. Later in the file at equal specificity, so these win. The chips
   need their padding and their pebble radii rescaled alongside the type,
   or a 44px-tall chip with radii cut for a 36px one stops reading as round. */
@media (min-width: 1100px) {
  .craft__row h3    { font-size: clamp(1.6rem, 4.4vw, 3.5625rem); }  /* 48 -> 57 */
  .craft__row p     { font-size: clamp(1rem, 1.5vw, 1.8125rem); }    /* 20 -> 29 */
  .bio__copy p      { font-size: clamp(1rem, 1.7vw, 1.8125rem); }    /* 20 -> 29 */
  .orock__in b      { font-size: clamp(1.2rem, 2.5vw, 2.4125rem); }  /* 29.6 -> 38.6 */
  .orock__in p      { font-size: clamp(.9375rem, 1.35vw, 1.6875rem); }/* 18 -> 27 */
  #pile .crock span { font-size: clamp(1.5rem, 2.6vw, 3.0625rem); }  /* 40 -> 49 */
  .wcard__note      { font-size: clamp(.9375rem, 1.5vw, 1.6875rem); }/* 18 -> 27 */
  .srock__name      { font-size: clamp(1.05rem, 1.7vw, 1.8625rem); } /* 20.8 -> 29.8 */
  .srock__note      { font-size: clamp(.9375rem, 1.5vw, 1.625rem); } /* 17 -> 26 */
  .edu__degree      { font-size: clamp(.6875rem, 1.4vw, 1.5rem); }   /* 15 -> 24 */
  .edu__facts span  { font-size: clamp(.6875rem, 1.4vw, 1.5rem); }   /* 15 -> 24 */
  .edu__facts b     { font-size: clamp(1.1rem, 2.8vw, 2.5625rem); }  /* 32 -> 41 */

  .craft__skills li {
    font-size: 1.375rem; letter-spacing: .06em;                      /* 13 -> 22 */
    padding: .5rem 1.125rem;
  }
  .craft__skills li                  { border-radius: 40px 22px 34px 27px / 22px 18px 22px 19px; }
  .craft__skills li:nth-child(6n+2)  { border-radius: 22px 43px 24px 36px / 18px 22px 19px 22px; }
  .craft__skills li:nth-child(6n+3)  { border-radius: 31px 24px 43px 22px / 22px 19px 18px 22px; }
  .craft__skills li:nth-child(6n+4)  { border-radius: 24px 40px 22px 36px / 19px 22px 22px 18px; }
  .craft__skills li:nth-child(6n+5)  { border-radius: 43px 22px 34px 26px / 22px 18px 21px 22px; }
  .craft__skills li:nth-child(6n)    { border-radius: 22px 34px 26px 46px / 18px 22px 19px 22px; }
}

/* ══ EXPERIENCE CARDS ═════════════════════════════════════════════════════
   The card body, plus the heading and the two labels above it. Those labels
   are at the 11px floor, and leaving them there under 24px body copy would
   read as broken rather than as hierarchy, so they come up with it. */
@media (min-width: 1100px) {
  .erock__p    { font-size: clamp(.9375rem, 1.35vw, 1.5rem); }    /* 15 -> 24 */
  .erock h3    { font-size: clamp(1.35rem, 2.6vw, 2.5rem); }      /* 29.6 -> 40 */
  .erock__k    { font-size: clamp(.6875rem, 1vw, .9375rem); }     /* 11 -> 15 */
  .erock__role { font-size: clamp(.6875rem, 1vw, .9375rem); }     /* 11 -> 15 */
}

/* ══ LINK BUTTONS ═════════════════════════════════════════════════════════
   "See the work" and "Open the archive" were 11px labels in a 44px pill,
   which is a phone-sized control sitting under 40-57px headings. The
   --sm variant inside the experience cards comes up too, by less, so it
   stays subordinate to the full-size one. */
@media (min-width: 1100px) {
  .link-go {
    font-size: clamp(.6875rem, 1.1vw, 1.0625rem);   /* 11 -> 17 */
    padding: 1.0625rem 1.75rem; min-height: 56px;
    letter-spacing: .15em;
  }
  .link-go svg { width: 22px; height: 16px; }
  .link-go--sm {
    font-size: clamp(.6875rem, .95vw, .9375rem);    /* 11 -> 15 */
    padding: .8125rem 1.375rem; min-height: 48px;
  }
}

/* ══ FOOTER SKYLINE ═══════════════════════════════════════════════════════
   Two faults. First, the plate is an inline <svg class="foot__sky">, but a
   dozen rules further up target `.foot__city img` — an <img> that has not
   existed since the photographic plate was replaced by drawn paths. Every
   one of those height / opacity / object-position rules is dead, which is
   why nothing responded to them.

   Second, the live rules had walked the skyline down to 120px tall at
   fill-opacity .07/.14, which is past faint into invisible. The viewBox is
   1200x220 with preserveAspectRatio slice, so at 1920 wide the art scales
   1.6x to 352px and a 120px box was showing only its bottom third — the
   roofline and every spire were cropped off the top. That is the cutoff.

   18.4vw is width x 220/1200, the exact height at which the whole skyline
   fits at full bleed, so it stays complete at any desktop width. */
@media (min-width: 1100px) {
  .foot__sky   { height: clamp(160px, 18.4vw, 400px); }
  .foot__sky-b { fill-opacity: .17; }
  .foot__sky-w { fill-opacity: .30; }
}

/* ══ HERO TITLE, BACK OFF THE EDGE ════════════════════════════════════════
   Earlier this padding came down to 24px to walk the vertical name outward.
   That went too far — it now sits ~86px in. The tagline is absolutely
   positioned with its own `right` offset, so this moves the name only and
   leaves the tagline where it is. */
@media (min-width: 1100px) {
  .stage { padding-right: clamp(2rem, 4.5vw, 5.5rem); }
}

/* ══ NAV: pulled in and scaled up ═════════════════════════════════════════
   The bar is fixed full-bleed with a 1fr auto 1fr grid, so at 1920 the two
   discs sat hard against opposite edges with the switch marooned in the
   middle — a control strip nearly two thousand pixels wide holding three
   small objects. Padding brings the ends inward to a sane measure, and the
   targets and labels come up off the phone-sized floor. */
@media (min-width: 1100px) {
  .nav { padding-inline: clamp(2.5rem, 9vw, 11rem); }
  .disc, .mail {
    width: 54px; height: 54px; min-width: 54px; min-height: 54px;
  }
  .disc svg { width: 23px; height: 23px; }
  .mail svg { width: 21px; height: 21px; }
  .nav__switch { padding: .3125rem; }
  .nav__switch button {
    font-size: clamp(.625rem, .95vw, .875rem);      /* 10 -> 14 */
    padding: .625rem clamp(1rem, 1.6vw, 1.5rem);
    letter-spacing: .16em;
  }
  .nav__ind { top: .3125rem; bottom: .3125rem; left: .3125rem; }
}

/* ══════════════════════════════════════════════════════════════════════════
   DESKTOP VERTICAL RHYTHM
   At 1440x900 the page ran 5366px across eight blocks, so no block filled a
   viewport and a single wheel gesture crossed a whole section. Space is the
   only separator this design has, so the fix is space, not slower scrolling:
   never hijack the wheel. Desktop only -- the mobile rhythm is re-authored,
   not scaled, and must not inherit these.
   ══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 1024px) {
  .stop   { padding-block: clamp(8rem, 16vw, 14rem); }
  .sites  { padding-block: clamp(4rem, 8vw, 6.5rem) clamp(5rem, 11vw, 9rem); }
  .offer  { padding-block: clamp(4rem, 8vw, 6.5rem) clamp(5rem, 11vw, 9rem); }
  .craft  { padding-block: clamp(4rem, 8vw, 6.5rem) clamp(7rem, 14vw, 11rem); }
  .work   { padding-block: clamp(4rem, 9vw, 7rem) clamp(5rem, 11vw, 8rem); }
  .creed  { padding-block: clamp(4rem, 9vw, 6rem) clamp(6rem, 14vw, 9rem); }
  .reel   { padding-block: clamp(3rem, 7vw, 5rem) clamp(6rem, 14vw, 9.5rem); }
  .bio    { padding-block: clamp(7rem, 14vw, 10rem) clamp(4rem, 8vw, 6rem); }
  .edu    { padding-block: clamp(2rem, 5vw, 3.5rem); }
  .pair,
  .view--about .erock { margin-block: clamp(1.5rem, 4vw, 3rem); }
}

/* ══════════════════════════════════════════════════════════════════════════
   VERTICAL TITLE: KEEP THE INK ON SCREEN
   .ttl is writing-mode: vertical-rl, where line-height sets the line box's
   HORIZONTAL advance. At .88 the box (70px at an 80px font) is narrower than
   the glyphs actually paint (96px), so the ink overhangs ~.16em on each side.
   .stage has overflow:clip, so on a narrow screen that overhang is sheared
   off -- the right edge of the name goes missing on an iPhone.

   Pad by more than the overhang, in em so it tracks the font size at every
   clamp step, and hold a gutter that respects the notch in landscape.
   ══════════════════════════════════════════════════════════════════════════ */
.ttl { padding-right: .22em; }

/* WebKit sizes this block wrong against a writing-mode: vertical-rl child:
   the parent lands correctly at the stage's content edge, then the .ttl box
   overhangs it by ~55px to the right and the name is sheared off on an
   iPhone. Chrome does not do this, which is why it only ever showed up on
   device. Shrink-wrapping the parent to its content makes both engines
   agree. Measured on iPhone SE / 12 Mini / 14 Pro / 14 Pro Max. */
.stage__title { width: max-content; }

.stage {
  padding-right: max(clamp(1rem, 3vw, 2.5rem), env(safe-area-inset-right, 0px));
  padding-left:  max(clamp(1rem, 3vw, 2.5rem), env(safe-area-inset-left, 0px));
}
@media (max-width: 760px) {
  .stage {
    padding-right: max(1rem, env(safe-area-inset-right, 0px));
    padding-left:  max(.75rem, env(safe-area-inset-left, 0px));
  }
}

/* Second education entry. Smaller than the degree-granting one above it, and
   spaced off the note so the two entries do not read as one run-on block. */
.edu__school--alt {
  font-size: clamp(1.5rem, 5vw, 2.75rem);
  margin-top: clamp(1.75rem, 5vw, 2.75rem);
}
