/* ==========================================================================
   Portfolio 2026 — homepage + case studies
   1200px content on a 12-column grid (12 x 45px columns, 11 x 60px gutters).
   Spacing scale: 2 4 8 12 16 20 24 32 48 60 72 84 96 120.
   Media compositions reproduce a fixed 1200px canvas and scale with cqw.
   ========================================================================== */

/* --- Fonts -------------------------------------------------------------- */
/* Self-hosted copies of the Google Fonts latin subsets. Browsers stopped
   sharing a CDN's cache between sites in 2020, so loading them from Google
   only added two connections and a stylesheet round trip. Every character on
   the site falls inside this range.

   Filenames carry Google's version, and vercel.json caches this folder for a
   year: a changed file needs a new name. Crimson Pro is one variable file for
   both weights. Crimson Pro and Figtree are preloaded from each page's <head>;
   rename those links too. */

@font-face {
  font-family: "Crimson Pro";
  font-style: normal;
  font-weight: 300 400;
  font-display: swap;
  src: url("../assets/fonts/crimson-pro-v28-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "DM Mono";
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url("../assets/fonts/dm-mono-v16-latin-300.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "DM Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../assets/fonts/dm-mono-v16-latin-500.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/figtree-v9-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- Tokens -------------------------------------------------------------- */

:root {
  /* Spacing scale */
  --space-2: 2px;
  --space-4: 4px;
  --space-8: 8px;
  --space-12: 12px;
  --space-16: 16px;
  --space-20: 20px;
  --space-24: 24px;
  --space-32: 32px;
  --space-48: 48px;
  --space-60: 60px;
  --space-72: 72px;
  --space-84: 84px;
  --space-96: 96px;
  --space-120: 120px;

  /* Colour — text. Four steps, named and valued as the Figma library:
     text/primary · text/secondary · text/tertiary · text/quaternary. Text is
     always given one of these outright — never a colour plus an opacity, which
     composites to a value no token can name. */
  --color-text-primary: #394153;
  --color-text-secondary: rgba(57, 65, 83, 0.8);
  --color-text-tertiary: rgba(57, 65, 83, 0.55);
  --color-text-quaternary: rgba(57, 65, 83, 0.4);
  /* For copy sitting on a dark tile. */
  --color-text-inverse-primary: #ffffff;
  --color-text-inverse-secondary: rgba(255, 255, 255, 0.5);

  /* Colour — surfaces */
  --color-bg: #f3f5fb;                   /* surface/primary */
  --color-tint: rgba(57, 65, 83, 0.03);  /* surface/secondary — footer band */
  /* Buttons: surface/button, border/button and shadow/button. The lift is
     ours — the Figma has one resting shadow and no hover state. */
  --surface-button: rgba(255, 255, 255, 0.5);
  --border-button: #ffffff;
  --shadow-button: 0 2px 2px rgba(0, 0, 0, 0.05);
  --shadow-button-lift: 0 5px 10px rgba(0, 0, 0, 0.06);
  --hover-scale: 1.075;
  /* The bar's slide. Far longer than a hover on purpose: it is a large object
     travelling its own height, and at interaction speed it reads as a snap. */
  --nav-slide: 500ms;
  --color-available: #2ab45f;            /* availability/available */
  --color-border: rgba(57, 65, 83, 0.1); /* border/default — every rule and edge */
  --color-border-subtle: rgba(57, 65, 83, 0.05); /* border/subtle */

  /* Media surfaces — layered radial gradients taken from the Figma fills.
     Figma applies an opacity to the whole tint layer; that is folded into the
     stop colours here, since CSS gradients have no layer opacity. */
  --surface-light: radial-gradient(53.39% 58% at 50% 50%, #eff2f9 0%, #f1f2f8 100%);

  --surface-heineken: #37466a;
  --surface-heineken-tint: radial-gradient(50% 54.6% at 50% 50%, rgba(57, 65, 83, 0.3) 0%, rgba(42, 44, 51, 0.6) 100%);

  --surface-emo: #4d5bd7;
  --surface-emo-tint: radial-gradient(50% 50% at 50% 50%, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.2) 100%);

  /* Radii */
  --r-out: 60px;
  --r-md: 24px;    /* card thumbnails */
  --r-logo: 10px;  /* résumé logo tiles */
  --r-in: 8px;
  --r-screen: 4px;
  --r-pill: 1000px;

  /* Type */
  --font-display: "Crimson Pro", Georgia, "Times New Roman", serif;
  --font-sans: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "DM Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* Type sizes are named for the Figma text styles they implement, so a
     style in the library and a token here are the same thing by name.
     Sizes that step down do so in the 1024 and 767 blocks at the foot. */
  --size-display: 80px;        /* Heading/Display — homepage H1 */
  --size-h1: 60px;             /* Heading/H1 */
  --size-h2: 42px;             /* Heading/H2 */
  --size-h3: 36px;             /* Heading/H3 */
  --size-h4: 24px;             /* Heading/H4 — résumé names, availability, award rank */
  --size-subheadline: 24px;    /* Body/Subheadline — case study lead, homepage intro */
  --size-body-l: 20px;         /* Body/Body L — the base size */
  --size-body-m: 18px;         /* Body/Body M */
  --size-body-s: 14px;         /* Body/Body S — meta values, pills, availability note */
  --size-eyebrow-xl: 40px;     /* Eyebrow/Eyebrow XL — stat values, award medals */
  --size-eyebrow-l: 24px;      /* Eyebrow/Eyebrow L — numbered step counters */
  --size-eyebrow-m: 14px;      /* Eyebrow/Eyebrow M — section labels, kickers, years */
  --size-eyebrow-s: 12px;      /* Eyebrow/Eyebrow S — card kickers, tags, colophon */
  /* Ours, not a Figma variable: labels that sit as one eyebrow beside another —
     résumé years, the intro's summary and meta labels, stat labels, award
     sources. Eyebrow M, stepping down to Eyebrow S on phones. Size and
     tracking change together, so a rule that uses one uses both. */
  --size-eyebrow-meta: var(--size-eyebrow-m);
  --tracking-eyebrow-meta: 0;
  --size-button-label: 14px;   /* Components/Button Label */
  --size-quote: 28px;          /* Components/Quote */
  --size-caption: 16px;        /* Components/Caption */

  /* Reveal timing. JS only assigns --reveal-index; the durations live here so
     the whole cascade can be retimed from one place. */
  --reveal-duration: 700ms;
  --reveal-base-delay: 200ms; /* beat between crossing into view and starting */
  --reveal-step: 200ms;
  --reveal-rise: 12px;
  /* Symmetric S-curve: 50% of the fade at 50% of the time, so the duration
     above is what you actually perceive. Sharp ease-outs like (.22,1,.36,1)
     hit ~90% opacity in the first quarter and spend the rest invisibly
     settling, which made a 700ms fade read as barely 100ms. */
  --reveal-ease: ease-in-out;

  /* Hover/focus timing. Separate from the reveal above on purpose: a reveal is
     something you watch, an interaction is something you steer, so it wants to
     land inside a fifth of a second. */
  --hover-duration: 200ms;
  --hover-ease: ease-out;

  /* Layout — the 12-column grid.
     12 columns of 45px + 11 gutters of 60px = 1200px. Every width in the
     design is a span on it: 2 cols = 150 (labels), 3 = 255, 4 = 360,
     5 = 465 (cards), 7 = 675, 8 = 780 (prose), 10 = 990. */
  --content: 1200px;
  --gutter: var(--space-60);
  --page-pad: var(--space-120);
  --measure: 780px;              /* 8 columns */
  --prose-indent: 210px;         /* 2 columns + gutter */
}

/* --- Reset --------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  /* In-page jumps glide rather than cut. Unguarded, matching js/links.js,
     which also runs under reduced motion: this is movement the reader asked
     for by clicking. The script overrides this with its own timing. */
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text-primary);
  font-family: var(--font-sans);
  font-size: var(--size-body-l);
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, p, figure, blockquote, ul, ol, li, dl, dt, dd { margin: 0; padding: 0; }
ul, ol { list-style: none; }
img, video { display: block; max-width: 100%; }

a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
a:hover { text-decoration-thickness: 2px; }

/* Anything jumped to keeps clear of the viewport edge. Not :target — that
   selector matches only once the hash is already set, which is a race with the
   scroll it is meant to offset. */
/* No allowance for the nav — it is usually away by the time you land. */
[id] { scroll-margin-top: var(--space-60); }

/* --- Reveal on scroll ---------------------------------------------------- */
/* Structural blocks only: headers, images, cards, and the lower sections as
   whole blocks. Nothing inside a block animates separately — animating every
   row and paragraph flattens the effect until none of it registers, so scope
   and the résumé each fade in as one piece.

   Only active once the inline script in each page's <head> adds .js-reveal, so
   the content is visible with no JS. The hidden state has to exist before the first paint, which rules out
   having JS tag the elements, so this list is repeated as SELECTOR in
   js/reveal.js. Change one, change both. */

.js-reveal :is(
  .article-header > *,
  .hero > *,
  .band > .band__label,
  .availability__inner,
  .card,
  .scope,
  .resume,
  .section > .tile,
  .media .tile,
  .panel > .panel-item
) {
  @media (prefers-reduced-motion: no-preference) {
    --reveal-delay: calc(
      var(--reveal-base-delay) + var(--reveal-step) * var(--reveal-index, 0)
    );
    transition:
      opacity var(--reveal-duration) var(--reveal-ease) var(--reveal-delay),
      transform var(--reveal-duration) var(--reveal-ease) var(--reveal-delay);

    /* Hiding via :not() rather than a competing .is-revealed rule keeps the
       transition on the element and avoids a specificity fight. */
    &:not(.is-revealed) {
      opacity: 0;
      transform: translateY(var(--reveal-rise));
    }
  }

  @media print {
    opacity: 1 !important;
    transform: none !important;
  }
}

/* On phones the first screen should not keep the reader waiting: whatever is
   on screen at load (tagged .is-initial by js/reveal.js) starts at once and
   fades faster. Later reveals, and wider screens, keep the timing above. */
@media (max-width: 767px) {
  .js-reveal .is-initial {
    --reveal-base-delay: 0ms;
    --reveal-step: 100ms;
    --reveal-duration: 400ms;
  }
}

/* --- Page transitions ---------------------------------------------------- */
/* Classes set on <html> by js/links.js when leaving, and by the inline script
   in each page's <head> when arriving. Above 767 a curtain wipes up over the
   page you leave and collapses into the top edge of the page you arrive at.
   At 767 and below the page fades instead: on iOS Safari a fixed
   full-viewport layer has no stable height while the toolbars move, and the
   wipe tore. js/links.js mirrors the durations for its timers; keep the two in
   step. */

@media (prefers-reduced-motion: no-preference) {
  @media (min-width: 768px) {
    /* scaleY off a bottom origin rather than an animated height: same
       picture, but it stays on the compositor instead of laying out every
       frame. */
    html::after {
      content: "";
      position: fixed;
      inset: 0;
      background: #edf0f6; /* --color-tint flattened over --color-bg */
      transform: scaleY(0);
      transform-origin: bottom;
      z-index: 2147483647;
      pointer-events: none;
    }

    /* Arriving, the origin flips to the top so the curtain retreats the same
       way it came in rather than dropping back the way it rose. */
    html.pt-covered::after { transform: scaleY(1); transform-origin: top; }

    html.pt-cover::after {
      transform: scaleY(1);
      transition: transform 700ms cubic-bezier(.5, 0, .75, .2);
    }

    html.pt-uncover::after {
      transform: scaleY(0);
      transform-origin: top;
      transition: transform 900ms cubic-bezier(.2, .6, .2, 1);
    }

    /* Swallow clicks while the curtain is up, so an impatient second click
       cannot start a navigation the user can no longer see. */
    html.pt-cover::after,
    html.pt-covered::after { pointer-events: auto; }
  }

  /* The ground stays put: body's background is painted on the canvas, which
     the body's opacity does not reach. No pointer events while fading out, for
     the same reason the curtain swallows them. */
  html.pt-faded body { opacity: 0; }

  html.pt-fade-out body {
    opacity: 0;
    pointer-events: none;
    transition: opacity 500ms ease;
  }

  html.pt-fade-in body { transition: opacity 400ms ease; }
}

/* --- Site nav ------------------------------------------------------------ */
/* Full-bleed, three columns so the avatar stays optically centred on the page
   regardless of how long the side links are. */

.site-nav {
  position: sticky;
  top: 0;
  z-index: 10;
  /* Slides away on the way down and comes back on the way up — js/nav.js sets
     the class. Hiding by its own height clears the viewport exactly. */
  transition: transform var(--nav-slide) ease-in-out;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--gutter);
  padding: var(--space-32) min(var(--space-60), var(--page-pad));
  width: 100%;
}

/* Taller than a button in the page body, so the text pill matches the height
   of the icon pills beside it. */
.site-nav .button {
  gap: var(--space-8);
  padding: var(--space-16) var(--space-20);
  letter-spacing: 0.05em;
}

.site-nav .button--icon { padding: var(--space-16); }

.site-nav.is-hidden { transform: translateY(-100%); }

.site-nav__start { justify-self: start; }

.site-nav__links {
  justify-self: end;
  display: flex;
  align-items: center;
  gap: var(--space-8);
}

.site-nav__avatar {
  display: block;
  width: 54px;
  height: 54px;
  border-radius: var(--r-pill);
  object-fit: cover;
  transition: transform var(--hover-duration) var(--hover-ease);
}

.site-nav a:hover > .site-nav__avatar,
.site-nav a:focus-visible > .site-nav__avatar {
  transform: scale(var(--hover-scale));
}

/* The homepage's section shortcut swaps its label as you pass the section it
   points at: the label fades out, its text changes while it is invisible, and
   it fades back in. The pill itself stays put and keeps its own hover.

   No counterpart rule for the way back: dropping .is-out at the swap leaves
   the label at 0 and lets the same transition carry it to 1. */
.nav-swap {
  display: inline-block;
  transition: opacity var(--hover-duration) var(--hover-ease);
}

.nav-swap.is-out { opacity: 0; }

/* --- Chevron -------------------------------------------------------------- */
/* Inline SVG so it inherits colour; 6x10 to match the Figma stroke. */

.chevron {
  --chevron-flip: 1;
  flex: none;
  width: 6px;
  height: 10px;
  stroke: currentColor;
  stroke-width: 1.6;
  fill: none;
  /* Mirrored rather than rotated, so the hover nudge below can be written once:
     translateX inside a flipped space already points the other way. */
  transform: scaleX(var(--chevron-flip));
  transition: transform var(--hover-duration) var(--hover-ease);
}

.chevron--left { --chevron-flip: -1; }

/* --- Button --------------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-8);
  border: 1px solid var(--border-button);
  border-radius: var(--r-pill);
  padding: var(--space-12) var(--space-20);
  background: var(--surface-button);
  /* Actually frosted, not just translucent — the nav is sticky, so copy
     scrolls behind these and a plain 50% white would let it read through. */
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
  box-shadow: var(--shadow-button);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-button-label);
  line-height: 20px;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  color: var(--color-text-secondary);
  transition:
    color var(--hover-duration) var(--hover-ease),
    background-color var(--hover-duration) var(--hover-ease),
    box-shadow var(--hover-duration) var(--hover-ease),
    transform var(--hover-duration) var(--hover-ease);
}

/* An icon button is square: the label's 20px side padding would leave a 20px
   glyph sitting in too wide a pill, so all four sides match the vertical. */
.button--icon {
  gap: var(--space-8);
  padding: var(--space-16);
}

/* Icon buttons in the nav and the footer are the large variant: a 20px glyph
   in 16px of padding. */
.button__icon {
  display: block;
  flex: none;
  width: 20px;
  height: 20px;
  fill: currentColor;
}

/* Scaling up reads as the pill coming toward you; the shadow spreads at the
   same time, the way a lifted object's shadow does. */
.button:hover,
.button:focus-visible {
  color: var(--color-text-primary);
  box-shadow: var(--shadow-button-lift);
  transform: scale(var(--hover-scale));
}

/* The arrow steps away from the label, and only when the pill itself is the
   target — it moves with the border colour, not with the card hover. */
.button:hover .chevron,
.button:focus-visible .chevron {
  transform: scaleX(var(--chevron-flip)) translateX(3px);
}

/* --- Page shell ---------------------------------------------------------- */

.page {
  display: flex;
  justify-content: center;
  padding: var(--page-pad);
}

/* Figma's section rhythm is the 7xl token. */
.article {
  display: flex;
  flex-direction: column;
  gap: var(--space-96);
  width: var(--content);
  max-width: 100%;
}

/* A section groups prose and media that belong together. */
.section {
  display: flex;
  flex-direction: column;
  gap: var(--space-60);
  width: 100%;
}

/* The reading column: columns 3-10 of the grid, so the 780px measure sits
   centred with a 210px shoulder on each side. */
.prose {
  width: 100%;
  max-width: calc(var(--prose-indent) + var(--measure));
  margin-right: auto;
  padding-left: var(--prose-indent);
}

.prose-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}
.prose-stack--tight { gap: var(--space-12); }
.prose-stack--loose { gap: var(--space-24); }
.prose-stack--section { gap: var(--space-48); }

.body-copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}

/* --- Typography ---------------------------------------------------------- */

.kicker {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-m);
  line-height: 1.3;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

/* The one above the title carries more weight than the section eyebrows. */
.article-header > .kicker { color: var(--color-text-secondary); }
/* The intro's "Key summary" label; the header kicker above keeps Eyebrow M. */
.prose-stack--tight > .kicker {
  font-size: var(--size-eyebrow-meta);
  letter-spacing: var(--tracking-eyebrow-meta);
}

.title {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--size-h1);
  line-height: 0.9;
  letter-spacing: -0.04em;
}

.lead {
  font-size: var(--size-subheadline);
  line-height: 1.3;
}

h2 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--size-h2);
  line-height: 1;
  letter-spacing: -0.02em;
}

h3 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--size-h3);
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.body-copy p { font-size: var(--size-body-l); line-height: 1.4; }

/* --- Article header ------------------------------------------------------ */

.article-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
  padding-top: var(--space-32);
  width: 100%;
}

/* --- Summary + meta ------------------------------------------------------ */

.meta {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-24);
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-20);
  width: 100%;
}

.meta-cell {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  align-self: start;
  word-break: break-word;
}

.meta-label {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-meta);
  letter-spacing: var(--tracking-eyebrow-meta);
  line-height: 1.3;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

.meta-value {
  font-size: var(--size-body-s);
  line-height: 1.4;
  color: var(--color-text-tertiary);
}

.pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.pill {
  border: 1px solid var(--color-border);
  border-radius: var(--r-pill);
  padding: var(--space-2) var(--space-8);
  font-size: var(--size-body-s);
  line-height: 1.4;
  color: var(--color-text-tertiary);
  white-space: nowrap;
}

/* --- Quotes -------------------------------------------------------------- */

.quote {
  display: flex;
  flex-direction: column;
  /* Above Figma's 8: the attribution is a small mono label now, and 8 sat it
     too close under a 28px quote. */
  gap: var(--space-12);
  border-left: 4px solid var(--color-border);
  padding: var(--space-16) var(--space-32);
  width: 100%;
}

.quote p:first-child {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--size-quote);
  line-height: 1.1;
  letter-spacing: -0.03em;
}

/* A label, not a line of prose — <cite>'s default italic is overridden. */
.quote cite {
  font-family: var(--font-mono);
  font-weight: 500;
  font-style: normal;
  font-size: var(--size-eyebrow-s);
  line-height: 1.3;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

/* --- Stats --------------------------------------------------------------- */

/* Like the Figma, a stats row stays on one line and is allowed to run past the
   800px measure into the empty right margin rather than wrapping. */
.stats {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-48);
}

.stat {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  border-top: 2px solid var(--color-border);
  padding-top: var(--space-16);
  padding-right: var(--space-48);
  white-space: nowrap;
}

.stat-value {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  font-family: var(--font-mono);
  font-weight: 300;
  font-size: var(--size-eyebrow-xl);
  line-height: 1;
  letter-spacing: -0.05em;
}

.stat-unit { color: var(--color-text-quaternary); }

.stat-label {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-meta);
  letter-spacing: var(--tracking-eyebrow-meta);
  line-height: 1.3;
  text-transform: uppercase;
  color: var(--color-text-secondary);
  white-space: normal;
}

/* --- Numbered steps ------------------------------------------------------ */

.step {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  width: 100%;
}

.step-head {
  display: flex;
  align-items: center;
  gap: var(--space-16);
}

.step-num {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--space-48);
  height: var(--space-48);
  border: 1px solid var(--color-border);
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-weight: 300;
  font-size: var(--size-eyebrow-l);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--color-text-tertiary);
}

.step-body { padding-left: calc(var(--space-48) + var(--space-16)); }

/* --- Awards -------------------------------------------------------------- */

.awards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-24);
  width: 100%;
}

.award {
  display: flex;
  align-items: center;
  gap: var(--space-24);
  border-left: 2px solid var(--color-border);
  padding-left: var(--space-24);
  padding-block: var(--space-8); /* runs the rule past the text, top and bottom */
}

.award-medal {
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", var(--font-mono);
  font-weight: 400;
  font-size: var(--size-eyebrow-xl);
  line-height: 1;
}

.award-detail {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  line-height: 1.3;
}

/* The rank reads as a small heading rather than bold body copy. */
.award-detail strong {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--size-h4);
  line-height: 1.05;
  letter-spacing: -0.01em;
  color: var(--color-text-primary);
}

.award-category {
  font-size: var(--size-body-m);
  color: var(--color-text-secondary);
}

.award-source {
  margin-top: var(--space-2); /* 2 + 2 gap = 4 below the category */
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-meta);
  letter-spacing: var(--tracking-eyebrow-meta);
  line-height: 1.3;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

/* --- Media: grids and tiles ---------------------------------------------- */

.media {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
  width: 100%;
}

.media-row {
  display: flex;
  gap: var(--space-24);
  width: 100%;
}
.media-row > * { flex: 1 1 0; min-width: 0; }

/* Corner system: 60px on the outside of a grid, 8px on every inside corner. */
.tile {
  --tl: var(--r-in);
  --tr: var(--r-in);
  --br: var(--r-in);
  --bl: var(--r-in);
  --tile-stroke: var(--color-border);
  border-radius: var(--tl) var(--tr) var(--br) var(--bl);
  overflow: hidden;
  position: relative;
  container-type: inline-size;
}

/* The stroke is drawn over the image edge rather than around it, matching
   Figma's inside-aligned stroke. Keeping it off the box also means the image
   fills the tile and the corner clip lines up with the stroke exactly. */
.tile::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--tile-stroke);
  border-radius: inherit;
  pointer-events: none;
  /* A <video> gets its own compositing layer and paints over ordinary
     positioned content, which drops the stroke on video tiles. */
  z-index: 1;
}

.tile > img { width: 100%; height: auto; }

.r-tl { --tl: var(--r-out); }
.r-tr { --tr: var(--r-out); }
.r-br { --br: var(--r-out); }
.r-bl { --bl: var(--r-out); }
.r-all { --tl: var(--r-out); --tr: var(--r-out); --br: var(--r-out); --bl: var(--r-out); }

.tile--subtle-border { --tile-stroke: var(--color-border-subtle); }

/* --- Media: rebuilt panels ----------------------------------------------- */

/* A panel reproduces a 1200x783 Figma composition. Side padding always matches
   the gap between the screens inside, so nothing sits tight to the edge.
   Padding percentages resolve against the 1200px parent; gap percentages and
   the children's cqw resolve against the padded content box, so both are
   restated per variant to keep the Figma sizes exact. */
.panel {
  display: flex;
  align-items: center;
  justify-content: center;
  padding-inline: 5%;    /* 60px */
  gap: 5.5556%;          /* 60px of the 1080px content box */
  aspect-ratio: 1200 / 783;
  background: var(--surface-light);
}

.panel--gap-wide { padding-inline: 10%; gap: 12.5%; }   /* 120px, box 960 */
.panel--gap-ads { padding-inline: 7%; gap: 8.14%; }     /* 84px, box 1032 */

.panel--heineken {
  background-color: var(--surface-heineken);
  background-image: var(--surface-heineken-tint);
}
.panel--emo {
  background-color: var(--surface-emo);
  background-image: var(--surface-emo-tint);
}

.panel-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-24);
}

/* Phone mockups share one height so mixed exports line up. */
.panel-item > img { height: 51.45cqw; width: auto; }          /* 555.7px */
.panel--gap-wide .panel-item > img { height: 57.88cqw; }       /* 555.7px */
.panel--ads .panel-item > img { height: 51.36cqw; }            /* 530px */

.caption {
  font-size: var(--size-caption);
  line-height: 1.4;
  text-align: center;
  text-wrap: balance; /* evens out the line lengths instead of a short last line */
}

/* A caption never runs wider than the image above it. Zero width keeps the
   caption out of the item's intrinsic sizing, so the image alone sets it. */
.panel-item > .caption {
  width: 0;
  min-width: 100%;
}

.panel--heineken .caption,
.panel--emo .caption,
.tile--emo .caption { color: var(--color-text-inverse-primary); }
.tile--emo .caption-credit { color: var(--color-text-inverse-secondary); }

/* Opacity rather than a fixed colour, so it dims against whichever caption
   colour the surface sets — dark on the light tiles, white on the coloured. */
.caption-credit { color: var(--color-text-tertiary); }

/* Holds a phrase together on one line. */
.nowrap { white-space: nowrap; }

/* Tiles that hold a screenshot on a background (Pocket Science, Finding Emo) */
.tile--composed {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5.44%; /* 32px at the 588px design width */
  aspect-ratio: 1 / 1;
  background: var(--surface-light);
}

.tile--emo {
  background-color: var(--surface-emo);
  background-image: var(--surface-emo-tint);
}

.tile--composed > img {
  width: 84.2%;
  height: auto;
  border-radius: var(--r-screen);
  filter: drop-shadow(0 1px 1.5px rgba(57, 65, 83, 0.1));
}

.tile--composed > .caption { max-width: 84.2%; }

/* Full-width composed panel (Pocket Science homepage shot) */
.panel--composed {
  flex-direction: column;
  gap: var(--space-24);
}
.panel--composed > .panel-item > img {
  height: auto;
  width: 79.63cqw; /* 860px of the 1080px content box */
  border-radius: var(--r-screen);
  filter: drop-shadow(0 1px 1.5px rgba(57, 65, 83, 0.1));
}

/* Illustrations sit flat on the tile — no screen chrome */
.tile--emo.tile--composed > img { width: 85%; border-radius: 0; filter: none; }
.tile--emo.tile--composed > .caption { max-width: 85%; }

/* Design-system diagram sits alone on a light panel */
.panel--diagram { aspect-ratio: 1200 / 717; }
.panel--diagram > img { width: 89.73cqw; height: auto; } /* 969px */

/* Captioned visuals on tablet. The fixed canvas ratio stops leaving room for
   the caption below the desktop widths, and a flex item with an aspect ratio
   will not grow past it — so here the tile drops the ratio, fits its content
   and keeps 24 above and below it. Desktop keeps the ratio: it already clears
   24 at every desktop width. Phones set their own padding further down. */
@media (min-width: 768px) and (max-width: 1024px) {
  .panel:has(.caption),
  .tile--composed {
    aspect-ratio: auto;
    padding-block: var(--space-24);
  }
  /* 32 at the 588px design width, as on desktop. With no fixed height, a
     percentage gap would collapse to nothing; a percentage margin resolves
     against the width instead. */
  .tile--composed { gap: 0; }
  .tile--composed > * + * { margin-top: 5.44%; }
}

/* --- Video --------------------------------------------------------------- */

.tile > video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  aspect-ratio: 1200 / 783;
}

/* Safari won't clip a composited video layer cleanly against the tile's
   border-radius, leaving seams along the edge while the video plays. A fully
   opaque mask forces WebKit down its masking path, which does clip correctly;
   both gradients are solid, so nothing changes in browsers that were already
   fine. Scoped to video tiles so the image tiles keep their working path. */
.tile:has(> video) {
  -webkit-mask-image: -webkit-radial-gradient(white, black);
  mask-image: linear-gradient(#000, #000);
}

/* --- Grid ---------------------------------------------------------------- */
/* One 12-column grid shared by the homepage and the case studies. Columns
   flex; the 60px gutter is fixed, because the gutter *is* the spacing token. */

.grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  width: var(--content);
  max-width: 100%;
}

/* --- Labelled band -------------------------------------------------------- */
/* A DM Mono label in the first two columns, content in the rest, under a rule.
   Used for case studies, scope, résumé and continue reading. */

.band {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-60);
}

.band__label {
  grid-column: 1 / span 2;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-m);
  line-height: 1.3;
  /* The elements are h2 for the document outline; this keeps the global
     heading tracking from reaching a DM Mono label. */
  letter-spacing: normal;
  text-transform: uppercase;
  color: var(--color-text-primary);
}

.band__body { grid-column: 3 / span 10; }

/* --- Card ----------------------------------------------------------------- */
/* 5 columns wide. Thumbnail, then a text stack, then the button. */

.card {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
  text-decoration: none;
  color: inherit;
}

.card__thumb {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  aspect-ratio: 465 / 310;
  background: var(--surface-light);
  container-type: inline-size;
}

/* Same inside-aligned stroke the media tiles use. */
.card__thumb::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--color-border);
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
}

.card__thumb > img,
.card__thumb > video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--hover-duration) var(--hover-ease);
}

/* Safari won't clip a composited video layer against a border-radius; an
   opaque mask forces it down the masking path, which does. */
.card__thumb:has(> video) {
  -webkit-mask-image: -webkit-radial-gradient(white, black);
  mask-image: linear-gradient(#000, #000);
}

.card__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-20);
  align-items: flex-start;
}

.card__copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-16);
}

.card__kicker {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-s);
  line-height: 1.3;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
  transition: color var(--hover-duration) var(--hover-ease);
}

.card__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--size-h3);
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--color-text-primary);
}

.card__desc {
  font-size: var(--size-body-m);
  line-height: 1.4;
  color: var(--color-text-secondary);
}

/* The card announces itself by moving into its own frame rather than by
   underlining the headline; the eyebrow comes up a step with it. */
.card:hover .card__kicker,
.card:focus-visible .card__kicker { color: var(--color-text-secondary); }

.card:hover .card__thumb > img,
.card:hover .card__thumb > video,
.card:focus-visible .card__thumb > img,
.card:focus-visible .card__thumb > video { transform: scale(1.04); }

/* Two cards side by side, 5 columns each. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--gutter);
}

/* --- Homepage hero -------------------------------------------------------- */

.hero {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: var(--space-12);
  padding-top: var(--space-32);
  width: 100%;
}

.hero__title {
  grid-column: 1 / span 7;
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--size-display);
  line-height: 0.9;
  letter-spacing: -0.04em;
  color: var(--color-text-primary);
  /* Two layouts only: "Julian is a product / designer and builder" or "Julian
     is a / product designer / and builder". The nowrap spans in the markup leave
     three places to break, and balance takes the most even split among them —
     which is always one of those two. The cap sits between the ~7.3em the
     two-line layout needs and the ~13.9em a single line would take, so the
     title never runs onto one line; it only binds on wide phones. */
  text-wrap: balance;
  max-width: 12em;
}

.hero__intro {
  grid-column: 8 / span 5;
  align-self: end;
  font-size: var(--size-subheadline);
  line-height: 1.3;
  color: var(--color-text-primary);
}

/* --- Scope ---------------------------------------------------------------- */

.scope {
  display: flex;
  flex-direction: column;
  gap: var(--space-32);
  max-width: var(--measure);
}

/* 3 columns + 4 columns + the icon: 255 + 360 + 45 + two gutters = 780. */
.scope__row {
  display: grid;
  grid-template-columns: 255px 360px 45px;
  column-gap: var(--gutter);
  align-items: start;
}

.scope__title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--size-h3);
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.scope__desc {
  font-size: var(--size-body-m);
  line-height: 1.4;
  color: var(--color-text-secondary);
}

/* A mark rather than text, so it takes Eyebrow XL — the same token as the stat
   values — instead of tracking the headings beside it. It steps down only where
   that token does, at 767. */
.scope__icon {
  font-size: var(--size-eyebrow-xl);
  line-height: 1.4;
  text-align: right;
}

/* --- Résumé --------------------------------------------------------------- */

.resume {
  display: flex;
  flex-direction: column;
  gap: var(--space-60);
}

.resume__group {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
  max-width: var(--measure);
}

/* Eyebrow M. */
.resume__group-label {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-m);
  line-height: 1.3;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

/* Every group but the first is separated by a rule across the measure. */
.resume__group + .resume__group .resume__group-label {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-16);
  width: 100%;
}

.resume__rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-24);
}

.resume__row {
  display: grid;
  grid-template-columns: 255px minmax(0, 1fr) auto;
  column-gap: var(--gutter);
  align-items: center;
  text-decoration: none;
  color: inherit;
}

/* Years and the arrow share the last cell, so the arrow sits at the row's
   right edge without adding a track. */
.resume__end {
  display: flex;
  align-items: center;
  gap: var(--space-12);
}

/* Reserved space, so revealing it on hover doesn't shift the row. */
.resume__arrow {
  color: var(--color-text-tertiary);
  opacity: 0;
  transform: translateX(-4px);
  transition:
    opacity var(--hover-duration) var(--hover-ease),
    transform var(--hover-duration) var(--hover-ease);
}

a.resume__row:hover .resume__arrow,
a.resume__row:focus-visible .resume__arrow {
  opacity: 1;
  transform: none;
}

.resume__name-cell {
  display: flex;
  align-items: center;
  gap: var(--space-16);
}

/* The stroke sits inside the tile, as on the case study images: drawn over the
   artwork's edge by an overlay rather than around it, so a 60px logo stays 60px. */
.resume__logo {
  position: relative;
  flex: none;
  width: 60px;
  height: 60px;
  border-radius: var(--r-logo);
  overflow: hidden;
  transition: transform var(--hover-duration) var(--hover-ease);
}

/* Lifts with the row, on the same scale as a button. */
a.resume__row:hover .resume__logo,
a.resume__row:focus-visible .resume__logo {
  transform: scale(var(--hover-scale));
}

.resume__logo::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--color-border);
  border-radius: inherit;
  pointer-events: none;
}

.resume__logo > img { display: block; width: 100%; height: 100%; }

.resume__name {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--size-h4);
  line-height: 1.05;
  letter-spacing: -0.01em;
}

.resume__detail {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  line-height: 1.4;
}

.resume__role {
  font-size: var(--size-body-m);
  color: var(--color-text-secondary);
  transition: color var(--hover-duration) var(--hover-ease);
}

a.resume__row:hover .resume__role,
a.resume__row:focus-visible .resume__role { color: var(--color-text-primary); }
/* Eyebrow S. */
.resume__tags {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-s);
  line-height: 1.3;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

.resume__years {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-meta);
  letter-spacing: var(--tracking-eyebrow-meta);
  line-height: 1.3;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
  white-space: nowrap;
}

/* --- Availability --------------------------------------------------------- */
/* Full-bleed tinted band that closes every page. */

.availability {
  background: var(--color-tint);
  padding: var(--space-60) var(--page-pad) var(--space-120);
  width: 100%;
}

.availability__inner {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  width: var(--content);
  max-width: 100%;
  margin-inline: auto;
}

.availability__content {
  grid-column: 3 / span 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-24);
}

.availability__links {
  display: flex;
  align-items: center;
  gap: var(--space-8);
}

.availability__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.availability__status {
  display: flex;
  align-items: center;
  /* 12px dot + 8px gap = the 20px the note below indents to. */
  gap: var(--space-8);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--size-h4);
  line-height: 1.05;
  letter-spacing: -0.01em;
}

.availability__dot {
  flex: none;
  width: 12px;
  height: 12px;
  border-radius: var(--r-pill);
  background: var(--color-available);
  /* Centred on the line box sits high against Crimson's x-height. */
  transform: translateY(2px);
}

.availability__note {
  /* Lines up under the title's first glyph rather than under the dot. */
  padding-left: var(--space-20);
  font-size: var(--size-body-s);
  line-height: 1.4;
  color: var(--color-text-tertiary);
}

/* Copyright line under a rule, closing the page. */
.colophon {
  grid-column: 1 / -1;
  margin-top: var(--space-60);
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-16);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-16);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--size-eyebrow-s);
  line-height: 1.3;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-text-tertiary);
}

.colophon > span:last-child { text-align: right; }

/* DM Mono carries no emoji glyph, so the colour font has to be named — the
   same reason the award medals do. `inherit` is only valid as a font-family's
   sole value, so the mono stack is repeated rather than inherited. */
/* A colour emoji is composited with the text colour's alpha, so it inherits
   the colophon's tertiary and comes out washed out. Opaque ink restores it. */
.emoji {
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", var(--font-mono);
  color: var(--color-text-primary);
}

/* Optical alignment: both of these labels sit beside a display-face first
   line, whose cap-height starts lower than the mono label's. */
.availability__inner > .band__label { padding-block: var(--space-4); }
#scope > .band__label { padding-block: var(--space-8); }

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

.article-footer {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  width: 100%;
}

.article-footer p {
  font-size: var(--size-body-m);
  line-height: 1.4;
  color: var(--color-text-tertiary);
}

/* --- Reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .resume__arrow { transition: none; transform: none; }
  .resume__logo { transition: none; }
  a.resume__row:hover .resume__logo,
  a.resume__row:focus-visible .resume__logo { transform: none; }

  .button,
  .chevron,
  .card__kicker,
  .card__thumb > img,
  .card__thumb > video { transition: none; }

  /* The colour changes stay; only the movement goes. */
  .site-nav,
  .site-nav__avatar,
  .nav-swap { transition: none; }

  .site-nav a:hover > .site-nav__avatar,
  .site-nav a:focus-visible > .site-nav__avatar,
  .button:hover,
  .button:focus-visible,
  .button:hover .chevron,
  .button:focus-visible .chevron,
  .card:hover .card__thumb > img,
  .card:hover .card__thumb > video,
  .card:focus-visible .card__thumb > img,
  .card:focus-visible .card__thumb > video { transform: none; }
}

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

@media (max-width: 1439px) {
  :root { --page-pad: var(--space-60); }
}

/* Below 1320 the 1200 canvas no longer fits, so the columns compress while the
   gutters hold. The reading column's fixed 210 indent and 780 measure stop
   matching that — it ends up left-indented and right-flush rather than centred
   — so from here down it is placed on the grid: ten columns, one of shoulder
   either side. */
@media (max-width: 1319px) {
  :root { --shoulder: calc((100% - 11 * var(--gutter)) / 12 + var(--gutter)); }

  .prose {
    max-width: 100%;
    padding-left: var(--shoulder);
    padding-right: var(--shoulder);
  }
}

@media (max-width: 1024px) {
  .stats { flex-wrap: wrap; gap: var(--space-24); }
  .stat { padding-right: var(--space-24); }

  /* The twelve columns survive here — only the gutter comes down. At 60px,
     eleven of them total 660 on their own and leave nothing for the columns;
     at 24 the template still fits a tablet. */

  /* The label stops paying for its own column and stacks over the content;
     both keep the band's full width. */
  .band__label,
  .band__body,
  .availability__content { grid-column: 1 / -1; }
  .band__label { margin-bottom: var(--space-16); }

  /* Full width and on its own line, the label no longer needs its break. */
  .band__label br { display: none; }

  /* Stacked, the rule reads as a divider under the heading rather than a line
     the heading hangs below, so it moves from the band's top edge to the label.
     Availability's label sits outside .band and stays ruleless. */
  .band { border-top: 0; padding-top: 0; }
  .band > .band__label,
  #scope > .band__label {
    padding-top: 0;                       /* the optical nudge is for beside,
                                             not above, the content */
    padding-bottom: var(--space-16);
    border-bottom: 1px solid var(--color-border);
    margin-bottom: var(--space-48);
  }
  /* Cards bring their own frame, so they sit closer under the rule. */
  .band--cards > .band__label { margin-bottom: var(--space-32); }

  /* The band below splits into two halves on the same twelve columns, so the
     hero takes the same split — its intro starts where the second card does. */
  .hero__title { grid-column: 1 / span 6; }
  .hero__intro { grid-column: 7 / span 6; }

  /* Image grids tighten further than the page gutter: at 24 the tiles read as
     separate pictures rather than one grid once they are this small. */
  .media,
  .media-row { gap: var(--space-12); }

  /* Title and description stack on the left; the emoji holds the right edge
     and balances them. */
  /* The emoji column is fixed, so it is the text measure that flexes across
     the tablet range. */
  .scope__row {
    grid-template-columns: minmax(0, 1fr) 96px;
    column-gap: var(--space-20);
    row-gap: var(--space-8);
  }
  .scope__title { grid-column: 1; grid-row: 1; }
  .scope__desc { grid-column: 1; grid-row: 2; }
  .scope__icon {
    grid-column: 2;
    grid-row: 1 / span 2;   /* centred against the pair, not against one line */
    align-self: center;
  }

  /* Scope and résumé join the reading column on the same ten. */
  .scope,
  .resume {
    padding-left: var(--shoulder);
    padding-right: var(--shoulder);
  }
  /* Capped to the measure for the desktop layout; here the shoulder sets the
     width instead. */
  .scope,
  .resume__group { max-width: 100%; }

  /* Name and role split the width evenly. The years column is fixed just too
     narrow for "2024–Present", so that one breaks after its dash while the
     date ranges stay on one line. */
  .resume__row { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 92px; }
  .resume__years { white-space: normal; }
  /* The hover arrow has no room beside the years at this width. The writing row
     has no years at all, so its description takes that column too. */
  .resume__arrow { display: none; }
  .resume__end:not(:has(.resume__years)) { display: none; }
  .resume__row:not(:has(.resume__years)) .resume__detail { grid-column: 2 / -1; }

  /* Two-up cards: rows sit further apart than columns, and the text stack
     tightens — 12 from kicker to title, 8 to the description, 16 to the button. */
  .card-grid { row-gap: var(--space-48); }
  .card__text { gap: var(--space-16); }
  .card__copy { gap: var(--space-8); }
  .card__kicker { margin-bottom: var(--space-4); }

  /* The closing band evens out: the desktop 60/120 is a deliberate weight at
     full width, but at this size it just strands the page in tint. */
  .availability {
    padding-top: var(--space-48);
    padding-bottom: var(--space-48);
  }
  .colophon { margin-top: var(--space-48); }

  :root {
    --page-pad: var(--space-48);
    --gutter: var(--space-24);
    --r-out: 48px;
    /* The Figma "Tablet" mode. Anything without a mobile override below
       keeps these values on phones too. */
    --size-display: 52px;
    --size-h1: 48px;
    --size-h2: 36px;
    --size-h3: 28px;
    --size-h4: 22px;
    --size-subheadline: 20px;
    --size-body-l: 18px;
    --size-body-m: 16px;
    --size-eyebrow-xl: 32px;
    --size-quote: 24px;
    --size-caption: 14px;
  }
  .article { gap: var(--space-96); }

  /* Case studies tighten their vertical rhythm here. The homepage shares
     .article and keeps its 96. */
  .article--case-study { gap: var(--space-72); }
  .section { gap: var(--space-48); }
  .section--intro { gap: var(--space-32); }
  .prose-stack--section { gap: var(--space-32); }
  /* The intro's summary keeps the wider step down to its meta row. */
  .section--intro .prose-stack--section { gap: var(--space-48); }
}

@media (max-width: 767px) {
  :root {
    --page-pad: var(--space-24);
    --r-out: 32px;
    /* The Figma "Mobile" mode — only the sizes that differ from Tablet. */
    --size-display: 44px;
    --size-h1: 40px;
    --size-h2: 32px;
    --size-h3: 24px;
    --size-subheadline: 18px;
    --size-body-l: 16px;
    --size-body-m: 15px;
    --size-eyebrow-xl: 28px;
    --size-eyebrow-l: 20px;
    --size-button-label: 12px;
    --size-quote: 20px;
    --size-eyebrow-meta: var(--size-eyebrow-s);
    --tracking-eyebrow-meta: 0.03em;
  }

  /* .article is shared with the homepage, so its rhythm stays consistent
     across both. */
  .article { gap: var(--space-60); }
  .section { gap: var(--space-32); }
  .prose-stack--section { gap: var(--space-24); }
  .section--intro .prose-stack--section { gap: var(--space-24); }
  .body-copy { gap: var(--space-20); }

  /* The heading closes up on its rule, and the rule on its content. */
  .band > .band__label,
  #scope > .band__label {
    padding-bottom: var(--space-12);
    margin-bottom: var(--space-24);
  }

  /* The last 60s on the page come in a step: at this width they read as gaps
     rather than rhythm. */
  .resume { gap: var(--space-48); }
  .availability {
    padding-top: var(--space-32);
    padding-bottom: var(--space-32);
  }
  .colophon { margin-top: var(--space-32); }
  /* Every page gets a little more room above the footer than the side padding
     gives. max() keeps the wider 600–767 padding. */
  .page { padding-bottom: max(var(--page-pad), var(--space-32)); }

  /* Collapse the 12-column template. Even at a 24px gutter, eleven of them
     take 264px of a 320px phone — the columns can shrink, the gutters can't. */
  .grid,
  .hero,
  .availability__inner { grid-template-columns: minmax(0, 1fr); }

  .band__label,
  .band__body,
  .availability__content,
  .hero__title,
  .hero__intro { grid-column: 1 / -1; }

  .hero { row-gap: var(--space-16); }
  .hero__intro { align-self: start; }

  /* No columns left to indent against. */
  :root { --shoulder: 0px; }

  /* Grids stack, so the 60px corners move to the top and bottom of the stack.
     flex-basis has to go back to auto, or it would size tiles by height here. */
  .media-row { flex-direction: column; }
  .media-row > * { flex: none; }
  .media .tile {
    --tl: var(--r-in);
    --tr: var(--r-in);
    --br: var(--r-in);
    --bl: var(--r-in);
  }
  .media > .tile:first-child,
  .media > :first-child > .tile:first-child {
    --tl: var(--r-out);
    --tr: var(--r-out);
  }
  .media > .tile:last-child,
  .media > :last-child > .tile:last-child {
    --bl: var(--r-out);
    --br: var(--r-out);
  }

  /* Panels stop being a fixed canvas and stack their contents. */
  .panel,
  .panel--diagram,
  .tile--composed,
  .tile--emo.tile--composed {
    flex-direction: column;
    aspect-ratio: auto;
    gap: var(--space-32);
    padding: var(--space-32) var(--space-24);
  }
  /* Beats every per-variant cqw height rule above. */
  .panel .panel-item > img {
    height: auto;
    width: 100%;
    max-width: 260px;
  }
  /* A composite of several phones needs the whole column to stay legible. */
  .panel .panel-item--wide > img { max-width: none; }
  .panel--composed > .panel-item > img,
  .panel--diagram > img,
  .tile--composed > img,
  .tile--emo.tile--composed > img { width: 100%; max-width: none; }

  .tile--composed > .caption,
  .tile--emo.tile--composed > .caption { max-width: 100%; }

  /* Three pills, an avatar and two fixed 60px gutters do not fit a 320px
     phone, so the row tightens rather than scrolling sideways. */
  .site-nav { gap: var(--space-12); }
  .site-nav .button { padding: var(--space-12) var(--space-16); }
  .site-nav .button--icon { padding: var(--space-12); }
  .site-nav__avatar { width: 48px; height: 48px; }

  .card-grid { grid-template-columns: minmax(0, 1fr); gap: var(--space-48); }
  .card__thumb { border-radius: var(--space-16); }

  .availability__content { flex-direction: column; align-items: flex-start; }

  /* Too narrow to strand the emoji at the far edge; it leads the stack. */
  .scope__row { grid-template-columns: auto minmax(0, 1fr); row-gap: var(--space-4); }
  .scope__icon { grid-column: 1; }
  .scope__title,
  .scope__desc { grid-column: 2; }

  /* One stack — name, role, tags, years — with the logo alongside on the
     right. The two wrappers step out of the way with display:contents so their
     children can be placed on the row's own grid. */
  .resume__row {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: var(--space-16);
    row-gap: var(--space-4);
    align-items: start;
  }
  .resume__name-cell,
  .resume__detail { display: contents; }

  .resume__name { grid-column: 1; grid-row: 1; }
  .resume__role { grid-column: 1; grid-row: 2; }
  .resume__tags { grid-column: 1; grid-row: 3; }
  .resume__end { grid-column: 1; grid-row: 4; }

  .resume__logo {
    grid-column: 2;
    grid-row: 1 / span 4;   /* centred against the stack, not against one line */
    align-self: center;
    width: 48px;
    height: 48px;
  }

  /* The row is its own affordance at this size; the arrow just crowds it. */
  .resume__arrow { display: none; }

  .meta { grid-template-columns: 1fr; gap: var(--space-24); }
  .awards { grid-template-columns: 1fr; }
  .stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-24); }
  .stat { padding-right: var(--space-12); }
  .quote { padding: var(--space-12) var(--space-20); }
  .step-body { padding-left: 0; }
}

/* The same stacked layout, given a wider shoulder once there is room for one. */
@media (min-width: 600px) and (max-width: 767px) {
  :root { --page-pad: var(--space-48); }
}

/* The smallest screens take another step off the section rhythm. */
@media (max-width: 599px) {
  .article { gap: var(--space-48); }
  .section { gap: var(--space-24); }
}
