: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 244 236;
  --accent-rgb: 47 107 43;
  --bloom-rgb:  168 216 78;

  --fg:         rgb(var(--fg-rgb));
  --bg:         rgb(var(--bg-rgb));
  --accent:     rgb(var(--accent-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.72);
  --fg-faint:   rgb(var(--fg-rgb) / 0.68);

  --line:       rgb(var(--fg-rgb) / 0.12);

  /* ---- Type -------------------------------------------------------- */
  --font-display: 'Instrument Serif', Georgia, 'Times New Roman', serif;
  --font-body: 'Inter', 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: 400;
  --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 */

  /* 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.024em;   /* Inter, pulled in to sit with the serif */
  --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;

  /* ---- 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;
  --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;

  /* ---- Elevation ---------------------------------------------------- */
  --shadow-mock: 0 1px 2px rgb(var(--fg-rgb) / 0.05),
                 0 18px 44px -8px rgb(var(--fg-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.38;
  --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.52;
  --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:         #4B7D06;
  --sheen-deep:   #2F6B2B;   /* the darkest point of the drift */
  --sheen-bright: #6DA00D;   /* a narrow highlight band, kept narrow on purpose */

  --sand: linear-gradient(140deg, #F2E5D6 0%, #EADCCB 55%, #E4D6C4 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 ------------------------------------------------------- */
  --max: 1080px;
  --pad: var(--sp-7);
}

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

html {
  scroll-behavior: 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;
  overflow-x: hidden;
}

.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; }

/* ---------- 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);
}

/* 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); }

/* 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-top: var(--sp-8); padding-bottom: var(--sp-14); }

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

.project {
  display: grid;
  grid-template-columns: 1.15fr 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.15fr; }
.project:nth-child(even) .project__media { order: 2; }

.project__media {
  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 */
.project__shot {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--ratio-media);
  object-fit: cover;
  object-position: top;
}

.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__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); border-color: var(--accent); }
.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-top: var(--sp-5); padding-bottom: var(--sp-14); }

.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); }

.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(--sp-14); }

.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-top: var(--sp-8);
  padding-bottom: var(--sp-14);
  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) {
  html { scroll-behavior: auto; }
  .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-12); }
  .about-grid { grid-template-columns: 1fr; gap: var(--sp-7); }
  .about-photo { max-width: var(--col-photo-sm); position: static; }
}

@media (max-width: 640px) {
  .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-11); }
  .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); }
  .work, .experience, .about, .contact { padding-bottom: 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(--sp-12);
}

/* 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;
  grid-template-columns: repeat(auto-fit, minmax(120px, 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(--sp-12);
  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-5);
}

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

.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); }

/* 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(--sp-12);
}

.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(--sp-9); }
.case-body--continues .case-section { margin-bottom: 0; }

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

.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(--sp-12);
}

/* 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);
}

/* 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);
  font-size: var(--fs-ui);
  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 {
  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__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); }

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

/* ---------- 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(--fg-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;
  /* No radius and no fill here. Inline, a screenshot is a card and rounds
     with the others; opened full size it is the screen itself, and rounded
     corners cut into the app chrome at the edges. */
}

.lightbox__bar {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--sp-5);
  color: rgb(var(--bg-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(--bg-rgb) / 0.28);
  border-radius: var(--radius-circle);
  background: rgb(var(--fg-rgb) / 0.5);
  color: rgb(var(--bg-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(--fg-rgb) / 0.8);
  border-color: rgb(var(--bg-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 {
  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 {
  color: var(--accent);
  text-decoration-color: var(--accent);
}

/* ---------- 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);
}

.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); color: var(--accent); }

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