:root {
  /* ---- Color -------------------------------------------------------
     Base hues are stored as channel triplets so any alpha variant
     composes as rgb(var(--x-rgb) / 0.22) without minting a new token. */
  --fg-rgb:     33 28 24;   /* warm ink: a brown-black, so the ink belongs to
                               the same family as the cream rather than sitting
                               against it. 15.54:1 on the ground. */
  --bg-rgb:     253 250 246;
  --accent-rgb: 142 26 78;        /* 8.0:1 on the cream */
  --accent-lift-rgb: 179 45 107;  /* the same hue with the lights on, for hover */
  --bloom-rgb:  236 160 196;

  --fg:         rgb(var(--fg-rgb));
  --bg:         rgb(var(--bg-rgb));
  --accent:     rgb(var(--accent-rgb));
  --accent-lift: rgb(var(--accent-lift-rgb));

  /* Derived so they track the foreground automatically. Alpha blends
     toward the ground, so faint must be heavier on light than on dark
     (0.58 only reached ~4.0:1 here) to keep 13px text above 4.5:1. */
  --fg-mid:     rgb(var(--fg-rgb) / 0.86);   /* between full ink and body copy: subsection headings */
  --fg-soft:    rgb(var(--fg-rgb) / 0.78);
  --fg-faint:   rgb(var(--fg-rgb) / 0.68);
  --fg-quiet:   rgb(var(--fg-rgb) / 0.78);   /* case study body copy: the caption grey, under a medium-weight black lede */

  --line:       rgb(var(--accent-rgb) / 0.22);

  /* Things that must stay dark whichever way the page is lit: cast shadows
     and the lightbox backdrop. They borrow the ink here, but on the dark
     theme the ink is cream, so they get their own hooks. */
  --shadow-rgb:      var(--fg-rgb);
  --overlay-rgb:     var(--fg-rgb);   /* lightbox backdrop and buttons */
  --overlay-ink-rgb: var(--bg-rgb);   /* type and rings on that backdrop */

  color-scheme: light;

  /* ---- Type -------------------------------------------------------- */
  --font-display: 'Instrument Serif', Georgia, 'Times New Roman', serif;
  --font-body: 'Instrument Sans', system-ui, sans-serif;

  /* Only these two weights exist. Instrument Serif ships a single
     400 cut, so display type gets hierarchy from SIZE, not weight.
     Adding a heavier token here would only fake-bold. */
  --fw-regular: 440;
  --fw-medium:  500;

  --fs-micro: 13px;
  --fs-meta:  14px;
  --fs-ui:    15px;
  --fs-body:  16px;
  --fs-lead:  18px;
  --fs-tl:    24px;
  --fs-logo:  30px;
  --fs-menu:  28px;
  --fs-mark:  52px;
  --fs-sub:  clamp(16px, 1.6vw, 19px);
  --fs-lede: clamp(19px, 2.2vw, 23px);
  --fs-h3:   clamp(26px, 3.1vw, 36px);
  --fs-h2:   clamp(32px, 4.6vw, 52px);
  --fs-h1:   clamp(40px, 9vw, 54px);   /* floor above h2's, so the hero never reads smaller than a section title on a phone */
  --fs-hero: clamp(44px, 5.6vw, 68px);   /* the home headline only; two lines, so it can run larger */

  /* Tracking — display type tightens as it grows, labels open up */
  --ls-h1:    -0.02em;
  --ls-h2:    -0.015em;
  --ls-h3:    -0.01em;
  --ls-lede:  -0.005em;
  --ls-body:  -0.012em;   /* Instrument Sans is already compact; only a hair of tracking */
  --ls-label:  0.08em;
  --ls-caps:   0.14em;

  /* Leading */
  --lh-body:    1.65;
  --lh-copy:    1.5;
  --lh-lede:    1.45;
  --lh-heading: 1.15;
  --lh-display: 1.08;

  /* ---- Spacing — 4px grid ------------------------------------------ */
  --sp-1:    4px;
  --sp-2:    8px;
  --sp-3:   12px;
  --sp-4:   16px;
  --sp-5:   20px;
  --sp-6:   24px;
  --sp-7:   32px;
  --sp-8:   40px;
  --sp-9:   48px;
  --sp-10:  56px;
  --sp-11:  64px;
  --sp-12:  80px;
  --sp-13:  96px;
  --sp-14: 120px;

  /* One gap between every homepage section: each section owns only the
     space below it, so the rhythm can't drift section to section. */
  --section-gap: var(--sp-14);

  /* ---- Borders ------------------------------------------------------ */
  --bw-hair:  1px;
  --bw-thin:  1.5px;
  --bw-thick: 2px;

  /* ---- Radii -------------------------------------------------------- */
  --radius-card:   8px;
  --radius-circle: 50%;

  /* ---- Component geometry -------------------------------------------
     Derived where one value depends on another, so the parts stay in
     register if a base size changes. */
  --size-node:   13px;                                        /* timeline dot */
  --size-burger: 22px;                                        /* menu bar width */
  --spine-left:  calc((var(--size-node) - var(--bw-hair)) / 2); /* centers spine on dot */
  --spine-inset: 10px;                                        /* spine top/bottom fade */
  --node-top:     7px;   /* seats the 13px node on the cap centre of a 24px serif title at heading leading: (27.6-31)/2 + 24 - 17.28/2 - 6.5 */
  --burger-shift: calc(var(--bw-thin) + var(--sp-1));         /* bar travel to form the X */

  /* ---- Measure (max line lengths & fixed columns) -------------------- */
  --measure-hero:    560px;
  --measure-blurb:   620px;
  --measure-media:   620px;   /* case study figures; match the reading column */
  --measure-media-wide: 980px; /* dense screenshots that need room to read */
  --measure-contact: 460px;
  --measure-card:    400px;   /* project row text, so it holds a column beside the image instead of floating */
  --measure-opts:    400px;   /* option sketches: small enough to read as diagrams, not screenshots */
  --col-photo:       260px;
  --col-photo-sm:    200px;

  /* ---- Ratios -------------------------------------------------------- */
  --ratio-media:    4 / 3;
  --ratio-portrait: 4 / 5;
  --ratio-wide:     16 / 9;

  /* ---- Motion ------------------------------------------------------- */
  --dur-fast: 0.2s;
  --dur-vortex: 1.05s;   /* one arrow turn: sprints, then crawls the last few degrees */
  --dur-base: 0.3s;
  --dur-slow: 0.6s;
  --dur-slide: 0.45s;   /* carousel step */
  --ease-out:   cubic-bezier(0.22, 1, 0.36, 1);
  --ease-vortex: cubic-bezier(0.32, 0.72, 0, 1);
  --ease-inout: cubic-bezier(0.4, 0, 0.2, 1);
  /* Overshoots a hair, then settles, so a phrase lands rather than stops. */
  --ease-spring: cubic-bezier(0.34, 1.42, 0.64, 1);

  --dur-wheel: 0.62s;   /* phrase slide */
  /* The sheen travels a fixed distance, not a percentage of the phrase. As a
     percentage a short phrase moved a fraction of the distance a long one did
     in the same time, so it looked frozen. */
  --sheen-span: 460px;
  --dur-sheen:  6s;
  --wheel-h:   1.55em;  /* one line, with real room for descenders: the box
                           clips to exactly this, so anything less ate the g */

  /* Travel distances for transforms */
  --lift-md:      6px;   /* project card hover */
  --nudge:        4px;   /* arrow slide, underline offset */
  --vortex-stagger: 0.018s;  /* how far each trailing arrow runs ahead of the next */
  --vortex-fade:    0.28;    /* opacity each trailing arrow drops by */
  --shift-reveal: 24px;  /* scroll-in animation */

  /* ---- Effects ------------------------------------------------------ */
  --blur-nav:      12px;
  --opacity-noise: 0.45;
  --shot-dim:      1;      /* screenshots are white UI; dark pulls them down a touch */

  /* ---- Elevation ---------------------------------------------------- */
  --shadow-mock: 0 1px 2px rgb(var(--shadow-rgb) / 0.05),
                 0 18px 44px -8px rgb(var(--shadow-rgb) / 0.22);
  --shadow-ring: 0 0 0 var(--sp-1) rgb(var(--accent-rgb) / 0.16),
                 0 0 26px 4px rgb(var(--bloom-rgb) / 0.55);

  /* ---- Ambient light ------------------------------------------------
     The bloom is NOT the accent diluted — it's a deliberately lighter,
     more acid green (--bloom-rgb). Sharing a hue family but not a value
     keeps the deep type crisp over the light instead of sinking into it.
     Lime is much lighter than the other hues, so it needs a higher alpha
     to register on cream. One hue for the whole page; rhythm comes from
     position, not hue. */
  --bloom-alpha: 0.16;
  --bloom:       radial-gradient(circle, rgb(var(--bloom-rgb) / var(--bloom-alpha)), transparent 50%);
  --bloom-size:  min(620px, 66%);
  --bloom-blur:  22px;
  --bloom-inset: 6%;

  /* The hero bloom is its own thing. Centred on the section it sat behind a
     single masthead column and read as a highlight on that cell, so here it
     runs bigger, brighter, and off the right edge: light entering the frame
     rather than an orb parked behind a word. */
  --bloom-alpha-hero: 0.20;
  --bloom-hero:       radial-gradient(circle, rgb(var(--bloom-rgb) / var(--bloom-alpha-hero)), transparent 58%);
  --bloom-size-hero:  min(900px, 78%);
  --bloom-hero-x:     8%;
  --bloom-hero-y:     28%;

  /* ---- Layering ----------------------------------------------------- */
  --z-behind:   -1;
  --z-noise:     1;
  --z-content:   2;
  --z-menu:     99;
  --z-nav:     100;
  --z-lightbox: 200;

  /* ---- Surfaces ----------------------------------------------------
     Gradients are scoped to image surfaces only. */
  /* The off-hours phrase, pitched limier and brighter than the accent so it
     reads against the yellow-green bloom behind it. As bright as lime gets
     while still clearing 4.5:1 on the cream. */
  --lime:         #B32D6B;
  --sheen-deep:   #8E1A4E;   /* the darkest point of the drift */
  --sheen-bright: #D9568F;   /* a narrow highlight band, kept narrow on purpose */

  --sand: linear-gradient(140deg, #F3E7E0 0%, #EBDDD6 55%, #E5D5CE 100%);
  --wash: radial-gradient(circle, rgb(var(--bloom-rgb) / 0.20), transparent 68%);
  --rule: linear-gradient(to right, transparent, var(--line) 10%, var(--line) 90%, transparent);

  /* ---- Layout ------------------------------------------------------- */
  /* Laptops keep 1080; bigger screens grow with the window, so the work
     images scale up instead of sitting small in a wide field of cream. */
  --max: clamp(1080px, 82vw, 1440px);
  /* Case study rhythm: one gap between sections, a tighter one where text
     leads straight into its own figure. */
  --case-gap: var(--sp-14);
  --case-gap-inner: var(--sp-11);
  --pad: var(--sp-7);
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html {
  /* No scroll-behavior here on purpose. Smooth scrolling on the document
     swallows the first wheel or trackpad gesture while its own animation
     runs, so the page reads as stuck until you scroll a second time. Anchor
     links animate from script.js instead, where only that one jump is
     smooth. */

  /* The section blooms bleed past the container by design; clip on html too
     or the document itself scrolls sideways (body alone isn't enough). */
  overflow-x: clip;
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-body);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  /* clip, not hidden: hidden turns the body into its own scroll box, which
     drew a second scrollbar beside the window's. */
  overflow-x: clip;
}

/* Line breaks: headings and decks split into even lines instead of one long
   line and a stub; body copy avoids leaving a single word on the last line. */
h1, h2, h3, .case__deck, .project__sub { text-wrap: balance; }
p, li, dd, figcaption { text-wrap: pretty; }

.noise {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: var(--z-noise);
  opacity: var(--opacity-noise);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='0.4'/%3E%3C/svg%3E");
}

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

/* One keyboard focus ring for the whole site, in the accent, so the browser's
   default blue never shows up on a link or button. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ---------- Nav ---------- */

.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* The nav is full-bleed but its contents must line up with the content
     column. Below --max that's just --pad; above it, add the centring
     gutter so the wordmark sits on the same edge as the headline.
     (100% is the viewport here, since .nav is fixed and pinned L/R.) */
  padding: var(--sp-5) max(var(--pad), calc((100% - var(--max)) / 2 + var(--pad)));
  backdrop-filter: blur(var(--blur-nav));
  border-bottom: var(--bw-hair) solid transparent;
  transition: border-color var(--dur-base) ease, padding var(--dur-base) ease;
}

.nav.is-scrolled {
  border-bottom-color: var(--line);
  padding: var(--sp-3) max(var(--pad), calc((100% - var(--max)) / 2 + var(--pad)));
}

.nav__logo {
  font-family: var(--font-display);
  line-height: 1;
  font-weight: var(--fw-regular);
  font-size: var(--fs-logo);
  letter-spacing: var(--ls-h3);
}

/* The mark: the Golden Gate, for San Francisco. Towers, cable and deck in
   the accent; three strokes, since it only gets 30px. */
.nav__logo { display: inline-flex; align-items: center; color: var(--fg); }
.nav__mark { display: block; overflow: visible; }

/* Recessive: the wordmark leads, the links stay quiet until hovered */
.nav__links {
  display: flex;
  gap: var(--sp-7);
  font-size: var(--fs-ui);
  font-weight: var(--fw-regular);
}

.nav__links a {
  position: relative;
  color: var(--fg-faint);
  transition: color var(--dur-fast) ease;
}

.nav__links a::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: calc(-1 * var(--nudge));
  width: 0;
  height: var(--bw-thin);
  background: var(--accent);
  transition: width var(--dur-base) ease;
}

.nav__links a:hover { color: var(--fg); }
.nav__links a:hover::after { width: 100%; }

.nav__toggle {
  display: none;
  flex-direction: column;
  gap: var(--sp-1);
  background: none;
  border: none;
  cursor: pointer;
  padding: var(--sp-1);
}

.nav__toggle span {
  display: block;
  width: var(--size-burger);
  height: var(--bw-thin);
  background: var(--fg);
  transition: transform var(--dur-base) ease, opacity var(--dur-fast) ease;
}

.nav__toggle.is-open span:nth-child(1) { transform: translateY(var(--burger-shift)) rotate(45deg); }
.nav__toggle.is-open span:nth-child(2) { opacity: 0; }
.nav__toggle.is-open span:nth-child(3) { transform: translateY(calc(-1 * var(--burger-shift))) rotate(-45deg); }

.mobile-menu {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-menu);
  background: var(--bg);
  padding: var(--sp-13) var(--pad) var(--sp-7);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  transform: translateY(-100%);
  transition: transform var(--dur-base) var(--ease-inout);
  border-bottom: var(--bw-hair) solid var(--line);
}

.mobile-menu.is-open { transform: translateY(0); }

.mobile-menu a {
  font-family: var(--font-display);
  font-size: var(--fs-menu);
  font-weight: var(--fw-regular);
  padding: var(--sp-3) 0;
  letter-spacing: var(--ls-h3);
}

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

main { position: relative; z-index: var(--z-content); }

section {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 var(--pad);
  scroll-margin-top: var(--sp-14);
}

/* ---------- Hero ---------- */

/* Ambient blooms — read as light sources, not gradients. Each is
   anchored to its own section and centred on it, so they hold position
   at any viewport height instead of drifting like a document-percentage
   background would. They alternate sides for rhythm. */
.hero,
.experience,
.contact { position: relative; }

.hero::before,
.experience::before,
.contact::before {
  content: '';
  position: absolute;
  z-index: var(--z-behind);
  pointer-events: none;
  width: var(--bloom-size);
  aspect-ratio: 1;
  top: 50%;
  transform: translateY(-50%);
  filter: blur(var(--bloom-blur));
}

.hero::before,
.experience::before,
.contact::before { background: var(--bloom); }

.hero::before {
  width: var(--bloom-size-hero);
  background: var(--bloom-hero);
  left: auto;
  right: var(--bloom-hero-x);
  top: var(--bloom-hero-y);
}
.experience::before { right: var(--bloom-inset); }
.contact::before { left:  var(--bloom-inset); }

.hero {
  min-height: 92vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: var(--sp-14);
  padding-bottom: var(--sp-10);
}

/* Phrase wheel: words roll vertically while the underline grows and
   shrinks to hug whichever phrase is showing. Pinning it to the longest
   phrase left a tail hanging off the shorter ones. */
.wheel {
  display: inline-block;
  overflow: hidden;
  height: var(--wheel-h);
  vertical-align: bottom;
  border-bottom: var(--bw-thick) solid var(--accent);
  transition: width var(--dur-wheel) var(--ease-out);
}

.wheel__stack {
  display: block;
  transition: transform var(--dur-wheel) var(--ease-spring);
}

.wheel__item {
  display: block;
  /* max-content, or a block item measures as wide as its container and
     every phrase reports the same width */
  width: max-content;
  height: var(--wheel-h);
  line-height: var(--wheel-h);
  color: var(--accent);
  white-space: nowrap;
}

.hero__title {
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  font-weight: var(--fw-regular);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-h1);
  margin-bottom: var(--sp-7);
}

.accent { color: var(--accent); }

/* Hero headline on two lines: the name in roman, the role in the serif's
   italic and the accent. One typographic contrast does the work a graphic
   would, without adding an object to the page. */
.hero .hero__title { font-size: var(--fs-hero); }
.hero__role {
  display: block;
  font-style: italic;
  color: var(--accent);
}

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

/* Kept for the _*.html hero explorations, which still use it. */
.hero__sub {
  max-width: var(--measure-hero);
  font-size: var(--fs-sub);
  color: var(--fg-soft);
  margin-bottom: var(--sp-8);
}

/* Hero masthead. Same grammar as .case-meta (label over value, one hairline
   above) promoted to the home page, so it reads as part of this site rather
   than a widget bolted on. Labels take the accent, like .section-head__label.

   Values stay in the body face on purpose. Setting them in the display face
   made the hero alternate serif/sans six times in a few inches; keeping the
   serif for the headline alone leaves one display moment instead of four
   competing ones. */
.masthead {
  margin-bottom: var(--sp-8);
  padding-bottom: var(--sp-7);
  border-bottom: var(--bw-hair) solid var(--line);
  display: grid;
  /* Columns take their content's width and the slack is split between them,
     so the gaps between the words are equal. Equal-width columns looked even
     on paper but read lopsided: "San Francisco" is half the length of the
     values either side of it, so it left four times the air after it. */
  grid-template-columns: repeat(3, max-content);
  justify-content: start;
  gap: var(--sp-7) var(--sp-13);
}

.masthead dt {
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--sp-2);
}

.masthead dd {
  font-size: var(--fs-lead);
  line-height: var(--lh-body);
  color: var(--fg);
}

/* No rule underneath: the colour in the letters is the whole signal, and a
   rule as well was one mark too many. --wheel-hug: 0 leaves the width alone,
   so the column holds still while the phrases roll. */
.masthead .wheel {
  --wheel-hug: 0;
  border-bottom: 0;
  vertical-align: baseline;
}

/* The one spot of colour in the masthead: the rest of the row is black, so
   the moving phrase is the only thing lit up. */
.masthead .wheel__item { color: var(--lime); }

/* A gradient drifts through the letters, slowly enough to read as light
   moving over them rather than as an animation. Behind @supports so a
   browser without background-clip: text keeps solid lime instead of
   transparent text. */
@keyframes wheel-sheen { to { background-position: calc(-1 * var(--sheen-span)) 0; } }

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .masthead .wheel__item {
    background-image: linear-gradient(100deg,
      var(--sheen-deep) 0%,
      var(--lime) 28%,
      var(--sheen-bright) 42%,
      var(--lime) 56%,
      var(--sheen-deep) 100%);
    background-size: var(--sheen-span) 100%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    animation: wheel-sheen var(--dur-sheen) linear infinite;
  }
}

@media (prefers-reduced-motion: reduce) {
  .masthead .wheel__item { animation: none; }
}



.hero__actions { display: flex; gap: var(--sp-3); flex-wrap: wrap; }

/* ---------- Section heads ---------- */

.section-head { margin-bottom: var(--sp-10); }

.section-head__label {
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--sp-3);
}

.section-head h2 {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-h2);
  line-height: var(--lh-display);
}

/* ---------- Work / Projects ---------- */

.work { padding-bottom: var(--section-gap); }

.projects { display: grid; gap: var(--sp-14); }

.project {
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  gap: var(--sp-10);
  align-items: center;
}

/* Alternate image side for visual rhythm. The wider track swaps with it, so
   the image is the same size whichever side it lands on. */
.project:nth-child(even) { grid-template-columns: 1fr 1.5fr; }
.project:nth-child(even) .project__media { order: 2; }

.project__media {
  position: relative;
  display: block;
  border-radius: var(--radius-card);
  overflow: hidden;
  transition: transform var(--dur-slow) var(--ease-out);
}

.project__media:hover { transform: translateY(calc(-1 * var(--lift-md))); }

/* Card image, sized to the card's own frame */
/* The client's mark, reversed to one white ink so the cards read as a set.
   Sits in the corner at a fixed height, quiet enough not to fight the shot. */
.project__brand {
  position: absolute;
  top: var(--sp-6);
  right: var(--sp-7);
  z-index: 1;
  width: auto;
  opacity: 0.8;
  pointer-events: none;
  /* matched on letter height, not box height: the SAP file carries the
     trapezoid above and below its type, coinbase's crops to the glyphs */
  height: 15px;
}
.project__brand--wide { height: 10px; }

.project__shot {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--ratio-media);
  object-fit: cover;
  object-position: top;
  filter: brightness(var(--shot-dim));
}

.project__ph {
  position: relative;
  aspect-ratio: var(--ratio-media);
  background: var(--sand);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* Soft green wash so placeholders feel on-brand, not grey boxes */
.project__ph::before {
  content: '';
  position: absolute;
  width: 62%;
  aspect-ratio: 1;
  border-radius: var(--radius-circle);
  background: var(--wash);
  top: -14%;
  right: -10%;
}

.project__ph span {
  position: relative;
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: rgb(var(--fg-rgb) / 0.45);
}

.project__body { max-width: var(--measure-card); }

.project__title {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: var(--fw-regular);
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-h2);
  margin-bottom: var(--sp-4);
}

.project__sub {
  color: var(--fg-soft);
  font-size: var(--fs-lead);
  margin-bottom: var(--sp-6);
}

/* ---------- Metadata rows ----------
   Two or three short items sitting under a project title, set quietly so
   they read as a credit line rather than as buttons. */

.project__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-6);
  margin-bottom: var(--sp-6);
}

.project__tags span {
  padding: var(--sp-2) var(--sp-4);
  border: var(--bw-hair) solid var(--line);
  border-radius: var(--radius-card);
  font-size: var(--fs-meta);
  color: var(--fg-soft);
  white-space: nowrap;
}

/* ---------- Arrow link ----------
   The site's single action component. Hierarchy comes from scale,
   not from a different shape — --lead is the primary (hero) size. */

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-ui);
  font-weight: var(--fw-medium);
  padding-bottom: var(--sp-1);
  border-bottom: var(--bw-thin) solid var(--fg);
  transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

.link-arrow em { font-style: normal; transition: transform var(--dur-fast) ease; }
.link-arrow:hover { color: var(--accent-lift); border-color: var(--accent-lift); }
.link-arrow:hover em { transform: translateX(var(--nudge)); }

/* The arrow rolls away: one turn on a curve that sprints then crawls home,
   travelling right at the same time. The trail is four stacked copies of the
   arrow, each pulled earlier along that curve by a negative delay, so they sit
   at different angles and offsets at any instant and read as motion blur.
   Primary links only; the quiet nav variant keeps the plain slide. */
.link-arrow__icon {
  display: grid;
  place-items: center;
  width: 1em;
  height: 1em;
}

/* line-height: 1 is load-bearing. Inheriting the body's 1.65 made each arrow
   a 29.7px box inside an 18px cell, so it overflowed downward instead of
   centring and the arrow sat low against the text. */
.link-arrow__icon em {
  grid-area: 1 / 1;
  display: block;
  line-height: 1;
  opacity: calc(1 - var(--i) * var(--vortex-fade));
  transition: transform var(--dur-vortex) var(--ease-vortex);
  transition-delay: calc(-1 * var(--vortex-stagger) * var(--i));
}

/* Index set here rather than inline, so the markup stays clean */
.link-arrow__icon em:nth-child(1) { --i: 0; }
.link-arrow__icon em:nth-child(2) { --i: 1; }
.link-arrow__icon em:nth-child(3) { --i: 2; }
.link-arrow__icon em:nth-child(4) { --i: 3; }

.link-arrow:hover .link-arrow__icon em,
.link-arrow:focus-visible .link-arrow__icon em {
  transform: translateX(var(--nudge)) rotate(360deg);
}

@media (prefers-reduced-motion: reduce) {
  .link-arrow__icon em { transition: none; }
  .link-arrow:hover .link-arrow__icon em { transform: none; }
  .link-arrow__icon em:not(:first-child) { display: none; }
}

/* Navigation variant. Same component, stepped down: smaller, body weight, and
   a hairline instead of the full rule. Hierarchy by scale rather than by a
   second shape, so back/next stay findable without competing with the CTA. */
.link-arrow--quiet {
  font-size: var(--fs-meta);
  font-weight: var(--fw-regular);
  color: var(--fg-soft);
  border-bottom: var(--bw-hair) solid var(--line);
}

/* Back/previous links: same component, arrow leads and slides the other way */
.link-arrow--back:hover em { transform: translateX(calc(-1 * var(--nudge))); }

.link-arrow--lead {
  font-size: var(--fs-lead);
  padding-bottom: var(--sp-2);
  border-bottom-width: var(--bw-thick);
}

/* ---------- Experience / Timeline ---------- */

.experience { padding-bottom: var(--section-gap); }

.timeline { list-style: none; position: relative; }

/* The vertical spine. Offsets here are optical — they align to the
   13px node, not to the spacing grid. */
.timeline::before {
  content: '';
  position: absolute;
  left: var(--spine-left);
  top: var(--spine-inset);
  bottom: var(--spine-inset);
  width: var(--bw-hair);
  background: linear-gradient(
    to bottom,
    var(--accent) 0%,
    rgb(var(--accent-rgb) / 0.35) 22%,
    var(--line) 60%,
    transparent 100%
  );
}

.tl-item {
  position: relative;
  padding-left: var(--sp-8);
  padding-bottom: var(--sp-9);
}

.tl-item:last-child { padding-bottom: 0; }

/* The node */
.tl-item::before {
  content: '';
  position: absolute;
  left: 0;
  top: var(--node-top);
  width: var(--size-node);
  height: var(--size-node);
  border-radius: var(--radius-circle);
  background: var(--bg);
  border: var(--bw-thin) solid rgb(var(--fg-rgb) / 0.22);
  transition: border-color var(--dur-base) ease, background var(--dur-base) ease;
}

.tl-item--current::before {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: var(--shadow-ring);
}

.tl-head { margin-bottom: var(--sp-1); }

/* Role and dates share one line and one treatment, so each entry is three
   voices, not four: the serif name, a quiet meta line, the body blurb. */
.tl-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-5);
  margin-bottom: var(--sp-3);
  font-size: var(--fs-meta);
  color: var(--fg-faint);
}

/* The one subsection heading on the site: the timeline entry title, shared
   with h3s inside case sections so a subsection reads the same everywhere. */
.tl-head h3,
.case-section h3 {
  font-family: var(--font-display);
  font-size: var(--fs-tl);
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-h3);
  line-height: var(--lh-heading);
}

.case-section h3 { margin-bottom: var(--sp-4); color: var(--fg-mid); }
/* Case study subheadings sit one clear step under the section heading. */
.case-section h3 { font-size: clamp(22px, 2.3vw, 28px); color: var(--fg); }

.tl-date {
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}

.tl-blurb {
  color: var(--fg-soft);
  font-size: var(--fs-body);
  max-width: var(--measure-blurb);
}

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

.about { padding-bottom: var(--section-gap); }

.about-grid {
  display: grid;
  grid-template-columns: var(--col-photo) 1fr;
  gap: var(--sp-10);
  align-items: start;
}

.about-photo {
  position: sticky;
  top: var(--sp-14);
  aspect-ratio: var(--ratio-portrait);
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--sand);
  display: flex;
  align-items: center;
  justify-content: center;
}

.about-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The section heading is already the display face. A serif lede directly
   under it reads as a second heading, so the lede steps down in size and
   weight instead of changing typeface. */
.about-lede {
  font-size: var(--fs-lede);
  font-weight: var(--fw-regular);
  line-height: var(--lh-lede);
  letter-spacing: var(--ls-body);
  color: var(--fg);
  margin-bottom: var(--sp-6);
}

.about-content > p:not(.about-lede) {
  color: var(--fg-soft);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  margin-bottom: var(--sp-5);
}

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

.contact { padding-bottom: var(--section-gap); text-align: center; }

.contact .section-head { margin-bottom: var(--sp-6); }

.contact__sub {
  max-width: var(--measure-contact);
  margin: 0 auto var(--sp-8);
  color: var(--fg-soft);
  font-size: var(--fs-lead);
}

/* Email and LinkedIn are the same ask, so they get the same weight rather
   than one being a headline and the other a footnote. */
.contact__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-6) var(--sp-10);
}

/* ---------- Footer ---------- */

.footer {
  position: relative;
  z-index: var(--z-content);
  max-width: var(--max);
  margin: 0 auto;
  padding: var(--sp-6) var(--pad) var(--sp-8);
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: var(--fs-meta);
  color: var(--fg-faint);
}

.footer::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--pad);
  right: var(--pad);
  height: var(--bw-hair);
  background: var(--rule);
}

.footer a { transition: color var(--dur-fast) ease; }
.footer a:hover { color: var(--fg); }

/* ---------- Reveal animation ---------- */

.reveal {
  opacity: 0;
  transform: translateY(var(--shift-reveal));
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
}

.reveal.is-visible { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .wheel__stack { transition: none; }
  * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

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

@media (max-width: 900px) {
  /* Three max-content columns overflow between 640 and 900; let the last
     one wrap to its own row instead of running off the edge. */
  .masthead { grid-template-columns: repeat(auto-fit, minmax(220px, max-content)); }
  .project,
  .project:nth-child(even) { grid-template-columns: 1fr; gap: var(--sp-6); }
  .project:nth-child(even) .project__media { order: 0; }
  .projects { gap: var(--sp-13); }
  .about-grid { grid-template-columns: 1fr; gap: var(--sp-7); }
  .about-photo { max-width: var(--col-photo-sm); position: static; }
}

@media (max-width: 640px) {
  :root { --case-gap: var(--sp-12); }
  .masthead { grid-template-columns: 1fr; justify-content: start; }

  :root { --pad: var(--sp-5); }
  .nav__links { display: none; }
  .nav__toggle { display: flex; }
  .hero { min-height: 88vh; padding-top: var(--sp-14); }
  .projects { gap: var(--sp-13); }
  .project__media:hover { transform: none; }
  .tl-head { flex-direction: column; gap: var(--sp-1); }
  .tl-item { padding-left: var(--sp-7); padding-bottom: var(--sp-8); }
  :root { --section-gap: var(--sp-13); }
  .footer { flex-direction: column; gap: var(--sp-3); }
}

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

.case { padding-top: var(--sp-13); padding-bottom: var(--sp-12); }

.case-head {
  position: relative;
  max-width: var(--measure-media);
  margin-bottom: var(--case-gap);
}

/* same light source as the home page, anchored to the header */
.case-head::before {
  content: '';
  position: absolute;
  z-index: var(--z-behind);
  pointer-events: none;
  width: var(--bloom-size);
  aspect-ratio: 1;
  top: 50%;
  left: var(--bloom-inset);
  transform: translateY(-50%);
  background: var(--bloom);
  filter: blur(var(--bloom-blur));
}

.case-head .link-arrow { margin-bottom: var(--sp-9); }

.case__title {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: var(--fw-regular);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-h2);
  margin-bottom: var(--sp-5);
}

.case__deck {
  font-size: var(--fs-lead);
  line-height: var(--lh-copy);
  color: var(--fg-soft);
  max-width: var(--measure-blurb);
}

/* Meta strip — the facts a hiring manager scans for first */
.case-meta {
  display: grid;
  /* Team is the wordy one; give it double width and let the four
     columns wrap to two on narrow screens */
  grid-template-columns: 1fr 2fr 1fr 1fr;
  gap: var(--sp-6);
  margin-top: var(--sp-9);
  padding-top: var(--sp-6);
  border-top: var(--bw-hair) solid var(--line);
}

.case-meta dt {
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--fg-faint);
  margin-bottom: var(--sp-2);
}

.case-meta dd { font-size: var(--fs-ui); color: var(--fg); }

/* Reading column stays narrow; figures run full width */
.case-body { max-width: var(--measure-blurb); }

/* These are <section> tags, so they inherit the global section rule
   (max-width + horizontal padding). Neutralise it — the reading column
   is already set by .case-body. */
.case-section {
  max-width: none;
  margin: 0 0 var(--case-gap);
  padding: 0;
}

.case-section h2 {
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: var(--fw-regular);
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-h3);
  margin-bottom: var(--sp-6);
}

.case-section p { color: var(--fg-soft); margin-bottom: var(--sp-5); }

/* The opening line of a section: same size and weight as the body, set in
   full ink against the grey copy. Contrast alone carries the point, so the
   page keeps one text style instead of a third size. */
.case-section .case-lede,
.case-callout .case-lede,
.case-note .case-lede { color: var(--fg); font-weight: var(--fw-medium); }

.case-section p:last-child { margin-bottom: 0; }
.case-section strong { color: var(--fg); font-weight: var(--fw-medium); }

/* Plain discs. Not a grid: grid items lose their list marker. */
.case-list {
  list-style: disc;
  padding-left: var(--sp-5);
}

.case-list li + li { margin-top: var(--sp-5); }
.case-list li::marker { color: var(--accent); }

.case-list li { color: var(--fg-soft); }
.case-list li strong { color: var(--fg); }

/* A flow written as one line of arrows, quieter than body copy */

/* A flow of steps on one line: a node per step on a hairline, the tool it
   happens in as an eyebrow and the step under it. The stalled last step of
   an old flow gets a hollow node and a dashed line into it; the final step
   of a good flow gets a filled accent node. Stacks vertically on phones. */
.flow {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--sp-5);
}
.flow__step {
  position: relative;
  padding-top: var(--sp-5);
}
.flow__step::before {
  content: '';
  position: absolute;
  top: 4px;
  left: 0;
  right: calc(-1 * var(--sp-5));
  height: var(--bw-hair);
  background: var(--line);
}
.flow__step:last-child::before { right: 0; }
.flow__step::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 9px;
  height: 9px;
  border-radius: var(--radius-circle);
  background: var(--bg);
  border: var(--bw-thick) solid var(--accent);
  box-sizing: border-box;
}
.flow__step--done::after { background: var(--accent); }
.flow__step--stall::after { border-color: var(--fg-faint); border-style: dashed; }
.flow__step--stall::before,
.flow__step:has(+ .flow__step--stall)::before {
  background: none;
  border-top: var(--bw-hair) dashed var(--fg-faint);
}
.flow__tool {
  display: block;
  margin-bottom: var(--sp-1);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--accent);
}
.flow__step--stall .flow__tool { color: var(--fg-faint); }
/* a step that happened but isn't shown in the case study */
.flow__step--skip::before {
  background: none;
  border-top: var(--bw-hair) dashed var(--fg-faint);
}
.flow__step--skip .flow__tool,
.flow__step--skip .flow__label { color: var(--fg-faint); }
.flow__aside {
  display: block;
  margin-top: var(--sp-1);
  font-style: italic;
}
.flow__label {
  display: block;
  font-size: var(--fs-meta);
  line-height: var(--lh-copy);
  color: var(--fg-soft);
  padding-right: var(--sp-3);
}
@media (max-width: 720px) {
  .flow { grid-auto-flow: row; gap: var(--sp-5); }
  .flow__step { padding-top: 0; padding-left: var(--sp-6); }
  .flow__step::before {
    top: 4px;
    bottom: calc(-1 * var(--sp-5));
    left: 4px;
    right: auto;
    width: var(--bw-hair);
    height: auto;
  }
  .flow__step:last-child::before { display: none; }
  .flow__step--stall::before,
  .flow__step:has(+ .flow__step--stall)::before,
  .flow__step--skip::before {
    border-top: 0;
    border-left: var(--bw-hair) dashed var(--fg-faint);
  }
}

/* Text beside an image, for when a visual belongs alongside the argument
   rather than after it. Falls back to stacked on narrow screens. */
.case-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-10);
  align-items: center;
  margin-bottom: var(--case-gap);
}

.case-split .case-section { margin-bottom: 0; }

/* When the figure is taller than the text, centring drops the heading below
   the top of the image. Top-align instead, so the heading and the image
   start on the same line. */
.case-split--top { align-items: start; }

/* Blocks that continue the same section: the intro before its subsections,
   or one subsection before the next. Closes the section gap to the rhythm
   used inside a section, so the parts read as one argument. */
.case-section--continues,
.case-split--continues,
.case-body--continues { margin-bottom: var(--case-gap-inner); }

/* Acts. Without these every block sits the same distance from the next and
   the case reads as one column of text; the rule and the numeral are what
   tell a reader a new beat has started. Wide measure, so the line crosses
   the figures as well as the prose. */
.case-act {
  max-width: var(--measure-media-wide);
  margin: var(--sp-13) auto var(--sp-7);
  padding-top: var(--sp-5);
  border-top: var(--bw-thick) solid var(--line);
  display: flex;
  align-items: baseline;
  gap: var(--sp-5);
}
.case-act:first-of-type { margin-top: var(--sp-10); }
.case-act__n {
  flex: none;
  color: var(--accent);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  font-variant-numeric: tabular-nums;
}
.case-act__t {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: var(--fw-regular);
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-h3);
}
@media (max-width: 640px) {
  .case-act { margin-top: var(--sp-11); gap: var(--sp-3); }
}
.case-body--continues .case-section:last-child { margin-bottom: 0; }

.case-figure {
  max-width: var(--measure-media);
  margin: 0 0 var(--case-gap);
}

.case-figure--side {
  max-width: none;
  margin-bottom: 0;
}

.case-figure--side .project__ph { aspect-ratio: var(--ratio-portrait); }

/* Detailed screenshots break past the reading column so the UI stays legible. */
.case-figure--wide { max-width: var(--measure-media-wide); }

/* Two figures that belong together, side by side at the wide measure. Each
   keeps its own caption; the pair tops align and the captions fall where
   their images end. */
.case-pair {
  display: flex;
  gap: var(--sp-8);
  align-items: start;
  max-width: var(--measure-media-wide);
  margin-bottom: var(--case-gap);
}

/* Each figure grows in proportion to its image's aspect ratio (--ar, set on
   the figure from the image's width / height), so two images of different
   shapes come out the same height with neither one cropped. */
.case-pair .case-figure {
  flex: var(--ar, 1) 1 0;
  min-width: 0;
  max-width: none;
  margin-bottom: 0;
}

.case-figure figcaption {
  margin-top: var(--sp-4);
  font-size: var(--fs-meta);
  color: var(--fg-faint);
  max-width: var(--measure-blurb);
}

/* A caption can open with the point, the way a section does */
.case-figure figcaption strong { color: var(--fg-soft); }

/* Option sketches: the design system's table types drawn as bare
   line drawings showing only what sets each apart: grey for the options,
   accent for the pick. */
.opts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-6) var(--sp-5);
  max-width: var(--measure-opts);
  margin-inline: auto;
}
/* Once the split stacks, the figure gets the full column: one row of four
   uses that width better than a 2x2. Phones go back to 2x2. */
@media (max-width: 860px) and (min-width: 641px) {
  .opts { grid-template-columns: repeat(4, minmax(0, 1fr)); max-width: none; gap: var(--sp-5); }
}
.opt > svg {
  display: block; width: 100%; height: auto; overflow: visible;
  fill: none;
  stroke: rgb(var(--fg-rgb) / 0.36);
  stroke-width: calc(var(--bw-hair) * 1.5);
  stroke-linecap: round;
  stroke-linejoin: round;
}
.opt > svg * { vector-effect: non-scaling-stroke; }
/* chrome (frame, dividers, selection) sits a step back from the content */
.opt .opt__frame  { stroke: rgb(var(--fg-rgb) / 0.18); stroke-width: var(--bw-hair); }
.opt .opt__strong { stroke: rgb(var(--fg-rgb) / 0.6); }
.opt .opt__sel    { fill: rgb(var(--fg-rgb) / 0.07); stroke: none; }
.opt .opt__panel  { fill: rgb(var(--fg-rgb) / 0.04); stroke: none; }
.opt__check {
  display: inline-block;
  width: 1.05em; height: 1.05em;
  margin-left: var(--sp-2);
  vertical-align: -0.17em;
}
.opt__check circle { fill: currentColor; }
.opt__check path {
  fill: none;
  stroke: var(--bg);
  stroke-width: calc(var(--bw-hair) * 1.75);
  stroke-linecap: round;
  stroke-linejoin: round;
}
.opt__name {
  display: block;
  margin-top: var(--sp-2);
  font-size: var(--fs-micro);
  line-height: var(--lh-copy);
  color: var(--fg-faint);
}
.opt.is-picked > svg,
.opt.is-picked .opt__frame { stroke: var(--accent); }
.opt.is-picked .opt__name { color: var(--accent); font-weight: var(--fw-medium); }

/* Closing note — points at the live walkthrough */
.case-note {
  max-width: var(--measure-blurb);
  padding: var(--sp-7);
  border: var(--bw-hair) solid var(--line);
  border-radius: var(--radius-card);
  /* Reads as an aside through its border and ground, not by shrinking the
     type: these boxes carry the turning point of a case study. */
  color: var(--fg-soft);
}

/* The note carries its own margins. It introduces the section after it, so
   it sits closer to that than to what came before. */
.case-note p { margin: 0 0 var(--sp-4); }
.case-note p:last-child { margin-bottom: 0; }
.case-note .case-figure { max-width: none; margin: var(--sp-5) 0 0; }

.case-note .case-figure--small { max-width: 420px; }
/* A note box beside its figure: the box keeps its own frame, the figure sits
   level with its top. */
/* One bordered box holds both the note and its figure, so they read as one
   thought. The note drops its own frame inside it. */
.case-split--note {
  align-items: center;
  padding: var(--sp-7);
  border: var(--bw-hair) solid var(--line);
  border-radius: var(--radius-card);
  gap: var(--sp-9);
}
.case-split--note .case-note { max-width: none; margin: 0; padding: 0; border: 0; }
.case-split--note .case-figure { margin: 0; }
@media (max-width: 640px) { .case-split--note { padding: var(--sp-5); } }
.case-note {
  margin-top: calc(-1 * var(--sp-4));
  margin-bottom: var(--sp-9);
}

.case-nav {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-6);
  margin-top: var(--sp-12);
  padding-top: var(--sp-7);
  border-top: var(--bw-hair) solid var(--line);
  flex-wrap: wrap;
}

@media (max-width: 860px) {
  .case-split { grid-template-columns: 1fr; gap: var(--sp-8); }
  .case-pair { flex-direction: column; }
  .case-pair .case-figure { flex: none; }
}

@media (max-width: 640px) {
  .case { padding-top: var(--sp-12); }
  .case-head .link-arrow { margin-bottom: var(--sp-9); }
  .case-meta { grid-template-columns: 1fr 1fr; }

.case__title { max-width: none; }
  .case-section { margin-bottom: var(--sp-10); }
}

/* ---------- Case study: feature blocks ---------- */

.case-features {
  list-style: none;
  display: grid;
  gap: var(--sp-12);
  max-width: var(--max);
  margin-bottom: var(--sp-12);
}

.case-feature h3 {
  font-family: var(--font-display);
  font-size: var(--fs-tl);
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-h3);
  margin-bottom: var(--sp-3);
}

.case-feature p {
  color: var(--fg-soft);
  max-width: var(--measure-blurb);
  margin-bottom: var(--sp-6);
}

.case-feature .project__ph { aspect-ratio: var(--ratio-wide); }

/* Opening visual: the first thing under the title, at the wide measure. */
.case-hero { margin: 0; }
.case-head:has(.case-intro) { max-width: none; }
.case-head:has(.case-intro) .case__title { max-width: var(--measure-media); }

/* The opening visual and the project facts side by side: the picture leads,
   the facts stack in a narrow column beside it. */
.case-intro {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 220px;
  gap: var(--sp-9);
  align-items: start;
  margin-top: var(--sp-9);
}
.case-meta--stack {
  grid-template-columns: 1fr;
  gap: var(--sp-6);
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}
.case-meta--stack > div { padding-top: var(--sp-4); border-top: var(--bw-hair) solid var(--line); }
@media (max-width: 900px) {
  .case-intro { grid-template-columns: 1fr; gap: var(--sp-8); }
  .case-meta--stack { grid-template-columns: 1fr 1fr; }
}

/* Case study figures, full content width. */
.case-shot {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-card);
  filter: brightness(var(--shot-dim));
}

/* Walkthrough recordings sit in the same frame as screenshots */
.case-video { background: #000; }

/* A sequence of short clips that tell one flow. One stage at the wide
   measure shows the current clip; the chapters above it switch clips and
   fill with a progress line as each plays. Without JS every clip shows. */
.case-sequence {
  max-width: var(--measure-media-wide);
  margin-bottom: var(--case-gap);
}
.case-sequence .case-figure { max-width: none; margin: 0 0 var(--sp-7); }
.case-sequence[data-ready] .case-figure { display: none; margin: 0; }
.case-sequence[data-ready] .case-figure.is-active { display: block; }

/* The same player driven by stills instead of clips: there is no recording
   of this flow, so the motion comes from the frames themselves. Each one
   fades up as it takes the stage, and a capture taller than the others pans
   down over its hold rather than being cropped to the fold. */
.case-sequence--stills[data-ready] .case-figure.is-active .case-shot {
  animation: shot-in var(--dur-slow) ease-out both;
}
@keyframes shot-in {
  from { opacity: 0; transform: scale(1.012); }
  to   { opacity: 1; transform: none; }
}

.case-pan {
  aspect-ratio: 1440 / 900;   /* the stage: the size the other frames are */
  overflow: hidden;
  border-radius: var(--radius-card);
}
.case-pan .case-shot { border-radius: 0; }
.case-sequence--stills[data-ready] .case-figure.is-active .case-pan .case-shot {
  animation:
    shot-in var(--dur-slow) ease-out both,
    shot-pan 5s var(--dur-slow) ease-in-out both;
}
@keyframes shot-pan {
  /* travel = (image height - stage height) as a share of the image:
     1 - (900/1440) / (1358/1444) */
  from { translate: 0 0; }
  to   { translate: 0 -33.5%; }
}

@media (prefers-reduced-motion: reduce) {
  .case-sequence--stills[data-ready] .case-figure.is-active .case-shot,
  .case-sequence--stills[data-ready] .case-figure.is-active .case-pan .case-shot {
    animation: none;
  }
  /* no pan means the fold is all that shows, so give the tall frame its
     own height back and let the page scroll it */
  .case-pan { aspect-ratio: auto; }
}

.case-chapters {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--sp-5);
  display: grid;
  /* auto-flow rather than a fixed 3, so a sequence can run to four frames
     without the last one dropping to its own row */
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--sp-4);
}
@media (max-width: 640px) {
  .case-chapters { grid-auto-flow: row; grid-template-columns: 1fr 1fr; }
}
.case-chapters button {
  position: relative;
  width: 100%;
  padding: var(--sp-3) 0 0;
  border: 0;
  border-top: var(--bw-thick) solid var(--line);
  background: none;
  font: inherit;
  font-size: var(--fs-meta);
  font-weight: var(--fw-medium);
  text-align: left;
  color: var(--fg-faint);
  cursor: pointer;
  transition: color var(--dur-fast);
}
.case-chapters button::before {
  /* progress, driven by --p from 0 to 1 */
  content: '';
  position: absolute;
  top: calc(-1 * var(--bw-thick));
  left: 0;
  height: var(--bw-thick);
  width: calc(var(--p, 0) * 100%);
  background: var(--accent);
}
.case-chapters button:hover,
.case-chapters button[aria-current] { color: var(--fg); }
.case-chapters__n {
  display: inline-block;
  min-width: 2ch;
  margin-right: var(--sp-2);
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

/* Tiny diagram beside a paragraph, in the flow's own grammar: nodes on a
   hairline, caps labels, nothing else. Draws in page tokens. */
.mini-flow {
  display: block;
  width: 100%;
  max-width: 440px;
  height: auto;
  margin: 0;
  overflow: visible;
  font-family: var(--font-body);
}
.mini-flow .mf-line { fill: none; stroke: var(--line); stroke-width: 1; }
.mini-flow .mf-person { fill: none; stroke: var(--accent); stroke-width: 1.5; stroke-linecap: round; }
.mini-flow .mf-person--back { opacity: 0.45; }
.mini-flow .mf-system { fill: var(--accent); }
.mini-flow .mf-label { fill: var(--accent); font-size: 11px; font-weight: var(--fw-medium); letter-spacing: var(--ls-caps); text-transform: uppercase; }
.mini-flow .mf-note { fill: var(--fg-soft); font-size: 11px; }

/* A strip of small thumbnails in one row: the point is the repetition, not
   the detail. Each still opens full size. */
.case-strip {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--sp-3);
  align-items: start;
}
/* In a half-width column the four thumbnails sit two by two. */
.case-strip--grid { grid-template-columns: 1fr 1fr; }

.case-strip .case-shot {
  aspect-ratio: 4 / 3;
  object-fit: cover;
  object-position: top;
}
@media (max-width: 640px) { .case-strip { grid-template-columns: repeat(2, 1fr); } }

/* ---------- Lightbox ---------- */

/* Case screenshots are dense. Clicking one opens it as large as the window
   allows, with arrows to walk the rest of the case study's images. */
.zoomable { cursor: zoom-in; }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  display: grid;
  grid-template-rows: 1fr auto;
  gap: var(--sp-5);
  padding: var(--sp-7);
  background: rgb(var(--overlay-rgb) / 0.92);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-base) var(--ease-out), visibility var(--dur-base);
}

.lightbox[data-open='true'] { opacity: 1; visibility: visible; }

.lightbox__stage {
  display: grid;
  /* A definite row, so the image's max-height: 100% has something to
     resolve against. With an auto row the percentage silently does nothing
     and tall images overflow. */
  grid-template-rows: 100%;
  place-items: center;
  min-height: 0;
  overflow: hidden;
}

/* Fit the whole image in the viewport, both axes. Tall screenshots used to
   overflow and scroll, which meant opening one showed the top half and
   nothing said there was more. */
.lightbox__img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  /* Rounded like the inline screenshot, so opening one keeps its shape. */
  border-radius: 12px;
}

.lightbox__bar {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--sp-5);
  color: rgb(var(--overlay-ink-rgb) / 0.72);
  font-size: var(--fs-meta);
  line-height: var(--lh-body);
}

.lightbox__caption {
  max-width: var(--measure-blurb);
  text-align: center;
}

.lightbox__count { flex: none; font-variant-numeric: tabular-nums; }

.lightbox__btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: var(--sp-11);
  height: var(--sp-11);
  padding: 0;
  border: var(--bw-hair) solid rgb(var(--overlay-ink-rgb) / 0.28);
  border-radius: var(--radius-circle);
  background: rgb(var(--overlay-rgb) / 0.5);
  color: rgb(var(--overlay-ink-rgb) / 0.9);
  font-size: var(--fs-lead);
  line-height: 1;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}

.lightbox__btn:hover {
  background: rgb(var(--overlay-rgb) / 0.8);
  border-color: rgb(var(--overlay-ink-rgb) / 0.6);
}

.lightbox__btn--prev { left: var(--sp-5); }
.lightbox__btn--next { right: var(--sp-5); }

.lightbox__btn--close {
  top: var(--sp-5);
  right: var(--sp-5);
  transform: none;
  width: var(--sp-10);
  height: var(--sp-10);
}

.lightbox__btn[hidden] { display: none; }

@media (max-width: 640px) {
  .lightbox { padding: var(--sp-4); }
  .lightbox__btn--prev { left: var(--sp-2); }
  .lightbox__btn--next { right: var(--sp-2); }
}

@media (prefers-reduced-motion: reduce) {
  .lightbox { transition: none; }
}
/* Inline links inside the timeline. Underlined rather than coloured, so a
   line of prose does not turn into a row of coloured words. */
.tl-blurb a, .xp__blurb a,
.case-section p a,
.about-content a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: var(--bw-hair);
  text-decoration-color: rgb(var(--fg-rgb) / 0.32);
  text-underline-offset: var(--nudge);
  transition: color var(--dur-fast) ease,
              text-decoration-color var(--dur-fast) ease;
}

.tl-blurb a:hover, .xp__blurb a:hover,
.case-section p a:hover,
.about-content a:hover {
  color: var(--accent-lift);
  text-decoration-color: var(--accent-lift);
}

/* ---------- Carousel ----------
   A run of screens shown one at a time, for when the argument is "look, it's
   the same thing again" and the reader needs to step through it. */
.carousel { width: 100%; }

/* The stage is the same sand the project cards and the window mock sit on,
   so it reads as the site's image surface rather than as an empty tray. */
.carousel__viewport {
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--sand);
}

/* The track is as tall as the tallest screen and shorter ones sit centred
   in it, so the box never changes size as you step. */
.carousel__track {
  display: flex;
  align-items: center;
  transition: transform var(--dur-slide) var(--ease-out);
}

.carousel__slide {
  flex: 0 0 100%;
  min-width: 0;
  margin: 0;
  padding: var(--sp-3);
}

/* Every screen shown whole, at its own shape, lifted off the sand the way
   the home card lifts a screen. */
.carousel__slide img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-mock);
  filter: brightness(var(--shot-dim));
}

.carousel__bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-5);
  margin-top: var(--sp-4);
}

.carousel__count {
  min-width: 3.5em;
  text-align: center;
  font-size: var(--fs-meta);
  font-variant-numeric: tabular-nums;
  color: var(--fg-faint);
}

.carousel__btn {
  display: grid;
  place-items: center;
  width: var(--sp-9);
  height: var(--sp-9);
  padding: 0;
  border: var(--bw-hair) solid var(--line);
  border-radius: var(--radius-circle);
  background: none;
  color: var(--fg);
  font-size: var(--fs-ui);
  line-height: 1;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}

.carousel__btn:hover { border-color: var(--accent-lift); color: var(--accent-lift); }

@media (prefers-reduced-motion: reduce) {
  .carousel__track { transition: none; }
}

/* ---------- Before / after packaging ----------
   Legacy screens sit in a flat grey tray, slightly drained of colour, so they
   read as "old" before you read a word. Redesigned screens sit on the same
   plum stage as the case study's opening visual. Each carries a small tag. */
.shot-frame {
  position: relative;
  border-radius: calc(var(--radius-card) + 6px);
  padding: var(--sp-9) var(--sp-5) var(--sp-5);
}
.shot-frame .case-shot { border-radius: var(--radius-card); }
.shot-tag {
  position: absolute;
  top: var(--sp-4);
  left: var(--sp-5);
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
}
.shot-tag::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }


.shot-frame--after {
  background: radial-gradient(90% 80% at 35% 0%, #5A2750 0%, rgba(90, 39, 80, 0) 62%), linear-gradient(160deg, #4A1F40 0%, #2A1125 100%);
  padding: var(--sp-10);
}
.shot-frame--after .case-shot { box-shadow: 0 30px 60px -20px rgb(10 2 10 / 0.6), 0 8px 18px -6px rgb(10 2 10 / 0.3); }


/* ---------- Accordion: the old console's pages ----------
   One page open, the rest folded to labelled strips. Hover or click a strip
   to open it; click the open page to see it full size. */
.acc { display: flex; gap: var(--sp-2); height: 320px; }
.acc__panel {
  position: relative;
  flex: 0 0 46px;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: rgb(var(--fg-rgb) / 0.1);
  cursor: pointer;
  transition: flex-basis var(--dur-slow) var(--ease-out), flex-grow var(--dur-slow) var(--ease-out);
}
.acc__panel.is-open { flex: 1 1 0; cursor: zoom-in; }
.acc__panel .case-shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left top;
  border-radius: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.acc__panel:not(.is-open) .case-shot { opacity: 0.35; }
.acc__tab {
  position: absolute;
  z-index: 1;
  left: 0;
  bottom: var(--sp-4);
  width: 46px;
  display: flex;
  justify-content: center;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: var(--fs-meta);
  color: #4A423C;
  white-space: nowrap;
}
.acc__tab :is(b, strong) { font-weight: var(--fw-medium); color: #8A817A; margin-bottom: var(--sp-2); }
.acc__panel.is-open .acc__tab {
  writing-mode: horizontal-tb;
  transform: none;
  width: auto;
  left: var(--sp-3);
  bottom: var(--sp-3);
  padding: var(--sp-1) var(--sp-3);
  border-radius: 999px;
  background: rgb(255 255 255 / 0.92);
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);
}
.acc__panel.is-open .acc__tab :is(b, strong) { margin: 0 var(--sp-2) 0 0; }
.acc__panel:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (max-width: 640px) {
  .shot-frame--after { padding: var(--sp-4); }
  .acc { height: 240px; }
  .acc__panel { flex-basis: 38px; }
  .acc__tab { width: 38px; }
}
@media (prefers-reduced-motion: reduce) {
  .acc__panel, .acc__panel .case-shot { transition: none; }
}


/* A callout: a bordered box tagged with a small chip instead of a heading,
   for context that frames the work (constraints, assumptions). */
.case-callout {
  max-width: var(--measure-media);
  margin-bottom: var(--sp-12);
  padding: var(--sp-6) var(--sp-7);
  border: var(--bw-hair) solid var(--line);
  border-radius: var(--radius-card);
}
.case-callout p { color: var(--fg-soft); margin: 0 0 var(--sp-3); }
.case-callout p:last-child { margin-bottom: 0; }
.case-callout strong { color: var(--fg); font-weight: var(--fw-medium); }
.case-chip {
  display: inline-block;
  margin-bottom: var(--sp-5);
  padding: var(--sp-1) var(--sp-3);
  border-radius: 999px;
  background: rgb(var(--accent-rgb) / 0.1);
  color: var(--accent);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
}
@media (max-width: 640px) { .case-callout { padding: var(--sp-6) var(--sp-5); } }

/* The problem beside the constraint that shaped it. */
.case-split--problem {
  align-items: center;
  column-gap: var(--sp-13);
  grid-template-columns: minmax(0, var(--measure-blurb)) auto;
  justify-content: start;
}
.case-split--problem .case-callout { max-width: none; margin: 0; }

/* A sequence followed by the next subsection of the same section. */
.case-sequence--continues { margin-bottom: var(--case-gap-inner); }
.case-callout--wide { max-width: none; margin-bottom: var(--case-gap); }

/* A project without a case study yet: same card, no link, a quiet label. */
.project--soon .project__media:hover { transform: none; }
.soon-label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-3);
  border-radius: 999px;
  background: rgb(var(--accent-rgb) / 0.1);
  color: var(--accent);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
}
.soon-label::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

/* Step-through captions lead the image, so the point is read before the
   screenshot, not lost under it. */
.case-sequence .case-figure { display: flex; flex-direction: column; }
.case-sequence[data-ready] .case-figure.is-active { display: flex; }
.case-sequence .case-figure figcaption {
  order: -1;
  margin: 0 0 var(--sp-5);
  max-width: var(--measure-blurb);
  font-size: var(--fs-ui);
  color: var(--fg-soft);
}
.case-sequence .case-figure figcaption strong { color: var(--fg); font-weight: var(--fw-medium); }

/* Pills read as labels, not alerts: hairline outline, soft text, no tint. */
.soon-label,
.case-chip {
  /* Same type and colour as the masthead labels (Designing, Based in). */
  background: rgb(var(--accent-rgb) / 0.08);
  color: var(--accent);
  border: 0;
}
.soon-label::before { display: none; }
/* Coming soon is a caption, not a badge: caps label in the masthead voice,
   no fill, so it doesn't read as a startup pill. */
.soon-label { background: none; padding: 0; border-radius: 0; color: var(--fg-faint); }
/* Callout labels (My role, Key decision, Constraint) take the same caps-label
   voice as the masthead: accent ink, no tinted pill. */
.case-chip { background: none; padding: 0; border-radius: 0; }

.shot-tag::before { display: none; }

/* ---------- Legacy screens ----------
   No tray, no frame: the old screen sits straight on the page in its own
   colour, with a hairline edge and a small label above it. */
.shot-frame--before {
  background: none;
  border-radius: 0;
  padding: 0;
}
.shot-frame--before .shot-tag {
  position: static;
  display: block;
  margin-bottom: var(--sp-3);
  color: var(--fg-faint);
}
.shot-frame--before .case-shot {
  box-shadow: 0 0 0 var(--bw-hair) rgb(var(--fg-rgb) / 0.14);
}
/* A plain legacy screenshot beside text is reference, not the point: hold it
   back from the column edge so the copy has room to breathe. */
.case-split > .case-figure--side:has(> .shot-frame--before > .case-shot) {
  max-width: 82%;
  justify-self: end;
}
@media (max-width: 860px) {
  .case-split > .case-figure--side:has(> .shot-frame--before > .case-shot) { max-width: none; }
}

/* The old console's folded pages sit on the same paper, labelled in ink. */
.shot-frame--before .acc__panel { background: rgb(var(--fg-rgb) / 0.06); }
.shot-frame--before .acc__panel .case-shot { box-shadow: none; }
.acc__tab { color: var(--fg-mid); }
.acc__tab :is(b, strong) { color: var(--fg-faint); }
.acc__panel.is-open .acc__tab {
  background: rgb(var(--bg-rgb) / 0.94);
  box-shadow: 0 0 0 var(--bw-hair) rgb(var(--fg-rgb) / 0.12), 0 2px 8px rgb(var(--shadow-rgb) / 0.1);
}



/* Client logos scale with the card image, so they hold their proportion as
   the layout widens on big screens. Phone sizes stay the floor. */
.project__media { container-type: inline-size; }
.project__brand { height: max(15px, 3.2cqw); top: max(var(--sp-6), 4.2cqw); right: max(var(--sp-7), 5cqw); }
.project__brand--wide { height: max(10px, 2.1cqw); }

/* Bold inside note boxes matches bold in the body: Inter 500 in the text
   colour. 700 isn't loaded, so the browser was faking a smeared bold. */
.case-note strong,
.case-callout strong { font-weight: var(--fw-medium); color: var(--fg); }

/* Box outlines stay neutral; the rose tint only reads well on thin rules. */
.case-callout,
.case-split--note { border-color: rgb(var(--fg-rgb) / 0.14); }

/* Case header rhythm: the back link sits close to the title it belongs to,
   the deck reads as the title's second line, and one even gap sets the
   header apart from the opening visual. */
.case-head .link-arrow { margin-bottom: var(--sp-7); }
.case__title { margin-bottom: var(--sp-3); }
.case-head:has(.case-intro) .case-intro { margin-top: var(--sp-10); }

/* Walkthrough highlight: one card of a screenshot stays lit, the rest dims. */
.spot { position: relative; display: block; border-radius: var(--radius-card); overflow: hidden; }
.spot .case-shot { border-radius: 0; }
.spot__hl {
  position: absolute;
  border-radius: 10px;
  pointer-events: none;
  box-shadow: inset 0 0 0 2.5px #F2A0C6, 0 0 0 9999px rgb(20 8 18 / 0.48);
}

/* Breakdown sequences: the whole view stands on its own, the parts sit
   grouped under a small label so they read as pieces of it. */
.case-chapters--breakdown { grid-template-columns: 1fr 1fr 1fr 1fr; grid-auto-flow: row; }
.case-chapters--breakdown li:first-child { margin-right: var(--sp-6); }
@media (max-width: 640px) {
  .case-chapters--breakdown { grid-template-columns: 1fr 1fr; }
}

/* Highlight, refined: a thin ring floating just outside the card, so the
   card's own edge stays clean, over a deeper dim. */
.spot__hl {
  border-radius: 12px;
  box-shadow: 0 0 0 9999px rgb(20 8 18 / 0.56);
  outline: 1.5px solid rgb(255 255 255 / 0.9);
  outline-offset: 5px;
}

/* Lightbox keeps a walkthrough step's highlight: the frame hugs the image so
   the percentage-positioned ring lands on the same card. */
.lightbox__frame { position: relative; display: block; overflow: hidden; border-radius: 12px; }
.lightbox__frame .lightbox__img { display: block; width: 100%; height: 100%; max-width: none; max-height: none; }
.lightbox__hl[hidden] { display: none; }

/* ---------- Experience, editorial rows ----------
   Years, company and role, then the blurb, across the full width with
   hairline rules between, so the section reads as a designed index rather
   than a narrow list with dates floating off to the right. */
.xp-list { list-style: none; border-top: var(--bw-hair) solid var(--line); }
.xp {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
  column-gap: var(--sp-8);
  align-items: start;
  position: relative;
  padding: var(--sp-7) 0;
}
/* Each row draws its own bottom rule. No hover state: the rows are an index
   to read, not targets to click. */
.xp::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: var(--bw-hair);
  background: var(--line);
  pointer-events: none;
}
/* Dates sit above the company as a small caps label, the same voice as the
   section labels, so the company name is the first thing the row says. */
.xp__yrs {
  grid-column: 1;
  grid-row: 1;
  margin-bottom: var(--sp-3);
  font-family: var(--font-body);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--fg-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.xp--now .xp__yrs { color: var(--accent); }
.xp__who h3 {
  font-family: var(--font-display);
  font-size: clamp(26px, 2.6vw, 34px);
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-h3);
  line-height: var(--lh-heading);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.xp__who { grid-column: 1; grid-row: 2; }
.xp__who p { margin-top: var(--sp-2); font-size: var(--fs-ui); color: var(--fg-soft); }
.xp__now {
  font-family: var(--font-body);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--accent);
  background: rgb(var(--accent-rgb) / 0.08);
  padding: var(--sp-1) var(--sp-3);
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.xp__now::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.xp__blurb {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  color: var(--fg-soft);
  font-size: var(--fs-body);
  line-height: var(--lh-copy);
}
@media (max-width: 900px) {
  .xp { grid-template-columns: 1fr; gap: var(--sp-3); padding: var(--sp-7) 0; }
  .xp__yrs,
  .xp__who,
  .xp__blurb { grid-column: auto; grid-row: auto; }
  .xp__yrs { margin-bottom: 0; }
}


/* ---------- Big numbers ----------
   A case study's headline figures, set large in the display serif so they
   read at a glance instead of inside a sentence. */
.case-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-7) var(--sp-9);
  margin: var(--sp-6) 0 var(--sp-7);
  padding-top: var(--sp-6);
  border-top: var(--bw-hair) solid var(--line);
}
.case-stat,
.case-section .case-stat { margin: 0; }   /* stats are <p>s; keep body-copy spacing off them */
.case-stat__n {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: var(--fw-regular);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-h2);
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.case-stat__l {
  display: block;
  margin-top: var(--sp-1);
  font-size: var(--fs-meta);
  color: var(--fg-faint);
}

/* A single headline figure set beside a paragraph: no rule, no padding, the
   number at display size and the label under it. */
.case-stats--hero { display: block; margin: 0; padding: 0; border: 0; align-self: center; }
.case-stats--hero .case-stat__n { font-size: var(--fs-hero); }
.case-stats--hero .case-stat__l { font-size: var(--fs-ui); max-width: 22ch; }

/* Outcomes: a black label, the result in pink display type, and one short
   line of detail. Three columns, falling away on narrow screens. */
.case-stats--outcomes {
  display: grid;
  border-top: 0;
  padding-top: 0;
  /* the first column fits its figure; the other two share what is left */
  grid-template-columns: max-content minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: auto auto auto;
  gap: 0 var(--sp-8);
}
/* Each result spans the label row and the figure row of the grid, so every
   label starts on one line and every figure sits on the next, whatever the
   label wraps to. */
.case-stats--outcomes .case-stat {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  padding-top: var(--sp-4);
  border-top: var(--bw-hair) solid var(--line);
}
.case-stats--outcomes .case-stat__n {
  display: block;
  margin-bottom: var(--sp-2);
  font-family: var(--font-body);
  font-size: var(--fs-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--fg-soft);
}
.case-arrow { color: var(--accent); }
/* An optional line under a result figure: what the number means. */
.case-stat__d {
  display: block;
  margin-top: var(--sp-3);
  font-size: var(--fs-meta);
  line-height: var(--lh-copy);
  color: var(--fg-faint);
}
/* Results rows run wider than the reading column so three figures get room. */
.case-body--wide { max-width: var(--measure-media-wide); }
/* Results share the row in equal thirds, so the rules above them line up as
   one even set rather than sizing to whichever figure is longest. */
@media (min-width: 641px) {
  .case-stats--outcomes { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.case-stats--outcomes .case-stat__fig {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: var(--fw-regular);
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-h3);
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

/* The problem split stacks on narrow screens, where the figure sits under the text. */
@media (max-width: 860px) {
  .case-split--problem { grid-template-columns: 1fr; }
}

@media (max-width: 640px) {
  /* Row gap would also open up inside each result (subgrid shares it), so
     results are spaced by margin instead. */
  .case-stats--outcomes { grid-template-columns: 1fr; gap: 0; }
  .case-stats--outcomes .case-stat + .case-stat { margin-top: var(--sp-7); }
}

/* Supporting figures under a problem statement: no rule, smaller numerals, so
   the sentence above stays the thing the eye lands on. */
.case-stats--supporting { border-top: 0; padding-top: 0; margin: var(--sp-5) 0 var(--sp-7); }
/* the line introducing the stats sits a little apart from the paragraph above */
.case-section p:has(+ .case-stats--supporting) { margin-top: var(--sp-7); }
.case-stats--supporting .case-stat__n { font-size: var(--fs-h3); }

/* Emphasis in case studies matches the figure captions: black at medium
   weight against the grey body copy. */
.case-section strong,
.case-note strong,
.case-callout strong,
.case-figure figcaption strong { font-weight: var(--fw-medium); color: var(--fg); }

/* A callout straight under the header is part of the opening, not a new
   section, so it sits closer to it. */
.case-head:has(+ .case-callout) { margin-bottom: var(--sp-12); }

/* Case study copy in two clear tones: the point in black, the detail well
   below it. At --fg-soft the gap was too small to register. */
.case-section p,
.case-list li,
.case-note,
.case-callout p { color: var(--fg-quiet); }
.case-section .case-lede,
.case-callout .case-lede,
.case-note .case-lede { color: var(--fg); font-weight: var(--fw-medium); }

/* The chip labels its box, so it sits tight on the text it labels. */
.case-chip { margin-bottom: var(--sp-3); }

/* A subsection heading that leads straight into a figure sequence: it sits on
   the figure it titles, not a section's distance above it. */
.case-body--heading { margin-bottom: var(--sp-6); }
.case-body--heading .case-section { margin-bottom: 0; }
.case-body--heading .case-section h3 { margin-bottom: 0; }

/* Walkthrough captions read as one plain line: the tabs above already name
   each step, so the caption needs no bold lead of its own. */
.case-sequence .case-figure figcaption strong { font-weight: inherit; color: inherit; }
