/* ==================================================================
   gamasierraalta.com
   Chrome is achromatic. The only hue on the site lives in the orb
   field on the landing page (navy → warm amber) and in the two
   atmosphere tokens below that ground it.
   ================================================================== */

:root {
  --ink: #000000;
  --paper: #F4F4F2;
  --mist: #9B9B98;
  --smoke: #5C5C5A;
  --hair: rgba(244, 244, 242, 0.14);
  --hair-strong: rgba(244, 244, 242, 0.34);

  /* Field anchors — the two ends of the orb ramp, mirrored in field.js */
  --navy: #103A8C;
  --amber: #DE9A40;

  /* How much of the headline shows through before the pointer clears it.

     Both are 0.50 now — the landing used to be 0.11, a deliberate
     near-blackout, back when the veil was the only thing covering the
     words. The pile of digits does that job there now, so the layer
     underneath no longer has to hide; it only has to stay quieter than
     the numbers stacked on top of it. One number across all three pages,
     and the headline is readable standing still everywhere: ~4.8:1
     against this background, which clears WCAG AA for *normal* text,
     never mind the large-text floor these headlines actually fall under.
     The reveal is a flourish on every page rather than a gate.

     They stay two tokens rather than one because the two fields are
     still tuned separately, and the day the landing wants to diverge
     again it should not have to be untangled first. Both are raised by
     `prefers-contrast: more` below. */
  --veil: 0.50;
  --veil-page: 0.50;

  /* Bottom falloff for the interior field layers, so the navy melts into
     the page black instead of stopping on an edge. */
  --field-fade: linear-gradient(to bottom,
    #000 0%,
    #000 52%,
    rgba(0, 0, 0, 0.72) 72%,
    rgba(0, 0, 0, 0.28) 88%,
    transparent 100%);

  --gutter: clamp(20px, 4.2vw, 60px);
  --maxw: 1560px;

  /* Height of the floating masthead on the interior pages. Two things read
     it: the bar's own min-height, and the top padding that gives the page
     back the space the bar no longer occupies in flow. */
  --bar-h: 64px;

  --display: 'Archivo', 'Helvetica Neue', Arial, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

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

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(15px, 1.05vw, 17px);
  line-height: 1.55;
  letter-spacing: -0.005em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* ------------------------------------------------------------------
   Orb field — two canvases straddle the display type so some orbs
   drift behind the headline and some in front of it. That split is
   what gives the field depth; without it the dust looks like a decal.

     z 0  #field-back    far orbs
     z 1  .landing       the headline
     z 2  #field-front   near orbs
     z 3  masthead / colophon (must stay legible)
   ------------------------------------------------------------------ */

#field-back,
#field-front {
  position: fixed;
  inset: 0;
  display: block;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1400ms var(--ease);
}

#field-back  { z-index: 0; }
#field-front { z-index: 2; }

.field-ready #field-back,
.field-ready #field-front { opacity: 1; }

/* Atmosphere the orbs sit in — a cold navy well with one warm ember
   low and right, so the amber end of the ramp has something to belong to. */
body[data-field]::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(96% 82% at 44% 36%, rgba(16, 58, 140, 0.22), transparent 76%),
    radial-gradient(54% 50% at 84% 92%, rgba(222, 154, 64, 0.08), transparent 76%),
    radial-gradient(130% 120% at 50% 50%, #060A16 0%, #03050C 60%, #000000 100%);
}

/* Fallback when canvas is unavailable */
.no-field body::before {
  background:
    radial-gradient(60% 55% at 46% 34%, rgba(16, 58, 140, 0.34), transparent 70%),
    radial-gradient(40% 40% at 80% 86%, rgba(222, 154, 64, 0.14), transparent 70%),
    radial-gradient(120% 110% at 50% 50%, #070B18 0%, #000000 100%);
}

/* ------------------------------------------------------------------
   Type primitives
   ------------------------------------------------------------------ */

.mono {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mist);
}

a {
  color: inherit;
  text-decoration: none;
}

a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--paper);
  outline-offset: 4px;
}

/* Underline that draws from the left on hover */
.link {
  position: relative;
  display: inline-block;
  padding-bottom: 2px;
}
.link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 420ms var(--ease);
}
.link:hover::after,
.link:focus-visible::after { transform: scaleX(1); }

.link--on::after { transform: scaleX(1); }

/* ------------------------------------------------------------------
   Shell
   ------------------------------------------------------------------ */

.shell {
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.masthead {
  position: relative;
  z-index: 3;
  padding-top: clamp(22px, 3.4vw, 40px);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
}

.masthead__name {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--paper);
}

.masthead__nav {
  display: flex;
  gap: clamp(16px, 2.4vw, 34px);
}

/* Primary nav carries the .link underline and full-brightness type; the
   colophon links are the quiet ones. That is the way round it should have
   been: Work / About / Contact is how the site is actually navigated, and
   it was reading as the smaller offer next to two footer links.

   Which page you are on is the underline held open rather than a colour
   change — with every item at --paper now, brightness has nothing left to
   say. It keys off `aria-current` directly so the state cannot drift away
   from what the markup claims. */
.masthead__nav a { position: relative; font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--paper); }
.masthead__nav a[aria-current='page']::after { transform: scaleX(1); }

/* 11px mono caps is the right size to read at, but it leaves a hit area only
   ~17px tall — well under the 44px a thumb needs. The target grows on a
   pseudo-element instead of padding because .masthead aligns on the baseline,
   and padding would push these links off it. */
.masthead__nav a::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}

/* ------------------------------------------------------------------
   The floating masthead — interior pages only

   These pages are long: a case study runs several screens, and the nav
   sitting at the top of the document meant scrolling all the way back up
   to leave the page you were on. So on anything carrying .page the bar
   comes out of flow and rides above the content.

   The landing keeps its masthead in flow. It is one screen tall by
   design, nothing scrolls past, and a translucent bar over the number
   field would be blurring the one thing the page is there to show.
   ------------------------------------------------------------------ */

.page .masthead {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 40;

  /* Full-bleed bar, contents still on the shell's grid. The gutter is the
     floor; past --maxw the extra is whatever the centred shell leaves over
     on each side, so the name and nav stay in line with the text below
     them however wide the window gets. */
  padding: 0 max(var(--gutter), calc((100% - var(--maxw)) / 2 + var(--gutter)));
  min-height: var(--bar-h);
  align-items: center;

  /* See-through rather than solid: the field and the type keep moving
     underneath, which is the point of floating it. The fallback below
     carries the same bar on engines without backdrop-filter — at that
     opacity it reads as dark chrome instead of glass, which is the right
     way to fail. */
  background: rgba(4, 6, 13, 0.55);
  -webkit-backdrop-filter: blur(16px) saturate(130%);
  backdrop-filter: blur(16px) saturate(130%);
  border-bottom: 1px solid var(--hair);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .page .masthead { background: rgba(4, 6, 13, 0.92); }
}

/* The space the bar used to take up in flow, handed back. */
.page { padding-top: var(--bar-h); }


/* Anchors and focus rings must not land underneath the bar. scroll-padding
   belongs on the scrolling element, which is the root — :has() is what
   keeps it off the landing, where there is no bar to clear. Engines
   without :has() drop the rule and lose nothing but the allowance. */
:root:has(.page) { scroll-padding-top: calc(var(--bar-h) + 16px); }

/* ------------------------------------------------------------------
   Back to top

   Injected by site.js on .page only — see the note there about why it is
   not in the markup. Same glass as the bar, so the two read as one layer
   floating over the page rather than two unrelated widgets.
   ------------------------------------------------------------------ */

.to-top {
  position: fixed;
  right: clamp(16px, 2.6vw, 40px);
  bottom: clamp(16px, 2.6vw, 40px);
  z-index: 39;

  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--hair);
  border-radius: 50%;
  background: rgba(4, 6, 13, 0.55);
  -webkit-backdrop-filter: blur(16px) saturate(130%);
  backdrop-filter: blur(16px) saturate(130%);
  color: var(--paper);
  cursor: pointer;

  /* visibility, not display, so it can transition — and so it leaves the
     tab order entirely while it is out, rather than being a focusable
     control nobody can see. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  transition: opacity 320ms var(--ease),
              transform 320ms var(--ease),
              border-color 320ms var(--ease),
              visibility 0s linear 320ms;
}

.to-top.is-on {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

.to-top:hover { border-color: var(--hair-strong); }
.to-top svg { display: block; width: 15px; height: 15px; }

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .to-top { background: rgba(4, 6, 13, 0.92); }
}

/* ------------------------------------------------------------------
   Landing
   ------------------------------------------------------------------ */

.landing {
  position: relative;
  z-index: 1;
  min-height: 100svh;
  display: grid;
  grid-template-rows: auto 1fr auto;
}

.hero {
  display: flex;
  align-items: center;
  /* Tight, because the display type is now doing the breathing — and
     because five lines at 9.5vw only just clear the fold on a 1280×800
     laptop. Scales back up on taller screens. */
  padding: clamp(12px, 2vh, 56px) 0;
}

/* The signature: the headline sits dim under the orb field and is
   uncovered rather than faded in. Two stacked copies — a muted base
   and a lit one masked to a window that follows the cursor. field.js
   writes --mx / --my / --reveal. */
.hero__title {
  /* Still smaller than the cleared void — stars scatter ahead of the
     cursor — but wide enough now to hand back a clause at a time rather
     than two or three words, since there is a second layer of digits to
     get out of the way before any of it is readable. field.js clears
     those over a slightly wider radius again (glyphClear*), so the pile
     is gone before the words come up rather than lifting off them.

     The vw term carries it. It was set from the phone end on purpose:
     a 375px phone reads well because the window covers a big share of
     the headline at once, and 27vw is what puts a desktop at roughly
     the same share of a much wider block. The floor stays near 190px
     because past that a phone has the whole headline lit and there is
     nothing left to walk. */
  --reveal-r: clamp(190px, 27vw, 500px);
  --mx: 50%;
  --my: 50%;
  --reveal: 0;

  position: relative;
  display: block;
  margin: 0;
  font-family: var(--display);
  font-weight: 700;
  font-stretch: 105%;
  /* 25% up from the original clamp(2.55rem, 7.6vw, 8rem). At this size the
     headline still holds four lines above the fold on a 1280×800 laptop. */
  font-size: clamp(3.2rem, 9.5vw, 10rem);
  line-height: 0.9;
  letter-spacing: -0.042em;
  max-width: 20ch;
}

/* Bright by default; only dims once the field is actually running, so a
   failed script leaves a fully readable headline rather than a ghost. */
.hero__title__layer {
  display: block;
  color: var(--paper);
  transition: color 700ms var(--ease);
}

.field-ready .hero__title__layer { color: rgba(244, 244, 242, var(--veil)); }

.hero__title__layer--lit,
.field-ready .hero__title__layer--lit {
  position: absolute;
  inset: 0;
  color: var(--paper);
  opacity: var(--reveal);
  text-shadow: 0 0 44px rgba(222, 154, 64, 0.16);
  -webkit-mask-image: radial-gradient(circle var(--reveal-r) at var(--mx) var(--my),
    #000 0%, rgba(0, 0, 0, 0.86) 38%, rgba(0, 0, 0, 0) 78%);
  mask-image: radial-gradient(circle var(--reveal-r) at var(--mx) var(--my),
    #000 0%, rgba(0, 0, 0, 0.86) 38%, rgba(0, 0, 0, 0) 78%);
}

.hero__title em {
  font-style: normal;
  font-weight: 400;
  color: inherit;
}

/* Colophon prompt: one or the other, never both. */
.touch-only { display: none; }

/* Touch and tablet get the same veil and the same reveal — the well is
   driven by a drag instead of a hover (see TOUCH_LIFT in field.js, which
   floats it above the finger so it isn't covering what it uncovers).
   Only the prompt in the colophon changes. */
@media (hover: none), (pointer: coarse) {
  .cursor-only { display: none; }
  .touch-only { display: inline; }
}

/* The prompt does in miniature what it is describing: a bright window
   travels across the words the way the reveal travels across the headline.
   Landing only — it is the only page that asks for a gesture.

   The gradient is three times the width of the text and tiles, with both
   of its ends on --mist so the seam is invisible; that way the band can be
   parked off either edge and every letter still has paint under it. A
   no-repeat gradient leaves bare box at the extremes of the travel, and
   bare box under background-clip: text is invisible text. */
.prompt {
  --sweep: rgba(244, 244, 242, 0.94);
}

/* Nested inside the motion query rather than reset later: the sweep works
   by painting the text with a gradient and making the glyphs themselves
   transparent, and a rule that has to be *undone* for reduced motion is a
   rule that leaves the prompt invisible the day the undo gets edited. */
@media (prefers-reduced-motion: no-preference) {
  @supports (background-clip: text) or (-webkit-background-clip: text) {
    .prompt {
      background-image: linear-gradient(96deg,
        var(--mist) 0%,
        var(--mist) 36%,
        var(--sweep) 48%,
        var(--sweep) 52%,
        var(--mist) 64%,
        var(--mist) 100%);
      background-size: 300% 100%;
      background-position: 100% 0;
      background-repeat: repeat-x;
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
      -webkit-text-fill-color: transparent;
      animation: prompt-sweep 7s var(--ease) 2s infinite;
    }
  }
}

/* 100% → 0% is exactly one full pass: the band starts just off the left
   edge and ends just off the right. The rest of the cycle is the pause. */
@keyframes prompt-sweep {
  0%,  8%  { background-position: 100% 0; }
  56%      { background-position: 0% 0; }
  100%     { background-position: 0% 0; }
}

@media (prefers-contrast: more) {
  :root { --veil: 0.72; --veil-page: 0.92; }
  /* A moving highlight is the first thing to go when someone has asked
     for contrast: the prompt goes back to being flat, legible type. */
  .prompt { --sweep: var(--paper); background-image: none; -webkit-text-fill-color: currentColor; color: var(--paper); }
}

.hero__meta {
  margin-top: clamp(16px, 1.8vw, 28px);
  display: flex;
  flex-wrap: wrap;
  gap: 10px 28px;
  align-items: baseline;
}

.hero__meta p { margin: 0; }

.colophon {
  position: relative;
  z-index: 3;
  padding-bottom: clamp(22px, 3.4vw, 40px);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px 28px;
  border-top: 1px solid var(--hair);
  padding-top: 18px;
}

/* No links left above it — the divider has nothing to separate. */
.colophon--bare { border-top: 0; padding-top: 0; }

.colophon__links {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(16px, 2.2vw, 32px);
}

.colophon a { font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--mist); transition: color 300ms var(--ease); }
.colophon a:hover,
.colophon a:focus-visible { color: var(--paper); }

/* ------------------------------------------------------------------
   Interior pages
   ------------------------------------------------------------------ */

/* The floor was 60px. The back-to-top button is fixed to the viewport and
   reaches 86px up from its bottom edge at the widest clamp, so at the end of
   a page it would come to rest on top of the colophon's © line, which sits
   on the same side. 96px of tail keeps the button in empty space instead. */
.page { padding-bottom: clamp(96px, 10vh, 140px); }

.page__head {
  position: relative;
  /* Anchors the field. The canvases and the gradient deliberately bleed
     *outward* from this box — up behind the masthead and out to both
     viewport edges — using the --bleed-* offsets field.js measures, so the
     atmosphere reaches the top of the page the way the landing's does.
     `isolation` keeps their negative z-index inside this subtree; there is
     no `overflow: hidden` because the bleed depends on escaping the box,
     and a canvas clips its own drawing anyway.

     No border-bottom either: the field's bottom fade is the separator now,
     and a hard 1px rule would undo the point of it. */
  isolation: isolate;
  padding: clamp(56px, 11vh, 140px) 0 clamp(66px, 10vh, 130px);
}

/* ------------------------------------------------------------------
   Interior headline field — the same number field as the landing,
   anchored to .page__head instead of the viewport. field.js injects both
   canvases; they straddle the type exactly the way the landing's pair
   does, so motes drift under some letters and over others.

     z -2  ::before        atmosphere gradient
     z -1  --back canvas   far motes
     z  0  the headline    (ordinary in-flow content)
     z  1  --front canvas  near motes
     z  3  .masthead       above the lot, so motes never sit on the nav
                           (same order the landing uses)
   ------------------------------------------------------------------ */

[data-field-scope]::before {
  content: '';
  position: absolute;
  z-index: -2;

  /* Bleed out of .page__head: up to the top of the page and across to
     both viewport edges. field.js writes these from measured geometry —
     they're scroll-stable, so this only changes on resize. */
  top: calc(-1 * var(--bleed-top, 0px));
  left: calc(-1 * var(--bleed-left, 0px));
  right: calc(-1 * var(--bleed-right, 0px));
  bottom: 0;

  background:
    radial-gradient(120% 150% at 28% 10%, rgba(16, 58, 140, 0.28), transparent 74%),
    radial-gradient(70% 100% at 90% 105%, rgba(222, 154, 64, 0.11), transparent 76%),
    radial-gradient(160% 210% at 50% 30%, #060A16 0%, #03050C 55%, #000000 100%);
}

.number-field {
  position: absolute;
  display: block;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1200ms var(--ease);
}

.number-field--back  { z-index: -1; }
.number-field--front { z-index: 1; }

.field-ready .number-field { opacity: 1; }

/* Only the field layers are masked, never the container — mask .page__head
   itself and the lede would dissolve along with the atmosphere. */
[data-field-scope]::before,
[data-field-scope] .number-field {
  -webkit-mask-image: var(--field-fade);
  mask-image: var(--field-fade);
}

/* Same two-layer treatment as the landing headline — a veiled base and a
   lit copy masked to a window that follows the well — but the base sits
   at --veil-page (0.50), so this reads perfectly well without ever being
   touched. The reveal just brightens what you're already reading. */
.page__title {
  --reveal-r: clamp(150px, 15vw, 260px);
  --mx: 50%;
  --my: 50%;
  --reveal: 0;

  position: relative;
  display: block;
  margin: 14px 0 0;
  font-weight: 700;
  font-stretch: 105%;
  font-size: clamp(2.1rem, 5.4vw, 4.6rem);
  line-height: 0.95;
  letter-spacing: -0.038em;
  max-width: 22ch;
}

/* Bright by default; only drops to the veil once the field is actually
   running, so a failed script leaves a fully readable headline. */
.page__title__layer {
  display: block;
  color: var(--paper);
  transition: color 700ms var(--ease);
}

.field-ready .page__title__layer { color: rgba(244, 244, 242, var(--veil-page)); }

.page__title__layer--lit,
.field-ready .page__title__layer--lit {
  position: absolute;
  inset: 0;
  color: var(--paper);
  opacity: var(--reveal);
  text-shadow: 0 0 44px rgba(222, 154, 64, 0.16);
  -webkit-mask-image: radial-gradient(circle var(--reveal-r) at var(--mx) var(--my),
    #000 0%, rgba(0, 0, 0, 0.86) 38%, rgba(0, 0, 0, 0) 78%);
  mask-image: radial-gradient(circle var(--reveal-r) at var(--mx) var(--my),
    #000 0%, rgba(0, 0, 0, 0.86) 38%, rgba(0, 0, 0, 0) 78%);
}

.page__lede {
  margin: clamp(20px, 3vw, 30px) 0 0;
  max-width: 62ch;
  font-size: clamp(1.05rem, 1.5vw, 1.32rem);
  line-height: 1.5;
  letter-spacing: -0.012em;
  color: var(--paper);
}

/* Work index --------------------------------------------------- */

.work-list {
  border-top: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 2.4vw, 24px);
}

/* Each project is a single block-level link — the whole card is
   clickable, not just the title. Hover/focus lift the border and tint
   the background so the target reads as interactive at any zoom. */
.entry {
  position: relative;
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr) minmax(0, 30ch);
  gap: clamp(16px, 3vw, 48px);
  align-items: start;
  padding: clamp(28px, 4vw, 44px) clamp(22px, 3.4vw, 40px);
  color: inherit;
  border: 1px solid var(--hair);
  border-radius: 10px;
  transition: border-color 300ms var(--ease), background-color 300ms var(--ease), transform 300ms var(--ease);
}
.entry:hover,
.entry:focus-visible {
  border-color: var(--hair-strong);
  background: rgba(244, 244, 242, 0.035);
  transform: translateY(-2px);
}

.entry__arrow {
  position: absolute;
  top: clamp(20px, 3vw, 32px);
  right: clamp(20px, 3vw, 32px);
  display: block;
  color: var(--mist);
  transition: transform 300ms var(--ease), color 300ms var(--ease);
}
.entry__arrow svg { display: block; width: 18px; height: 18px; }
.entry:hover .entry__arrow,
.entry:focus-visible .entry__arrow {
  color: var(--paper);
  transform: translate(3px, -3px);
}

.entry__year { font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; color: var(--mist); padding-top: 0.6em; }

.entry__name {
  margin: 0;
  padding-right: clamp(26px, 4vw, 34px);
  font-size: clamp(1.5rem, 3.1vw, 2.6rem);
  font-weight: 600;
  letter-spacing: -0.032em;
  line-height: 1.02;
}

.entry__role { margin: 12px 0 0; color: var(--mist); max-width: 46ch; }

/* Extra top clearance keeps the note from running under the corner
   arrow in the three-column desktop layout. */
.entry__note { margin: 0; padding-top: clamp(26px, 3vw, 32px); padding-right: clamp(26px, 4vw, 34px); color: var(--paper); }

.entry__tags { margin: 18px 0 0; display: flex; flex-wrap: wrap; gap: 8px; padding: 0; list-style: none; }
.entry__tags li {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--mist);
  border: 1px solid var(--hair);
  border-radius: 999px;
  padding: 5px 11px;
}

.entry__status {
  display: inline-block;
  margin-left: 12px;
  vertical-align: middle;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--paper);
  padding: 4px 9px;
}

/* About -------------------------------------------------------- */

.rows { margin-top: clamp(40px, 6vw, 80px); }

.row {
  display: grid;
  grid-template-columns: minmax(0, 22ch) minmax(0, 1fr);
  gap: clamp(14px, 3vw, 56px);
  padding: clamp(26px, 3.6vw, 44px) 0;
  border-bottom: 1px solid var(--hair);
}

.row__label { padding-top: 0.35em; }

.row__body { max-width: 68ch; }
.row__body > p { margin: 0 0 1em; }
.row__body > p:last-child { margin-bottom: 0; }

.stack { list-style: none; margin: 0; padding: 0; }
.stack li { padding: 10px 0; border-bottom: 1px solid var(--hair); }
.stack li:last-child { border-bottom: 0; }
.stack strong { font-weight: 600; letter-spacing: -0.012em; }
.stack span { color: var(--mist); }

.inline-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px 10px; }
.inline-list.page-tags { margin-top: clamp(28px, 4vw, 44px); }
.inline-list li {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--mist);
  border: 1px solid var(--hair);
  border-radius: 999px;
  padding: 5px 11px;
}

.contact {
  margin-top: clamp(50px, 8vw, 100px);
  padding-top: clamp(30px, 4vw, 50px);
  border-top: 1px solid var(--hair);
}

.contact__line {
  margin: 16px 0 0;
  font-weight: 600;
  font-size: clamp(1.6rem, 4.2vw, 3.2rem);
  letter-spacing: -0.036em;
  line-height: 1.05;
}

/* CTA arrow — the trail slides out on hover/focus so the big text
   reads unmistakably as a link, not a heading. */
.contact__cta { display: inline-flex; align-items: center; gap: clamp(14px, 2vw, 22px); }

.contact__arrow { display: block; flex: none; color: var(--mist); transition: transform 300ms var(--ease), color 300ms var(--ease); }
.contact__arrow svg { display: block; width: clamp(22px, 3vw, 34px); height: clamp(22px, 3vw, 34px); }

.contact__cta:hover .contact__arrow,
.contact__cta:focus-visible .contact__arrow {
  color: var(--paper);
  transform: translateX(10px);
}

.contact__arrow--back svg { transform: scaleX(-1); }
.contact__cta:hover .contact__arrow--back,
.contact__cta:focus-visible .contact__arrow--back {
  transform: translateX(-10px);
}

/* Case study ---------------------------------------------------- */

/* Case studies reuse the About page's .row grid, but their section labels
   are real <h2>s so a long page has an outline to navigate. Resetting the
   margin on the heading only leaves About's decorative <p> labels alone. */
h2.row__label { margin: 0; }

/* Sub-heads inside a section body. Deliberately quiet — the mono label in
   the left column is the section marker; this only has to separate two
   ideas inside one section, not compete with it. */
.row__body h3 {
  margin: clamp(30px, 3.4vw, 42px) 0 12px;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.012em;
}
.row__body > h3:first-child { margin-top: 0; }

/* Bullet list. .stack's hairline rules read as a table of records — right
   for About's job history, wrong for a list of points, where they chop the
   section into slabs. These get a marker instead: the site's own mark
   shrunk to a 6px ring, sat on the first line's x-height rather than the
   top of the line box so it reads level with the text. */
.bullets { list-style: none; margin: 0; padding: 0; }

.bullets li {
  position: relative;
  margin-bottom: 12px;
  padding-left: 22px;
}
.bullets li:last-child { margin-bottom: 0; }

.bullets li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.58em;
  width: 6px;
  height: 6px;
  border: 1px solid var(--mist);
  border-radius: 50%;
}

/* Prose that continues after a list — the list carries no bottom margin
   of its own, so without this it butts straight into the last item. */
.row__body .bullets + p { margin-top: 1em; }

/* Prose resuming after a diagram. Every component in the set below leads
   with a top margin and carries none at the bottom — correct, because each
   is introduced by the text above it. But when prose picks up again
   underneath, nothing holds it off, and it reads as a caption on the
   diagram rather than as the next thought. This was found twice as a
   one-off (.bullets, then .annot) before it was worth stating once for the
   whole set. 1em is deliberately not enough here: these are blocks, not
   list items, and they need a real pause on both sides.

   Specificity is doing real work — `.pair__text > p` and `.row__body > p`
   both set margin-top to 0 and both outrank a plain descendant selector, so
   the set sits in :where() (weightless) and the leading class is doubled to
   clear them. */
.rows--case .row__body
  :where(.matrix, .chain, .trail, .converge, .compare, .swap, .scatter, .shells, .annot)
  + :is(p, blockquote) { margin-top: clamp(24px, 2.8vw, 34px); }

/* The last section's rule and .contact's top border land within a screen
   of each other and read as a double line. The section rules are what
   structure the page, so .contact's is the one that stays. */
.rows--case .row:last-child,
.rows .row:last-child { border-bottom: 0; }

/* Pull quote. No quote glyphs in the markup — the rule and the step up in
   size already mark it, and real quotation marks would hang into the
   gutter or sit crooked against the border. Same size as .page__lede. */
.pull {
  margin: clamp(30px, 3.4vw, 42px) 0;
  padding-left: clamp(18px, 2.4vw, 28px);
  border-left: 1px solid var(--hair-strong);
  font-size: clamp(1.05rem, 1.5vw, 1.32rem);
  line-height: 1.45;
  letter-spacing: -0.012em;
  color: var(--paper);
}
.pull p { margin: 0; }

/* A pull quote stepped back down to body size. Both case studies run a
   spine of .statement lines that carry the argument when the page is
   scanned; a pull quote at .page__lede size sits close enough to those to
   read as one more of them. The left rule stays, so the line is still
   marked as an aside — it just stops competing for the eye.

   A modifier rather than a change to .pull itself, so the plain .pull
   remains available to a page that has no spine to compete with. */
.pull--quiet {
  font-size: 1rem;
  line-height: 1.55;
  letter-spacing: -0.005em;
}

.figure { margin: clamp(30px, 3.6vw, 44px) 0 0; }
/* Video sits here alongside img because a figure holding a UI clip should
   be indistinguishable from one holding a screenshot — same width, same
   corner, same place in the rhythm. Playback is the only difference, and
   that is handled in site.js. */
.figure img,
.figure video { display: block; width: 100%; height: auto; border-radius: 10px; }
/* Figure captions. Deliberately not .mono: that is a label treatment — 11px,
   uppercase, 0.16em of tracking — which is right for "01 · THE CHALLENGE" and
   was fine on the placeholders, but a caption is a sentence somebody is meant
   to actually read, and tracked-out uppercase fights that at any length past
   a few words. So this uses the same quiet register the page already gives
   secondary prose everywhere else: --mist against --ink, one step down from
   body, capped at a caption-length measure so it never runs the full width of
   the artwork above it. */
.figure figcaption {
  margin-top: 14px;
  max-width: 60ch;
  font-size: 0.94rem;
  line-height: 1.5;
  letter-spacing: -0.005em;
  color: var(--mist);
}

/* Slot for artwork that hasn't been made yet. Dashed rather than solid so
   it never reads as a finished frame. To fill one: swap the inner <p> for
   an <img> and drop the --empty modifier — nothing else changes. */
.figure--empty {
  display: grid;
  place-items: center;
  min-height: clamp(150px, 20vw, 240px);
  padding: 24px;
  border: 1px dashed var(--hair);
  border-radius: 10px;
  text-align: center;
}
.figure--empty p { margin: 0; max-width: 46ch; }

/* Case study — diagram set ---------------------------------------- */

/* The research-practice page argues that scattered work was turned into a
   system, and a page made only of paragraphs cannot argue that. So the
   diagrams below are drawn in HTML and CSS rather than shipped as flat
   artwork: the labels stay real text (selectable, translatable, legible at
   320px), the rules stay hairlines at any zoom, and none of it needs an
   asset pipeline or a second render for dark mode.

   All of it is achromatic. The only hue on the site still lives in the
   field; emphasis here is carried by weight and by --paper against --mist.
   --smoke is deliberately absent — it measures ~3:1 on black, which is
   under the floor for the small mono labels these components are full of. */

/* A section that needs the full content width for a diagram. The label
   moves above the body instead of beside it, and prose inside stays capped
   at the same measure as every other row so only the artwork goes wide.
   Below 900px every row is single-column anyway, so the two converge. */
.rows--case .row--wide { grid-template-columns: 1fr; gap: clamp(16px, 2.2vw, 26px); }
.rows--case .row--wide .row__label { padding-top: 0; }
.rows--case .row--wide .row__body { max-width: none; }
/* Prose in a wide row keeps the same measure as every other row, so only
   the artwork actually goes wide. Wrapped in :where() so the rule carries
   no specificity: `ch` resolves against the element's own font size, and at
   40px .statement would read this as 1564px and swallow its own 26ch cap.
   A component that sets its own measure now simply keeps it. */
:where(.row--wide .row__body) > :where(p, h3, .bullets) { max-width: 68ch; }
:where(.row--wide .row__body) > :where(.pull) { max-width: 52ch; }

/* ---- Text beside the diagram that explains it -------------------

   Before this, prose ran at 68ch and diagrams at the full 1320px content
   width. That left ~730px of void beside every paragraph and made every
   diagram 2.2× the width of the text explaining it — which inverts the
   hierarchy, since size is the first thing that tells a reader where to
   look. Equal columns put the diagram and its explanation at the same
   weight and close the gap.

   The columns only exist where both halves stay usable (see the
   min-width block below). Until then .pair is ordinary flow, so the
   stacked reading order is whatever the markup says. */

.pair > * + * { margin-top: clamp(26px, 3.4vw, 40px); }

/* Two paired blocks in one section — the AI practice page runs a second
   pair where the first one's diagram has finished making its point. Both
   columns zero their own first child's top margin (see below), so without
   this the second pair's opening sub-head butts straight into the bottom
   of the diagram above it. Wider than the gap inside a pair: this is a
   change of subject, not a change of column. */
.pair + .pair { margin-top: clamp(38px, 4.4vw, 60px); }

/* .row__body's own child combinators stop reaching prose once it moves a
   level down, so paragraph rhythm is restored here. Descendant rules
   (.row__body h3, .pull, .statement) still apply and are left alone. */
.pair__text > p { margin: 0 0 1em; }
.pair__text > p:last-child { margin-bottom: 0; }

/* The rule above is `.pair__text > p` (0,1,1), which outranks `.statement`
   (0,1,0) — so the narrative statements were silently taking a plain 1em
   paragraph gap instead of the pause they exist to create. Restored here
   with enough specificity to win. Top margin stays 0: all four open their
   column, and the column top is already the pause. */
.pair__text > p.statement { margin: 0 0 clamp(34px, 3.6vw, 52px); }

/* Nearly every component leads with a top margin meant to separate it
   from prose above. At the top of a column there is nothing above it. */
.pair__text > :first-child,
.pair__art > :first-child { margin-top: 0; }

@media (min-width: 1001px) {
  .pair {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: clamp(30px, 3.2vw, 54px);
    align-items: start;
  }
  .pair > * + * { margin-top: 0; }

  /* Sections where the outline wants the picture before the explanation
     (01 — see the mess, then read why it was a mess) keep the art first
     in the markup, so it still leads when the columns stack. Placement
     is explicit here so it lands on the right regardless. */
  .pair--lead-art > .pair__art  { grid-column: 2; grid-row: 1; }
  .pair--lead-art > .pair__text { grid-column: 1; grid-row: 1; }

  /* Columns top-align by default, which is right when the diagram is the
     thing the opening sentence is pointing at. Where one side is much
     shorter than the other, that leaves a block of void under it and the
     dead space simply moves rather than goes — so those pairs centre
     instead. Centring is a no-op on the taller column, which sets the row
     height, so this only ever moves the short one. */
  .pair--center { align-items: center; }

  /* A dashed placeholder has no intrinsic height to balance against the
     column beside it. The Confluence screenshot it stands in for is tall. */
  .pair__art .figure--empty { min-height: clamp(240px, 30vw, 420px); }

  /* The text track is a little wider than 68ch; the prose is not. Keeps
     the measure identical to every other page. :where() again so
     .statement and .swap keep the measures they set for themselves. */
  :where(.pair__text) > :where(p, h3, .bullets, ul, ol, dl) { max-width: 68ch; }
  :where(.pair__text) > :where(.pull) { max-width: 52ch; }

  /* Diagrams drawn for a full-width row need their counts revisited at
     half that width. Fixed counts, not auto-fit: at ~630px auto-fit picks
     whatever fits and orphans the last item onto a row of its own. */
  .pair__art .trail   { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .pair__art .compare { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pair__art .matrix  { grid-template-columns: minmax(0, 20ch) minmax(0, 1fr); }

  /* Annotations belong in a numbered column down the side of the
     screenshot they annotate, not in a 2×2 block under it. */
  .pair__text .annot { grid-template-columns: 1fr; gap: clamp(18px, 2vw, 24px); }

  /* A figure that is evidence for the column beside it rather than the
     argument itself. The Confluence template is a tall portrait screenshot;
     at full column width it stood as high as the annotations explaining it
     and read as the section's headline instead of its footnote. Capped so
     the prose leads. Only inside a paired column — once the layout stacks,
     a dense screenshot needs every pixel it can get to stay legible. */
  .pair__art .figure--inset { max-width: 68%; }

  /* A pair is only a pair where the row is wide enough to hold two readable
     columns. .pair grids on a viewport query, not a container one, so inside
     a narrow row — whose body is capped at the 68ch measure — it would still
     split into two ~265px columns and wreck both halves. Narrow rows keep the
     markup and the reading order, and simply stack. This is what lets a
     section switch between the two rhythms by adding or dropping one class. */
  .row:not(.row--wide) .pair { display: block; }
  .row:not(.row--wide) .pair > * + * { margin-top: clamp(26px, 3.4vw, 40px); }
  .row:not(.row--wide) .pair__art .figure--inset { max-width: 100%; }
}

/* Credit line under the hero, on both case studies. The opening is meant to
   breathe, so the housekeeping — role, team, timeline, status — sits inline
   here as a short dl rather than taking a labelled row in the case grid,
   and section 01 opens on the argument instead. Flex-wrapped, so the field
   count can differ per page without either one needing its own rule. */
.meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px clamp(24px, 4vw, 54px);
  margin: clamp(24px, 3vw, 34px) 0 0;
}
.meta-row dt { margin: 0 0 6px; }
.meta-row dd { margin: 0; max-width: 34ch; }

/* One-line arguments the page turns on. Sized between .page__lede and
   .contact__line — big enough to stop a scroll, small enough that it never
   competes with the headline three screens up. */
.statement {
  margin: clamp(34px, 4vw, 52px) 0;
  max-width: 26ch;
  font-size: clamp(1.5rem, 3.4vw, 2.5rem);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.032em;
  color: var(--paper);
}
.statement em { font-style: normal; color: var(--mist); }

/* Same specificity problem as `.pair__text > p.statement` above, in the
   other place a statement lives: `.row__body > p` (0,1,1) outranks
   `.statement` (0,1,0), so on Benchmarks AI — where the spine sits in
   plain rows rather than paired columns — the four statements were taking
   a 1em paragraph gap instead of the pause they exist to create.

   First-child gets no top margin: every statement opens its section, and
   the row's own padding is already the pause. Without this the statement
   would also drop out of alignment with the mono label beside it. */
.row__body > p.statement { margin: clamp(34px, 4vw, 52px) 0; }
.row__body > p.statement:first-child { margin-top: 0; }

/* 01 — the inherited mess. Three columns of artifacts drop into a shared
   rail and then into a node full of question marks. Dashed throughout,
   because none of these were connections anyone designed. */
.scatter { margin: clamp(30px, 3.6vw, 44px) 0 0; }

.scatter__cols {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(12px, 2vw, 24px);
}

.scatter__col { position: relative; min-width: 0; padding-bottom: 34px; }
.scatter__col::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 0;
  height: 30px;
  border-left: 1px dashed var(--hair-strong);
}

.scatter__col > p { margin: 0 0 14px; }

.scatter__col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.scatter__col li {
  padding: 9px 12px;
  border: 1px solid var(--hair);
  border-radius: 6px;
  background: rgba(244, 244, 242, 0.02);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.01em;
  color: var(--mist);
  overflow-wrap: anywhere;
}

/* Half a degree of tilt each — enough that the column reads as a pile
   rather than a list, not enough that anything looks broken. */
.scatter__col li:nth-child(3n+1) { transform: rotate(-0.5deg); }
.scatter__col li:nth-child(3n+2) { transform: rotate(0.45deg); }
.scatter__col li:nth-child(3n+3) { transform: rotate(-0.22deg); }

/* The rail runs the full width rather than stopping at the outer column
   centres: gaps make those centres a few px off a clean 1/6 and 5/6, and a
   dashed line that misses its drop by 8px just looks like a mistake. */
.scatter__funnel { position: relative; height: clamp(40px, 5vw, 56px); }
.scatter__funnel::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  border-top: 1px dashed var(--hair-strong);
}
.scatter__funnel::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  border-left: 1px dashed var(--hair-strong);
}

.scatter__sink {
  width: min(100%, 40ch);
  margin: 0 auto;
  padding: clamp(18px, 2.2vw, 26px);
  border: 1px dashed var(--hair-strong);
  border-radius: 10px;
  text-align: center;
}
.scatter__sink p { margin: 0; }
.scatter__q {
  font-family: var(--mono);
  font-size: clamp(15px, 2vw, 20px);
  letter-spacing: 0.42em;
  color: var(--mist);
}
.scatter__sink .mono { margin-top: 12px; }

/* 02 — the discovery sequence. One row of cells divided by the grid gap
   showing through as hairlines, so there is no border to double up. */
.trail {
  list-style: none;
  margin: clamp(30px, 3.6vw, 44px) 0 0;
  padding: 0;
  display: grid;
  /* Fixed counts rather than auto-fit. There are exactly six steps, and
     auto-fit picks whatever fits — which is 5 at 768px, orphaning the last
     one on its own row. 3 and 2 both divide 6, so the sequence never ends
     on a short row at any width. Three across also suits the half-width
     column this now lives in far better than six would. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--hair);
  border: 1px solid var(--hair);
  border-radius: 10px;
  overflow: hidden;
}
.trail li { background: var(--ink); padding: clamp(16px, 2vw, 22px) clamp(14px, 1.6vw, 18px); }
.trail__n { display: block; margin-bottom: 10px; }
.trail__t { display: block; letter-spacing: -0.012em; }

/* 04 — the five stages, and the return that makes them a loop. The return
   is a rail rather than a curve: a rail stays exact at any width, and a
   curve would need an SVG that distorts the moment the box is not square. */
.loop {
  /* Lives on the parent so the return rail can solve for it too. */
  --g: clamp(10px, 1.4vw, 18px);
  /* The bottom margin is not decoration. At full width .loop__label is
     absolutely positioned and translated to straddle the rail, so it hangs
     ~8px below .loop's own box — with no margin the label collided with
     the first line of the paragraph underneath. This clears the overhang
     and then leaves a real gap. */
  margin: clamp(34px, 4vw, 50px) 0 clamp(48px, 5vw, 68px);
}

.loop__stages {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--g);
}

.loop__stage {
  position: relative;
  min-width: 0;
  padding: clamp(18px, 2vw, 24px) clamp(14px, 1.4vw, 18px);
  border: 1px solid var(--hair);
  border-radius: 10px;
}
.loop__stage p { margin: 0; color: var(--mist); }
.loop__name {
  margin: 0 0 10px;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.014em;
  color: var(--paper);
}
.loop__n { display: block; margin-bottom: 12px; }

/* Connector and arrowhead sitting in the gap to the left of every stage
   after the first. */
.loop__stage + .loop__stage::before {
  content: '';
  position: absolute;
  top: 50%;
  left: calc(-1px - var(--g));
  width: var(--g);
  border-top: 1px solid var(--hair-strong);
}
.loop__stage + .loop__stage::after {
  content: '';
  position: absolute;
  top: 50%;
  left: calc(1px - var(--g));
  width: 5px;
  height: 5px;
  border-top: 1px solid var(--hair-strong);
  border-right: 1px solid var(--hair-strong);
  transform: translateY(-50%) rotate(45deg);
}

.loop__return {
  position: relative;
  height: clamp(44px, 5vw, 60px);
  /* The arms land exactly on the first and last card's centre. For five
     equal columns of width c separated by gap g, c/2 = W/10 − 0.4g — so
     this stays on centre at every width instead of drifting a few px the
     way a flat 10% inset does. */
  margin: 0 calc(10% - 0.4 * var(--g));
  border: 1px solid var(--hair-strong);
  border-top: 0;
  border-radius: 0 0 14px 14px;
}
/* Arrowhead at the top of the left arm, pointing back up into Discovery. */
.loop__return::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 6px;
  height: 6px;
  border-top: 1px solid var(--hair-strong);
  border-left: 1px solid var(--hair-strong);
  transform: translate(-3.5px, -0.5px) rotate(45deg);
}
/* Sits on the rail and punches a hole in it. */
.loop__label {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translate(-50%, 50%);
  margin: 0;
  padding: 0 14px;
  background: var(--ink);
}

/* 05 — annotations under a screenshot. Numbered so a caption can point at
   a region of the image without an overlay that breaks on resize. */
.annot {
  list-style: none;
  margin: clamp(24px, 2.8vw, 34px) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: clamp(20px, 2.6vw, 34px);
}
.annot__n { display: block; margin-bottom: 12px; }
.annot__t { display: block; margin-bottom: 6px; font-weight: 600; letter-spacing: -0.012em; }
.annot p { margin: 0; color: var(--mist); }

/* 06 — need on the left, capability on the right. A description list
   rather than a table: it is a set of definitions, not a data set. */
.matrix {
  display: grid;
  grid-template-columns: minmax(0, 28ch) minmax(0, 1fr);
  margin: clamp(30px, 3.6vw, 44px) 0 0;
  border-top: 1px solid var(--hair);
}
.matrix dt,
.matrix dd {
  margin: 0;
  padding: clamp(13px, 1.7vw, 18px) 0;
  border-bottom: 1px solid var(--hair);
}
.matrix dt { padding-right: clamp(14px, 3vw, 40px); color: var(--mist); }

/* 07 — a sequence read as one continuous thread. The connector runs
   down the left rather than the centre so it reads as a thread rather than
   a set of boxes that happen to be stacked. */
.chain { list-style: none; margin: clamp(30px, 3.6vw, 44px) 0 0; padding: 0; }
.chain li {
  position: relative;
  padding: clamp(11px, 1.4vw, 14px) clamp(16px, 2vw, 22px);
  border: 1px solid var(--hair);
  border-radius: 8px;
  letter-spacing: -0.012em;
}
.chain li + li { margin-top: 26px; }
.chain li + li::before {
  content: '';
  position: absolute;
  left: 30px;
  top: -27px;
  height: 27px;
  border-left: 1px solid var(--hair-strong);
}
.chain li + li::after {
  content: '';
  position: absolute;
  left: 27px;
  top: -9px;
  width: 5px;
  height: 5px;
  border-right: 1px solid var(--hair-strong);
  border-bottom: 1px solid var(--hair-strong);
  transform: rotate(45deg);
}
/* The last link is where the evidence lands, so it is the one that is lit. */
.chain li:last-child { border-color: var(--hair-strong); color: var(--paper); }
.chain--tight li + li { margin-top: 20px; }
.chain--tight li + li::before { top: -21px; height: 21px; }
.chain--tight li + li::after { top: -6px; }

/* 08 — many studies, one synthesis. The brace is three stubs meeting a
   spine, and the spine has to start and finish exactly on the first and
   last node's centre. For three equal nodes of height h separated by gap
   g, h/2 = H/6 − g/3, which is what the inset below solves for. A flat
   16.6% lands ~7px short at either end and reads as a drawing error. */
.converge {
  --brace: clamp(44px, 6vw, 84px);
  --g: clamp(14px, 2vw, 22px);
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--brace) minmax(0, 1fr);
  align-items: center;
  margin: clamp(30px, 3.6vw, 44px) 0 0;
}

.converge__in { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--g); }
.converge__in li {
  position: relative;
  padding: clamp(12px, 1.5vw, 15px) clamp(14px, 1.8vw, 18px);
  border: 1px solid var(--hair);
  border-radius: 8px;
  color: var(--mist);
  letter-spacing: -0.012em;
}
.converge__in li::after {
  content: '';
  position: absolute;
  left: 100%;
  top: 50%;
  width: calc(var(--brace) / 2);
  border-top: 1px solid var(--hair-strong);
}

.converge__brace { position: relative; align-self: stretch; }
.converge__brace::before {
  content: '';
  position: absolute;
  left: 50%;
  top: calc(16.667% - var(--g) / 3);
  bottom: calc(16.667% - var(--g) / 3);
  border-left: 1px solid var(--hair-strong);
}
.converge__brace::after {
  content: '';
  position: absolute;
  left: 50%;
  right: 0;
  top: 50%;
  border-top: 1px solid var(--hair-strong);
}

.converge__out {
  padding: clamp(16px, 2vw, 22px) clamp(16px, 2vw, 22px);
  border: 1px solid var(--hair-strong);
  border-radius: 10px;
}
.converge__out p { margin: 0; color: var(--mist); }
.converge__out .loop__name { margin-bottom: 8px; }

/* The question the repository could answer, over the one it can answer
   now. The first is dimmed rather than struck out — it was never a bad
   question, it was just the only one available. */
.swap { margin: clamp(30px, 3.6vw, 44px) 0 0; }
.swap p {
  margin: 0;
  max-width: 36ch;
  /* Was 1.6rem, which made this the second-largest editorial voice on the
     page and close enough to the .statement spine to read as a fifth one
     while scanning. It sits in the text column, not the art column, so it
     is prose rather than diagram typography. Stepped down to the lede
     tier: the device here is the dim-to-bright contrast between the two
     questions, not the size, and that survives intact. */
  font-size: clamp(1.05rem, 1.4vw, 1.32rem);
  line-height: 1.34;
  letter-spacing: -0.016em;
}
.swap p + p { margin-top: 18px; }
.swap__was { color: var(--mist); }
.swap__now { color: var(--paper); }

/* 09 — before and after, side by side. No dashboard, no invented numbers:
   the claim is that the shape of the work changed, so the evidence is a
   list of things that are true now and were not before. */
.compare {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(250px, 100%), 1fr));
  gap: clamp(20px, 3vw, 48px);
  margin: clamp(30px, 3.6vw, 44px) 0 0;
}
.compare__col > p { margin: 0 0 16px; }
.compare ul { list-style: none; margin: 0; padding: 0; }
.compare li { padding: 12px 0; border-top: 1px solid var(--hair); }
.compare li:first-child { border-top: 0; padding-top: 0; }
.compare--was li { color: var(--mist); }
.compare--now li { color: var(--paper); }

/* AI practice — the Custom GPT's three layers. Nested boxes rather than a
   numbered list: the whole argument for that build was that the evidence
   sat *inside* two rings of instruction, and a list flattens the nesting
   back out. The outer ring is dashed because it is the defensive layer —
   it describes what the persona refuses, not what it knows. Everything
   else is borrowed from the diagram set above. */
.shells {
  margin: clamp(30px, 3.6vw, 44px) 0 0;
  padding: clamp(14px, 1.8vw, 20px);
  border: 1px dashed var(--hair-strong);
  border-radius: 14px;
}
.shells__layer {
  padding: clamp(14px, 1.8vw, 20px);
  border: 1px solid var(--hair);
  border-radius: 10px;
}
/* The centre is the only part backed by research, so it is the only part lit. */
.shells__layer--core { border-color: var(--hair-strong); }
.shells p { margin: 0; letter-spacing: -0.012em; color: var(--mist); }
.shells__head { margin: 0 0 10px; color: var(--paper); }
.shells p + .shells__layer { margin-top: clamp(14px, 1.8vw, 20px); }

/* Housekeeping line at the foot of a case study — what the screenshots are,
   who is not in them. It has to be present and findable without ever being
   read before the work, so it takes .mono's fine-print voice and sits in the
   gap between the last section and the contact block rather than inside
   either one.

   No max-width on purpose. A `ch` cap under-measures here — .mono adds
   0.16em of tracking on top of the glyph, so 70ch reserves ~460px for a
   line that actually sets ~570px and the note broke onto a second line at
   full width. The text is short enough to hold one line unaided, and still
   wraps on a phone, which is where wrapping is correct. */
.page-note { margin: clamp(30px, 3.6vw, 44px) 0 0; }

/* Contact form ---------------------------------------------------- */

.form-status {
  margin: clamp(40px, 6vw, 80px) 0 0;
  padding: 16px 20px;
  border: 1px solid var(--hair-strong);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--paper);
}

.form-status[data-status="error"] { border-color: var(--amber); color: var(--amber); }

.contact-form {
  margin-top: clamp(40px, 6vw, 80px);
  max-width: 640px;
}

.form-field {
  margin-bottom: clamp(24px, 3vw, 34px);
}

.form-field label {
  display: block;
  margin-bottom: 10px;
}

.form-field input,
.form-field textarea {
  display: block;
  width: 100%;
  margin: 0;
  padding: 12px 0;
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--hair-strong);
  border-radius: 0;
  color: var(--paper);
  font-family: var(--display);
  font-size: 1rem;
  letter-spacing: -0.005em;
  line-height: 1.5;
  transition: border-color 300ms var(--ease);
}

.form-field textarea { resize: vertical; min-height: 9em; }

.form-field input::placeholder,
.form-field textarea::placeholder { color: var(--smoke); }

.form-field input:focus,
.form-field textarea:focus {
  outline: none;
  border-color: var(--paper);
}

.form-field:focus-within label { color: var(--paper); }

.form-honeypot {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.form-submit {
  margin-top: clamp(8px, 1.4vw, 16px);
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 30px;
  background: var(--paper);
  color: var(--ink);
  border: none;
  border-radius: 0;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: opacity 300ms var(--ease);
}

.form-submit:hover,
.form-submit:focus-visible { opacity: 0.8; }

/* ------------------------------------------------------------------
   Entrance
   ------------------------------------------------------------------ */

.reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 900ms var(--ease), transform 900ms var(--ease);
  transition-delay: var(--delay, 0ms);
  /* Failsafe. Everything on every page is a .reveal, and all of it starts at
     opacity 0 waiting for .is-in from site.js — so a blocked, failed or
     throwing script leaves a blank black page rather than a degraded one.
     This brings the content in on its own if that class never arrives. When
     the script is healthy .is-in lands within a frame and cancels it below,
     so this path is never seen. Scripts off entirely is handled earlier, by
     the <noscript> override in the head, which fires with no delay at all. */
  animation: reveal-failsafe 400ms var(--ease) 2s forwards;
}
.is-in .reveal { opacity: 1; transform: none; animation: none; }

@keyframes reveal-failsafe { to { opacity: 1; transform: none; } }

/* ------------------------------------------------------------------
   Responsive
   ------------------------------------------------------------------ */

/* The framework loop stacks earlier than everything else. Five columns in
   a 900px window leaves each card ~165px wide and five lines tall, which
   reads as a cramped column rather than a cycle. Below 1040px the row
   becomes a stack: the connectors turn to point down, and the return rail
   becomes a rule, since a bracket wrapping a screen-tall stack stops
   reading as a return at all. */
@media (max-width: 1040px) {
  .loop__stages { grid-template-columns: 1fr; }
  .loop__stage + .loop__stage::before {
    top: calc(-1px - var(--g));
    left: 50%;
    width: 0;
    height: var(--g);
    border-top: 0;
    border-left: 1px solid var(--hair-strong);
  }
  .loop__stage + .loop__stage::after {
    top: calc(1px - var(--g));
    left: 50%;
    transform: translateX(-50%) rotate(135deg);
  }
  .loop__return {
    height: auto;
    margin: 20px 0 0;
    padding-top: 16px;
    border: 0;
    border-top: 1px dashed var(--hair-strong);
    border-radius: 0;
  }
  .loop__return::before { display: none; }
  .loop__label { position: static; transform: none; padding: 0; background: none; }
}

/* The same stacked loop, reached the other way. The rules above are a
   viewport query, and a viewport query stopped describing this component the
   moment its row went narrow: a narrow row's body is the 68ch measure —
   about 590px — however wide the window is, so at 1440px the five stages
   were still trying to hold five columns of 103px each. Restated against the
   row. Duplication rather than a container query, because converting one
   component to @container while every neighbouring diagram still asks about
   the viewport would leave two models in the same stylesheet. */
.row:not(.row--wide) .loop__stages { grid-template-columns: 1fr; }
.row:not(.row--wide) .loop__stage + .loop__stage::before {
  top: calc(-1px - var(--g));
  left: 50%;
  width: 0;
  height: var(--g);
  border-top: 0;
  border-left: 1px solid var(--hair-strong);
}
.row:not(.row--wide) .loop__stage + .loop__stage::after {
  top: calc(1px - var(--g));
  left: 50%;
  transform: translateX(-50%) rotate(135deg);
}
.row:not(.row--wide) .loop__return {
  height: auto;
  margin: 20px 0 0;
  padding-top: 16px;
  border: 0;
  border-top: 1px dashed var(--hair-strong);
  border-radius: 0;
}
.row:not(.row--wide) .loop__return::before { display: none; }
.row:not(.row--wide) .loop__label { position: static; transform: none; padding: 0; background: none; }

@media (max-width: 900px) {
  /* The 92px year column reads as wasted margin at this width — so the
     card becomes a single stack and .entry__body (a plain wrapper, no
     styles of its own) is dissolved with display:contents, letting the
     title/role/tags reorder freely alongside the year and note as if
     they were all direct children of the card. */
  .entry { grid-template-columns: 1fr; }
  .entry__body { display: contents; }
  .entry__name { order: 1; }
  .entry__year { order: 2; margin-top: 10px; padding-top: 0; }
  .entry__role { order: 3; }
  .entry__tags { order: 4; }
  .entry__note { order: 5; grid-column: auto; padding-top: 0; padding-right: 0; margin-top: 14px; color: var(--mist); }
  .row { grid-template-columns: 1fr; }
  .row__label { padding-top: 0; }

  /* Case-study diagrams. Every one of them is a row on desktop and a stack
     here; what changes is which edge the connectors run along. Where a
     connector would have to be redrawn rather than reoriented, it is
     dropped and the label carries the meaning on its own. */

}

/* Diagram internals hold their multi-column form well below the point
   where the page itself reflows. A tablet at 768px has ~684px of body
   once the columns stack — plenty for three branches or three artifact
   piles — so collapsing these at 900px turned tablets into a very long
   phone. They go single-column only when a column genuinely stops fitting. */
@media (max-width: 640px) {
  .scatter__cols { grid-template-columns: 1fr; gap: clamp(18px, 4vw, 26px); }
  .scatter__col { padding-bottom: 0; }
  .scatter__col::after { display: none; }
  .scatter__funnel { height: 40px; }
  .scatter__funnel::before { display: none; }

  .matrix { grid-template-columns: 1fr; }
  .matrix dt {
    padding: 18px 0 0;
    border-bottom: 0;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
  }
  .matrix dd { padding-top: 7px; }

  .converge { grid-template-columns: 1fr; }
  .converge__in li::after { display: none; }
  .converge__brace { height: 38px; }
  .converge__brace::before { top: 0; bottom: 0; }
  .converge__brace::after { display: none; }
}

@media (max-width: 560px) {
  :root { --bar-h: 54px; }

  .trail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .masthead { flex-direction: column; align-items: flex-start; gap: 14px; }

  /* The landing's masthead is free to stack; a fixed bar is not — two rows
     of it would eat a sixth of a phone screen on every interior page. It
     stays one row and the pieces tighten up to fit. */
  .page .masthead {
    flex-direction: row;
    align-items: center;
    gap: 10px;
  }
  .page .masthead__name { font-size: 12px; letter-spacing: 0; }
  .page .masthead__nav { gap: 13px; }
  .page .masthead__nav a { letter-spacing: 0.08em; }

  .to-top { width: 42px; height: 42px; }
  .hero__title { letter-spacing: -0.034em; }
  .colophon { flex-direction: column; align-items: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
  .reveal { opacity: 1; transform: none; }
  /* The field renders one static frame and never repels, so the headlines
     must not depend on a cursor they will never see. */
  .field-ready .hero__title__layer { color: var(--paper); }
  .hero__title__layer--lit { display: none; }
  .field-ready .page__title__layer { color: var(--paper); }
  .page__title__layer--lit { display: none; }
  .cursor-only { display: none; }
}
