/* ==========================================================================
   Gauri Chakravarti — Portfolio
   ==========================================================================

   Cascade layers, lowest priority first. `legacy` is the pre-revamp
   stylesheet, kept as a floor so every page keeps rendering while the new
   system is built on top of it. Because layer order beats selector
   specificity, a flat `.callout {}` here defeats a legacy
   `body.case-study-prep .callout {}` without any prefix war.

   `legacy` shrinks to nothing by the end of the revamp — when css/legacy.css
   is empty, delete it and the @import below.

   Layer map
     tokens      design system — the only place raw values are allowed
     base        reset, document, selection, focus, motion
     layout      containers, grid, vertical rhythm, rules
     type        the type ramp and its named classes
     components  nav, hero, work, case-study internals, footer
     utilities   single-purpose escape hatches, last resort
   ========================================================================== */

@layer legacy, tokens, base, layout, type, components, utilities;
@import url("./legacy.css") layer(legacy);

/* ==========================================================================
   TOKENS
   ========================================================================== */

@layer tokens {
  :root {
    color-scheme: light;

    /* --- Typefaces -------------------------------------------------------
       Display carries personality, body carries prose, mono carries every
       label and number. Hand is the fourth and last: annotation only, and
       the landing page spends it exactly three times. Loaded by index.html
       alone — no other page marks anything up by hand. */
    --font-display: "Fraunces", Georgia, serif;
    --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    --font-mono: "Geist Mono", ui-monospace, "SF Mono", "SFMono-Regular", Menlo, monospace;
    --font-hand: "Caveat", "Bradley Hand", "Segoe Script", cursive;

    /* --- Type scale — 10 steps ------------------------------------------
       No font-size may be written outside these. The ramp is deliberately
       short and low-contrast at the small end; hierarchy comes from
       weight, colour and tracking before it comes from size. */
    --text-2xs: 0.6875rem;   /* 11 — micro label */
    --text-xs: 0.75rem;      /* 12 — eyebrow, meta, nav */
    --text-sm: 0.8125rem;    /* 13 — caption, figcaption */
    --text-base: 0.9375rem;  /* 15 — UI, secondary copy */
    --text-md: 1.0625rem;    /* 17 — body prose */
    --text-lg: 1.1875rem;    /* 19 — lede, callout */
    --text-xl: 1.375rem;     /* 22 — h3, work title */
    --text-2xl: clamp(1.5rem, 1.1rem + 1.4vw, 1.75rem);      /* 24–28 — h2 */
    --text-3xl: clamp(1.875rem, 1.3rem + 2.2vw, 2.375rem);   /* 30–38 — h1 */
    --text-4xl: clamp(2.25rem, 1.4rem + 3.4vw, 3.25rem);     /* 36–52 — home hero */
    /* Two steps matched to the case-study hero and section heads in
       v1b-mono.css, so a page can sit at that scale without a second ramp. */
    --text-display-lg: clamp(2.0625rem, 3.26vw, 3rem);       /* 33–48 — entry hero */
    --text-display-md: clamp(1.5rem, 2.3vw, 2.0625rem);      /* 24–33 — section head */

    /* --- Leading — 4 steps ---------------------------------------------- */
    --leading-tight: 1.08;
    --leading-snug: 1.25;
    --leading-normal: 1.5;
    --leading-relaxed: 1.65;
    --leading-index: 1.85;   /* optical: mono index aligned to first prose line */

    /* --- Tracking — 4 steps --------------------------------------------- */
    --tracking-label: 0.08em;
    --tracking-normal: 0;
    --tracking-tight: -0.012em;
    --tracking-display: -0.022em;

    /* --- Weights — only what is actually loaded -------------------------- */
    --weight-normal: 400;
    --weight-medium: 500;
    --weight-semi: 600;

    /* --- Space — 4px base ----------------------------------------------- */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-7: 28px;
    --space-8: 32px;
    --space-9: 36px;
    --space-10: 40px;
    --space-11: 44px;
    --space-12: 48px;
    --space-14: 56px;
    --space-16: 64px;
    --space-20: 80px;
    --space-24: 96px;
    --space-32: 128px;
    --section-y: clamp(var(--space-16), 8vw, var(--space-32));

    /* --- Colour — flat grounds, hairline separation, no gradients --------
       The grounds sit slightly warm of neutral. --ink-secondary and
       --ink-muted were already warm; this brings the rest into line so the
       page reads as paper rather than as a canvas. */
    /* A faint parchment cast, and — more importantly — real separation between
       the ground and the sheets that sit on it. At 251/255 the two were four
       levels apart, so depth had to come entirely from shadow. */
    --bg: rgb(247 243 235);
    --surface: rgb(243 239 230);
    --surface-raised: rgb(253 251 246);
    --tint: rgb(240 235 226);

    --ink: rgb(28 27 25);
    --ink-secondary: rgb(74 73 72);
    /* Was rgb(145 142 137) — 2.8:1 on --surface, under the 4.5 AA floor, and
       it carries every mono label and caption. Darkened until it clears 4.5
       on all three grounds (bg, surface, surface-raised). */
    --ink-muted: rgb(110 108 104);

    /* AI entry cards: translucent pane + soft blue bloom. */
    /* Frosted glass: the pane is mostly a white veil and the work is done by
       the blur behind it, not by tint or shadow. The blue is a hint inside the
       glass rather than a glow — at these values you should notice it only
       once you look for it. Same recipe in dark, different veil. */
    --ai-pane: rgb(250 250 253 / 0.5);
    --ai-wash: rgb(145 35 85 / 0.045);
    --ai-edge: rgb(255 255 255 / 0.72);
    --ai-edge-hover: rgb(145 35 85 / 0.16);
    --ai-sheen: rgb(255 255 255 / 0.6);
    /* Elevation comes from the edge being lit on top and grounded underneath,
       plus a hairline ring so the silhouette reads against the paper. Both
       stay under 8% — enough to place the card, not enough to draw a line. */
    --ai-ring: rgb(28 27 25 / 0.07);
    --ai-underline: rgb(28 27 25 / 0.05);
    --ai-glow: rgb(145 35 85 / 0.1);
    --ai-glow-hover: rgb(145 35 85 / 0.15);
    --ai-shadow: rgb(28 27 25 / 0.1);

    --rule: rgb(28 27 25 / 0.09);
    --rule-strong: rgb(28 27 25 / 0.16);

    --accent: rgb(41 82 255);
    --accent-ink: rgb(28 60 200);
    --accent-wash: rgb(41 82 255 / 0.08);
    --hero-pink: rgb(145 35 85);

    /* Status pairs — pills, flow states, callouts */
    --status-good-ink: rgb(52 104 66);
    --status-good-wash: rgb(52 104 66 / 0.09);
    --status-warn-ink: rgb(146 100 24);
    --status-warn-wash: rgb(146 100 24 / 0.1);
    --status-info-ink: rgb(48 88 130);
    --status-info-wash: rgb(48 88 130 / 0.09);
    --status-bad-ink: rgb(158 62 46);
    --status-bad-wash: rgb(158 62 46 / 0.09);

    /* --- Landing tiles — solid blocks, same-hue ink, no gradients ---------
       Constant across themes: these are intentional colour, not surface. */
    --tile-slate: rgb(126 151 179);  --tile-slate-ink: rgb(30 46 66);
    --tile-ochre: rgb(198 148 86);   --tile-ochre-ink: rgb(104 52 12);
    --tile-rust: rgb(180 103 74);    --tile-rust-ink: rgb(94 34 18);
    --tile-olive: rgb(163 161 119);  --tile-olive-ink: rgb(62 62 32);
    --tile-clay: rgb(185 178 166);   --tile-clay-ink: rgb(58 52 46);

    /* Dashed = structure. Solid --rule = a cut, and the landing page uses none. */
    --rule-dashed: rgb(28 27 25 / 0.16);

    --grain-opacity: 0.032;
    --grain-blend: multiply;

    /* The hero sheet is stock in its own right, so it gets a heavier fibre
       than the page grain: pulp specks plus a faint laid weave. */
    --fibre-opacity: 0.18;
    --fibre-blend: multiply;
    --fibre-laid: rgb(28 27 25 / 0.05);

    --machine-cast: rgb(28 27 25 / 0.22);
    /* The fold's height. Named on :root because two elements need it: the hero caps
       itself to it, and the sky backdrop — which lives on .fold-stage, a PARENT of
       the hero — has to match. A token on the hero would be invisible to that
       parent. */
    --fold-h: calc(100vh - 92px);
    /* How strongly the painted sky behind the fold reads. */
    --sky-strength: 0.7;
    /* Tech-stack capsules: the two ends of the hue ramp, plus the shade the
       gradient falls into at the bottom of each pill. */
    --stack-hot: rgb(236 62 148);
    --stack-cool: rgb(96 88 232);
    --stack-deep: rgb(26 16 46);
    /* Light escaping the keyboard's slot. Warm, and weak — on a cream ground a
       glow can only be a little lighter than the paper before it reads as haze. */
    --slot-glow: rgb(255 224 172 / 0.6);
    --slot-glow-far: rgb(255 214 168 / 0.26);
    /* Prism fringes. Offset either side of the warm core so the light splits at
       its shoulders instead of the whole glow turning a colour. Pink leans on
       --hero-pink's family so it reads as the same palette. */
    --slot-pink: rgb(255 158 198 / 0.5);
    --slot-cool: rgb(152 182 255 / 0.42);
    /* normal, not screen. Screening a pale colour onto a near-white ground is
       almost a no-op — measured R-B of 12 over the paper, i.e. invisible. On a
       light ground a glow has to read as a warm TINT, painted over. */
    --slot-blend: normal;
    --paper-lit-pink: rgb(255 214 228 / 0.2);
    --paper-lit-cool: rgb(214 224 255 / 0.16);
    --paper-through: rgb(255 236 216 / 0.26);
    /* The aura around the sheet — the shaft seen where the paper is not. --aura is
       the source; the three far-field hues are what is left of it after diffusing
       the height of the page, so they are a fraction of its strength and split into
       separate colours rather than staying one tint. */
    --aura: rgb(255 210 174 / 0.42);
    --aura-pink: rgb(255 188 212 / 0.17);
    --aura-warm: rgb(255 224 186 / 0.15);
    --aura-cool: rgb(198 214 255 / 0.14);

    /* --- Layout ---------------------------------------------------------- */
    --col: 44rem;        /* 704 — the editorial column */
    --col-narrow: 34rem; /* 544 — dense prose */
    --col-wide: 66rem;   /* 1056 — media, work rows */
    --measure: 68ch;
    --gutter: var(--space-6);
    --nav-height: 64px;
    /* The nav is a floating pill, not a full-bleed bar: it is inset from the
       viewport on all sides, so anything clearing it must clear the inset too
       (see --nav-clear). */
    --nav-inset: var(--space-6);
    --nav-inset-x: var(--space-4);
    --nav-clear: calc(var(--nav-height) + var(--nav-inset));
    --radius: 10px;
    --radius-sm: 6px;
    --radius-nav: 20px;
    --radius-pill: 999px;

    --shadow-lift: 0 6px 18px -12px rgb(20 20 24 / 0.3);
    --shadow-nav: none;
    --nav-glass: rgb(253 251 246 / 0.68);
    --nav-glass-strong: rgb(253 251 246 / 0.82);
    --nav-glass-border: rgb(28 27 25 / 0.08);
    --nav-backdrop: saturate(145%);
    --dot: rgb(28 27 25 / 0.12);
    /* The closing strip has its own pair rather than reusing --ink/--bg: those
       invert with the theme, which turned the strip into a cream slab in dark
       mode. A grey holds its role in both. */
    --closing-bg: rgb(58 56 53);
    --closing-ink: rgb(247 246 243);

    /* --- Footer ground ----------------------------------------------------
       Craft's construction: a warm cream slab with a saturated aurora bloomed
       across it and a near-white logotype cropped on the bottom edge. The
       aurora is --stack-hot, the hero's own pink, run at full strength so the
       wordmark reads white against it and disappears into the cream elsewhere.
       Ink stays the page's own ink, so contrast is the same as body copy. */
    --foot-bg: rgb(247 243 235);
    --foot-bg-2: rgb(247 243 235);
    /* One hue, low opacity. The saturated pink and the violet both went: two
       colours plus a blue made the band the loudest thing on a quiet page. */
    --foot-glow-pink: rgb(255 188 212 / 0.34);
    --foot-glow-hot: rgb(255 205 224 / 0.26);
    --foot-dot: rgb(28 27 25 / 0.07);
    --foot-ink: rgb(28 27 25);
    --foot-ink-2: rgb(74 73 72);
    --foot-eyebrow: rgb(145 35 85);
    /* White only worked while the pink behind it was saturated. At this
       opacity the ground is barely tinted, so the wordmark has to carry its own
       colour — the same pink, kept faint.

       Raised from 0.3 once the painted sky went in behind it: at that alpha the
       cloud detail read straight through the strokes and the word broke up.
       Deeper hue as well, so it holds over the light cloud tops. */
    --foot-wordmark: rgb(203 106 148 / 0.46);
    --foot-rule: rgb(28 27 25 / 0.12);
    --foot-rule-strong: rgb(28 27 25 / 0.3);

    /* --- Motion ----------------------------------------------------------- */
    --ease: cubic-bezier(0.16, 1, 0.3, 1);
    --dur-fast: 140ms;
    --dur: 220ms;
    --dur-slow: 420ms;
  }

  /* Wider gutter once there is room for it. */
  @media (min-width: 900px) {
    :root {
      --gutter: var(--space-8);
    }
  }

  /* Vivid accent on displays that can show it. No gradients anywhere.
     The two live tiles are pushed just past the sRGB gamut here — same hue,
     more saturation, only where the display can render it. */
  @supports (color: color(display-p3 1 1 1)) {
    :root {
      --accent: color(display-p3 0.22 0.34 1);
      --accent-ink: color(display-p3 0.16 0.26 0.78);

      --tile-slate: color(display-p3 0.47 0.59 0.71);
      --tile-ochre: color(display-p3 0.79 0.57 0.31);
    }
  }

  html[data-theme="dark"] {
    color-scheme: dark;

    --bg: rgb(16 15 14);
    --surface: rgb(23 22 20);
    --surface-raised: rgb(29 28 26);
    --tint: rgb(32 31 28);

    --ink: rgb(241 239 235);
    --ink-secondary: rgb(178 176 172);
    /* Was rgb(129 126 121) — 4.2:1 on the raised surface. Lightened to clear
       4.5 on all three dark grounds. */
    --ink-muted: rgb(134 132 127);

    /* Same frost, inverted: a thin white veil over the dark ground rather than
       a dark panel, so the glass reads the same way in both themes. */
    --ai-pane: rgb(255 255 255 / 0.055);
    --ai-wash: rgb(232 113 162 / 0.05);
    --ai-edge: rgb(255 255 255 / 0.14);
    --ai-edge-hover: rgb(232 113 162 / 0.22);
    --ai-sheen: rgb(255 255 255 / 0.1);
    --ai-ring: rgb(0 0 0 / 0.28);
    --ai-underline: rgb(0 0 0 / 0.22);
    --ai-glow: rgb(232 113 162 / 0.12);
    --ai-glow-hover: rgb(232 113 162 / 0.2);
    --ai-shadow: rgb(0 0 0 / 0.3);

    --rule: rgb(241 239 235 / 0.11);
    --rule-strong: rgb(241 239 235 / 0.2);

    --accent: rgb(126 156 255);
    --accent-ink: rgb(163 185 255);
    --accent-wash: rgb(126 156 255 / 0.13);
    --hero-pink: rgb(232 113 162);

    --status-good-ink: rgb(139 190 150);
    --status-good-wash: rgb(139 190 150 / 0.13);
    --status-warn-ink: rgb(219 175 105);
    --status-warn-wash: rgb(219 175 105 / 0.13);
    --status-info-ink: rgb(140 178 219);
    --status-info-wash: rgb(140 178 219 / 0.13);
    --status-bad-ink: rgb(224 138 118);
    --status-bad-wash: rgb(224 138 118 / 0.13);

    --rule-dashed: rgb(241 239 235 / 0.18);

    --grain-opacity: 0.02;
    --grain-blend: screen;

    --fibre-opacity: 0.1;
    --fibre-blend: screen;
    --fibre-laid: rgb(241 239 235 / 0.045);

    --machine-cast: rgb(0 0 0 / 0.6);
    --sky-strength: 0.4;
    --stack-hot: rgb(246 78 162);
    --stack-cool: rgb(120 112 246);
    --stack-deep: rgb(18 11 34);
    /* Dark has the range to let it actually glow, so it carries further. */
    --slot-glow: rgb(255 206 130 / 0.9);
    --slot-glow-far: rgb(255 194 122 / 0.34);
    --slot-pink: rgb(255 122 182 / 0.72);
    --slot-cool: rgb(132 166 255 / 0.62);
    /* Dark has black to add light to, so screen is right here and can be strong. */
    --slot-blend: screen;
    /* Kept low on purpose: the rail links sit only ~15% up the sheet, so this is
       painted behind live text and its contrast is a checked constraint, not a
       matter of taste. The glow in the gap carries the effect; the paper only
       hints at it. */
    --paper-lit-pink: rgb(255 172 204 / 0.1);
    --paper-lit-cool: rgb(158 182 255 / 0.075);
    --paper-through: rgb(255 214 176 / 0.115);
    --aura: rgb(255 200 150 / 0.46);
    --aura-pink: rgb(255 150 195 / 0.15);
    --aura-warm: rgb(255 204 148 / 0.13);
    --aura-cool: rgb(150 180 255 / 0.13);

    --shadow-lift: 0 6px 18px -12px rgb(0 0 0 / 0.7);
    --shadow-nav: none;
    --nav-glass: rgb(26 25 23 / 0.66);
    --nav-glass-strong: rgb(26 25 23 / 0.78);
    --nav-glass-border: rgb(241 239 235 / 0.1);
    --nav-backdrop: saturate(140%);
    /* A dark ground needs less pattern to read the same. */
    --dot: rgb(241 239 235 / 0.07);
    /* Raised slightly off --bg so it reads as a panel, not a hole. */
    --closing-bg: rgb(38 37 34);
    --closing-ink: rgb(241 239 235);

    /* Same construction, ground inverted: the aurora has to come down or it
       blazes on near-black, and the wordmark drops to a whisper. */
    --foot-bg: rgb(16 15 14);
    --foot-bg-2: rgb(16 15 14);
    --foot-glow-pink: rgb(255 150 195 / 0.16);
    --foot-glow-hot: rgb(255 180 210 / 0.12);
    --foot-dot: rgb(241 239 235 / 0.05);
    --foot-ink: rgb(241 239 235);
    --foot-ink-2: rgb(178 176 172);
    --foot-eyebrow: rgb(255 150 194);
    --foot-wordmark: rgb(255 190 215 / 0.26);
    --foot-rule: rgb(241 239 235 / 0.12);
    --foot-rule-strong: rgb(241 239 235 / 0.28);

    /* Tiles are deliberate colour, but the light-mode values blaze against a
       near-black page. Same hues, taken down; the ink flips light to keep
       contrast once the ground is dark. Higher specificity than the :root
       display-p3 block above, so these win on wide-gamut displays too. */
    --tile-slate: rgb(52 70 92);     --tile-slate-ink: rgb(226 236 248);
    --tile-ochre: rgb(104 74 36);    --tile-ochre-ink: rgb(248 234 214);
    --tile-rust: rgb(96 51 36);      --tile-rust-ink: rgb(250 228 220);
    --tile-olive: rgb(78 77 52);     --tile-olive-ink: rgb(238 238 218);
    --tile-clay: rgb(64 61 56);      --tile-clay-ink: rgb(236 232 225);
  }

  @supports (color: color(display-p3 1 1 1)) {
    html[data-theme="dark"] {
      --accent: color(display-p3 0.52 0.62 1);
      --accent-ink: color(display-p3 0.66 0.74 1);
    }
  }
}

/* ==========================================================================
   BASE
   ========================================================================== */

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

  html {
    -webkit-text-size-adjust: 100%;
    scroll-padding-top: calc(var(--nav-clear) + var(--space-6));
  }

  body {
    margin: 0;
    background: var(--bg);
    color: var(--ink-secondary);
    font-family: var(--font-body);
    font-size: var(--text-md);
    line-height: var(--leading-relaxed);
    letter-spacing: var(--tracking-tight);
    font-synthesis-weight: none;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
  }

  /* Paper grain. Subtle enough to read as texture, not noise. */
  body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 999;
    pointer-events: none;
    opacity: var(--grain-opacity);
    mix-blend-mode: var(--grain-blend);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  }

  img,
  video {
    display: block;
    max-width: 100%;
    height: auto;
  }

  a {
    color: inherit;
  }

  ::selection {
    background: var(--accent-wash);
    color: var(--ink);
  }

  :focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: var(--radius-sm);
  }

  /* Nothing on this site depends on motion to be legible. */
  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }

    [data-reveal] {
      opacity: 1 !important;
      transform: none !important;
    }
  
}

}

/* ==========================================================================
   LAYOUT
   ========================================================================== */

@layer layout {
  .container {
    width: 100%;
    max-width: var(--col-wide);
    margin-inline: auto;
    padding-inline: var(--gutter);
  }

  /* Legacy modifier, retargeted rather than removed — 58 call sites. */
  .container--media {
    max-width: var(--col-wide);
  }

  .container--narrow {
    max-width: var(--col);
  }

  section {
    padding-block: var(--section-y);
  }

  section.tight {
    padding-block: calc(var(--section-y) * 0.62);
  }

  .section-border-top {
    border-top: 1px solid var(--rule);
  }

  .stack {
    display: flow-root;
  }

  /* Reveal-on-scroll. `main.js` adds .is-visible; the <noscript> in each
     page neutralises this so copy is never hidden when JS fails. */
  [data-reveal] {
    opacity: 0;
    transform: translateY(12px);
    transition:
      opacity var(--dur-slow) var(--ease),
      transform var(--dur-slow) var(--ease);
  }

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

}

/* ==========================================================================
   TYPE
   ========================================================================== */

@layer type {
  h1,
  h2,
  h3,
  h4,
  h5 {
    margin: 0;
    color: var(--ink);
    font-family: var(--font-display);
    font-weight: var(--weight-normal);
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-display);
    font-variation-settings: "opsz" 40;
  }

  p {
    margin: 0;
  }

  /* --- Display ---------------------------------------------------------- */

  .display-1 {
    font-size: var(--text-3xl);
    line-height: var(--leading-tight);
    font-variation-settings: "opsz" 96;
  }

  .display-2 {
    font-size: var(--text-2xl);
  }

  .display-3 {
    font-size: var(--text-xl);
  }

  /* --- Labels — mono, uppercase, tracked -------------------------------- */

  .eyebrow {
    display: block;
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    line-height: var(--leading-normal);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  /* The legacy eyebrow drew a 14px rule before the text. The new system
     separates with space, not strokes. */
  .eyebrow::before {
    content: none;
  }

  /* --- Prose ------------------------------------------------------------ */

  .subtitle {
    max-width: var(--measure);
    color: var(--ink-secondary);
    font-size: var(--text-lg);
    line-height: var(--leading-relaxed);
    letter-spacing: var(--tracking-tight);
  }

  .section-intro {
    max-width: var(--measure);
    color: var(--ink-secondary);
    font-size: var(--text-md);
    line-height: var(--leading-relaxed);
  }

  .metric-line {
    color: var(--ink);
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-style: normal;
    font-weight: var(--weight-normal);
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-display);
  }

  /* --- Section head ----------------------------------------------------- */

  .section-head {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    max-width: var(--measure);
    margin-bottom: var(--space-12);
  }

  .section-head .eyebrow {
    margin-bottom: calc(var(--space-1) * -1);
  }

}

/* ==========================================================================
   COMPONENTS
   ========================================================================== */

@layer components {

  /* --- Navigation ------------------------------------------------------- */

  /* A floating pill rather than a full-bleed bar: the wrapper is a transparent
     positioning shell and the pill itself is .nav > .container, so the page
     scrolls visibly past it on all four sides. */
  .nav {
    position: fixed;
    inset: 0 0 auto;
    z-index: 100;
    padding: var(--nav-inset) var(--nav-inset-x) var(--space-4);
    background: none;
    border: 0;
    pointer-events: none;
  }

  /* Case studies only — main.js adds this once the hero clears the viewport, and
     removes it on the way back up. .page-outline takes over as the navigation
     past the fold.

     visibility, not opacity alone: an opacity-0 bar still takes tab focus, which
     would strand keyboard users on links they cannot see. It is delayed by --dur
     on the way out so the fade can finish, and snaps back instantly on return
     because visibility is not an interpolable property.

     The pill inside re-declares pointer-events: auto, so the shell's `none` above
     is not enough on its own to make this inert. */
  .nav.is-retired {
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px);
    pointer-events: none;
  }

  .nav.is-retired > .container {
    pointer-events: none;
  }

  .case-study-prep .nav {
    transition:
      opacity var(--dur) var(--ease),
      transform var(--dur) var(--ease),
      visibility 0s linear var(--dur);
  }

  .case-study-prep .nav:not(.is-retired) {
    transition:
      opacity var(--dur) var(--ease),
      transform var(--dur) var(--ease),
      visibility 0s;
  }

  @media (prefers-reduced-motion: reduce) {
    .case-study-prep .nav,
    .case-study-prep .nav:not(.is-retired) {
      transition: none;
    }
  }

  .nav > .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-6);
    min-height: var(--nav-height);
    padding: var(--space-3) var(--space-6);
    border-radius: var(--radius-nav);
    border: 1px solid var(--nav-glass-border);
    background: var(--nav-glass);
    -webkit-backdrop-filter: var(--nav-backdrop);
    backdrop-filter: var(--nav-backdrop);
    box-shadow: var(--shadow-nav);
    pointer-events: auto;
    transition:
      background-color var(--dur) var(--ease),
      border-color var(--dur) var(--ease);
  }

  .nav.is-scrolled > .container {
    background: var(--nav-glass-strong);
  }

  .nav-logo {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: var(--weight-semi);
    letter-spacing: 0.01em;
    text-transform: none;
    text-decoration: none;
  }

  .nav-logo span {
    display: inline-flex;
    align-items: center;
    min-height: 1.2em;
    padding-left: var(--space-2);
    border-left: 1px solid var(--rule);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-style: normal;
    font-weight: var(--weight-medium);
    letter-spacing: calc(var(--tracking-label) * 0.92);
    text-transform: uppercase;
  }

  .nav-actions {
    display: flex;
    align-items: center;
    gap: var(--space-6);
  }

  .nav-links {
    display: flex;
    align-items: center;
    gap: var(--space-5);
  }

  /* Quiet utility copy rather than a second headline: the nav now reads as
     orientation, while the editorial voice stays in the page content. */
  .nav-links a {
    position: relative;
    color: color-mix(in srgb, var(--ink) 76%, var(--ink-muted));
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    letter-spacing: 0.01em;
    text-transform: none;
    text-decoration: none;
    transition: color var(--dur-fast) var(--ease);
  }

  .nav-links a::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -0.45rem;
    height: 1px;
    border-radius: 999px;
    background: currentColor;
    opacity: 0.56;
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--dur) var(--ease), opacity var(--dur-fast) var(--ease);
  }

  .nav-links a:hover,
  .nav-links a[aria-current="page"] {
    color: var(--ink);
  }

  .nav-links a:hover::after,
  .nav-links a[aria-current="page"]::after {
    opacity: 0.88;
    transform: scaleX(1);
  }

  /* The one primary action in the chrome. It opts out of the shared nav-link
     treatment entirely: the sliding underline is an indicator for peer links, and
     on a filled pill it would draw a bar across the inside of the button. */
  .nav-links a.nav-cta {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    height: 30px;
    padding: 0 var(--space-4);
    border: 1px solid var(--ink);
    border-radius: var(--radius-pill);
    background: var(--ink);
    color: var(--surface-raised);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    transition:
      background-color var(--dur-fast) var(--ease),
      border-color var(--dur-fast) var(--ease),
      color var(--dur-fast) var(--ease),
      transform var(--dur-fast) var(--ease);
  }

  .nav-links a.nav-cta::after {
    content: none;
  }

  .nav-links a.nav-cta span {
    transition: transform var(--dur-fast) var(--ease);
  }

  .nav-links a.nav-cta:hover {
    background: var(--hero-pink);
    border-color: var(--hero-pink);
    color: var(--surface-raised);
  }

  .nav-links a.nav-cta:hover span {
    transform: translateX(2px);
  }

  @media (prefers-reduced-motion: reduce) {
    .nav-links a.nav-cta,
    .nav-links a.nav-cta span {
      transition: none;
    }
  }

  /* --- Tech stack: overlapping capsules ---------------------------------- */

  /* Eight capsules that overlap only read as one object in a single row, and at
     this size the row is ~200px wide, so it always fits the text column — no
     scrolling, and crucially no clipping: the labels are absolutely positioned and
     the widest ("Visual Studio Code") reaches past the last pill, which an
     overflow-x on this element would cut in half. Left-aligned, because it sits
     under left-aligned copy. */
  .stack-rail {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .stack-pill {
    /* --i is the pill's index, set inline. Everything positional derives from it:
       the hue, the stacking order, and nothing else has to be hand-tuned. */
    /* Bubbles: round, so the row is only as tall as it is per-item wide. The tall
       capsule version stood 62px; this is 34, which is most of what the fold gets
       back. */
    --pill-w: clamp(1.75rem, 2.2vw, 2.15rem);
    --overlap: 0.3;
    /* The pill's place in the hue ramp, named once so the tint, the rim and the
       bloom all read from the same value. */
    --pill-hue: color-mix(in oklab, var(--stack-hot), var(--stack-cool) calc(var(--i) * 13%));

    position: relative;
    flex: none;
    width: var(--pill-w);
    aspect-ratio: 1;
    margin-left: calc(var(--pill-w) * var(--overlap) * -1);
    /* Left-most on top, matching the way a fanned deck is held. */
    z-index: calc(20 - var(--i));
    transition: transform var(--dur) var(--ease);
  }

  .stack-pill:first-child {
    margin-left: 0;
  }

  /* The capsule. Two gradients: the body, whose hue is interpolated from the
     pill's index so the row ramps hot pink to violet, and a highlight down the
     upper left that gives the glass its curvature. */
  .stack-pill-face {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    border-radius: var(--radius-pill);
    /* Glass: a wash of the pill's hue over whatever is behind it, not a solid fill.
       The tint has to stay this light — the marks inside are now in their own brand
       colours, and a saturated ground would fight every one of them. */
    background:
      /* Specular highlight, upper left, the way light lands on a bubble. */
      /* Pulled up and left of centre so it lights the bubble's shoulder rather than
         sitting on top of the mark. */
      radial-gradient(
        54% 54% at 29% 22%,
        rgb(255 255 255 / 0.44) 0%,
        transparent 60%
      ),
      /* Lighter than it looks like it should be. The marks sit in brand colours on
         top of this, and every extra percent of tint is contrast taken off them. */
      linear-gradient(
        162deg,
        color-mix(in srgb, var(--pill-hue) 16%, transparent) 0%,
        color-mix(in srgb, var(--pill-hue) 7%, transparent) 62%,
        color-mix(in srgb, var(--stack-deep) 8%, transparent) 100%
      );
    -webkit-backdrop-filter: blur(6px) saturate(1.3);
    backdrop-filter: blur(6px) saturate(1.3);
    /* Rim, then a whisper of the hue around it. Much less bloom than the solid
       version needed: glass catches light at its edge, it does not emit. */
    box-shadow:
      inset 0 0 0 1px rgb(255 255 255 / 0.55),
      inset 0 1px 0 rgb(255 255 255 / 0.7),
      0 0 0 1px color-mix(in srgb, var(--pill-hue) 34%, transparent),
      0 6px 12px -10px rgb(0 0 0 / 0.45);
  }

  /* Official colours. Figma carries its own fills inside the symbol; the rest are
     currentColor marks picking up the brand hues already defined for .card-tool
     further up this file, via data-tool on each pill. */
  .stack-mark {
    width: 50%;
    height: auto;
    fill: currentColor;
  }

  /* Stand-in for the two marks the repo does not have yet. A letter, not an
     approximated logo. */
  .stack-mark--letter {
    font-family: var(--font-mono);
    font-size: calc(var(--pill-w) * 0.4);
    font-weight: 500;
    line-height: 1;
    color: var(--ink-secondary);
  }

  /* Always in the accessibility tree — faded, not hidden, so the pill has a name
     without a second visually-hidden copy of it. */
  .stack-pill-label {
    position: absolute;
    top: calc(100% + var(--space-2));
    left: 50%;
    transform: translateX(-50%);
    white-space: nowrap;
    font-family: var(--font-mono);
    font-size: 0.66rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-muted);
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease);
    pointer-events: none;
  }

  .stack-pill:hover,
  .stack-pill:focus-within {
    transform: translateY(-6px);
  }

  .stack-pill:hover .stack-pill-label,
  .stack-pill:focus-within .stack-pill-label {
    opacity: 1;
  }

  @media (prefers-reduced-motion: reduce) {
    .stack-pill { transition: none; }
    .stack-pill:hover,
    .stack-pill:focus-within { transform: none; }
  }

  /* --- Theme toggle — segmented pill ------------------------------------ */

  .theme-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    height: 30px;
    padding: 0 var(--space-2) 0 var(--space-1);
    border: 1px solid color-mix(in srgb, var(--nav-glass-border) 78%, transparent);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--nav-glass-strong) 86%, transparent);
    color: var(--ink-secondary);
    cursor: pointer;
    transition:
      background-color var(--dur-fast) var(--ease),
      border-color var(--dur-fast) var(--ease),
      color var(--dur-fast) var(--ease);
  }

  .theme-toggle:hover {
    border-color: var(--rule-strong);
    background: color-mix(in srgb, var(--nav-glass-strong) 94%, transparent);
    color: var(--ink);
  }

  /* Pink, not the blue --accent: this sits in the nav a few pixels from the
     hero, where the pink runs the whole page — the stack rail, the paper glow,
     the marked text. --stack-hot is the same hot pink the first stack bubble
     uses, so the toggle joins that family instead of introducing a second
     accent hue. It carries its own dark-mode value, so the dot stays legible
     on the dark nav without a separate override here. */
  .theme-toggle-track {
    position: relative;
    width: 26px;
    height: 15px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--stack-hot) 12%, transparent);
    transition: background var(--dur) var(--ease);
  }

  .theme-toggle-thumb {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 11px;
    height: 11px;
    border-radius: 50%;
    background: var(--stack-hot);
    transition: transform var(--dur) var(--ease);
  }

  html[data-theme="dark"] .theme-toggle-thumb {
    transform: translateX(11px);
  }

  .theme-toggle-text {
    min-width: 4ch;
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-align: left;
    text-transform: uppercase;
  }

  /* --- Mobile nav -------------------------------------------------------- */

  .nav-toggle {
    display: none;
    position: relative;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 1px solid var(--rule);
    border-radius: var(--radius-sm);
    background: transparent;
    cursor: pointer;
  }

  .nav-toggle span,
  .nav-toggle::before,
  .nav-toggle::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    width: 14px;
    height: 1.5px;
    margin-left: -7px;
    margin-top: -0.75px;
    border-radius: 2px;
    background: var(--ink);
    transition: transform var(--dur) var(--ease), opacity var(--dur-fast) var(--ease);
  }

  /* Offset from the box's CENTRE, not from its top. The mobile pass grows this
     control to 44px for the thumb, and tops tuned for the 32px box (11/15/19)
     left the three bars sitting high in the larger one with the weight of the
     icon above the middle. Centre-relative offsets hold at any box size. */
  .nav-toggle::before { transform: translateY(-4px); }
  .nav-toggle::after  { transform: translateY(4px); }

  .nav-toggle.is-open::before { transform: rotate(45deg); }
  .nav-toggle.is-open span    { opacity: 0; }
  .nav-toggle.is-open::after  { transform: rotate(-45deg); }

  @media (max-width: 640px) {
    .nav-toggle { display: block; }

    /* Hangs below the floating pill and matches its shape, instead of the
       full-bleed sheet the old bar dropped. */
    .nav-links {
      position: fixed;
      inset: calc(var(--nav-clear) + var(--space-2)) var(--nav-inset-x) auto;
      flex-direction: column;
      align-items: flex-start;
      gap: var(--space-5);
      padding: var(--space-6);
      border: 1px solid var(--nav-glass-border);
      border-radius: var(--radius-nav);
      background: var(--nav-glass-strong);
      -webkit-backdrop-filter: var(--nav-backdrop);
      backdrop-filter: var(--nav-backdrop);
      box-shadow: var(--shadow-nav);
      transform: translateY(-8px);
      opacity: 0;
      pointer-events: none;
      transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
    }

    .nav-links a {
      font-size: var(--text-base);
    }

    .nav-links.is-open {
      transform: none;
      opacity: 1;
      pointer-events: auto;
    }
  
}

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

  .hero {
    padding-top: calc(var(--nav-clear) + clamp(var(--space-16), 10vw, var(--space-32)));
    padding-bottom: var(--space-4);
    text-align: left;
  }

  /* --- Media blocks -------------------------------------------------------
     One filled, rounded, tinted shape. No dashed borders, no shadows. */

  .img-placeholder {
    display: block;
    overflow: hidden;
    min-height: 0;
    padding: 0;
    gap: 0;
    border: 0;
    border-radius: var(--radius);
    background: var(--tint);
    aspect-ratio: 16 / 9;
  }

  .img-placeholder.wide { aspect-ratio: 2 / 1; }
  .img-placeholder.tall { aspect-ratio: 4 / 3; }

  .img-placeholder.natural {
    aspect-ratio: auto;
    height: auto;
  }

  .img-placeholder img,
  .img-placeholder video {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .img-placeholder.natural img,
  .img-placeholder.natural video {
    height: auto;
    object-fit: contain;
  }

  .img-placeholder img,
  .img-placeholder video {
    transition: transform var(--dur-slow) var(--ease);
  }

  /* --- Back link ----------------------------------------------------------- */

  .back-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    text-decoration: none;
    transition: gap var(--dur) var(--ease), color var(--dur-fast) var(--ease);
  }

  .back-link:hover {
    gap: var(--space-3);
    color: var(--ink);
  }

  /* --- Proof split: the session on the left, the prompt on the right --------- */

  .proof-split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-6);
    align-items: start;
    margin-top: var(--space-10);
  }

  .proof-shot {
    margin: 0;
    border: 1px solid var(--rule-dashed);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--surface-raised);
  }

  .proof-shot img { display: block; width: 100%; height: auto; }

  .proof-shot figcaption {
    padding: 14px 18px 18px;
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--ink-muted);
  }

  .proof-split .code-block { margin-top: 0; }

  /* The code column matches the screenshot's height exactly and the excerpt
     scrolls inside it, so neither column leaves a trailing gap. height:100% on
     the grid item alone could not do it: the item sizes the row from its own
     content first, so the percentage resolved against a row the tall <pre> had
     already stretched. .code-fit takes the block out of flow so the row is the
     screenshot's height and the excerpt fills it. */
  .code-fit {
    position: relative;
    align-self: stretch;
    min-width: 0;
    min-height: 320px;
  }

  /* bottom:auto + max-height, not a full inset: the block is CAPPED by its
     neighbour, never stretched to it, so a short excerpt keeps its own height
     instead of opening a card of dead space. */
  .code-fit > .code-block {
    position: absolute;
    inset: 0 0 auto 0;
    max-height: 100%;
  }

  /* The filename bar keeps its height; only the excerpt below it scrolls. */
  .code-fit > .code-block > .code-head { flex: 0 0 auto; }

  .code-fit > .code-block {
    height: auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  .code-fit > .code-block > pre {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    overflow: auto;
    overscroll-behavior: contain;
  }

  .proof-foot {
    grid-column: 1 / -1;
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--ink-muted);
    max-width: 78ch;
  }

  @media (max-width: 900px) {
    .proof-split { grid-template-columns: minmax(0, 1fr); gap: var(--space-8); }
    .code-fit > .code-block > pre { max-height: 380px; }

    /* Stacked there is no left column to match, and an out-of-flow block in a
       collapsed wrapper would have no height at all. */
    .code-fit { position: static; min-height: 0; }
    .code-fit > .code-block { position: static; }
  }

  /* --- Code / plan excerpt --------------------------------------------------- */

  .code-block {
    margin-top: var(--space-10);
    background: var(--surface-raised);
    border: 1px solid var(--rule-dashed);
    border-radius: var(--radius-sm);
    overflow: hidden;
  }

  .code-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 18px;
    border-bottom: 1px solid var(--rule-dashed);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--ink-muted);
  }

  .code-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--ink-muted);
    opacity: 0.5;
    flex: none;
  }

  .code-block > pre {
    margin: 0;
    padding: 22px 18px;
    overflow-x: auto;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.7;
    color: var(--ink-2, var(--ink));
    white-space: pre;
  }

  .code-foot {
    padding: 0 18px 20px;
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--ink-muted);
  }

  /* --- Prototype embed ------------------------------------------------------ */

  .proto-embed {
    display: block;
    margin: 0;
  }

  .proto-embed-frame {
    overflow: hidden;
    border: 1px solid var(--rule-dashed);
    border-radius: var(--radius);
    background: var(--surface, #fff);
  }

  .proto-embed iframe {
    display: block;
    width: 100%;
    height: 680px;
    border: 0;
  }

  .proto-embed figcaption {
    margin-top: var(--space-3);
  }

  @media (max-width: 760px) {
    .proto-embed iframe { height: 560px; }
  }

  /* --- Prototype link ------------------------------------------------------- */

  .proto-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding-block: var(--space-3);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    text-decoration: none;
    border-bottom: 1px solid currentColor;
    transition: gap var(--dur) var(--ease), color var(--dur-fast) var(--ease);
  }

  .proto-link::after {
    content: '→';
    transition: transform var(--dur) var(--ease);
  }

  .proto-link:hover {
    gap: var(--space-3);
    color: var(--ink);
  }

  .proto-link:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 3px;
  }

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

  .site-footer {
    padding-block: var(--space-16) var(--space-20);
    border-top: 1px solid var(--rule);
  }

  .site-footer > .container {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-6);
  }

  .footer-note {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .footer-links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-6);
  }

  .footer-links a {
    color: var(--ink-secondary);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-normal);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    text-decoration: none;
    transition: color var(--dur-fast) var(--ease);
  }

  .footer-links a:hover {
    color: var(--ink);
  }

}

/* ==========================================================================
   LEGACY SUPPRESSION
   --------------------------------------------------------------------------
   Decorative chrome from the old design that the new system replaces with
   space and hairlines. Listed explicitly rather than deleted piecemeal from
   legacy.css so the removal is reviewable in one place. Goes away with the
   legacy layer.
   ========================================================================== */

@layer components {
  /* Rounded "frame" panels behind every hero. */
  .hero::before,
  .hero::after,
  .hero-home::before,
  .hero-home::after {
    content: none;
  }

  /* The old hero centred itself; the new one is left-aligned editorial. */
  .hero-home,
  .about-page .hero,
  .case-study-prep .hero {
    align-items: flex-start;
    text-align: left;
  }

  .case-study-prep .hero h1,
  .about-page .hero h1 {
    max-width: 22ch;
  }

}

/* ==========================================================================
   CASE STUDY
   --------------------------------------------------------------------------
   Same editorial column as the home page. Panels become hairline-bounded
   blocks instead of filled cards; every label is mono; every number is
   Fraunces. Selectors are flat — the legacy `body.case-study-prep` prefixes
   lose on layer order.
   ========================================================================== */

@layer components {

  /* --- Shell + sticky outline -------------------------------------------- */

  .page-shell {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
    width: 100%;
    max-width: var(--col-wide);
    margin-inline: auto;
    padding-inline: var(--gutter);
  }

  .page-shell > .container,
  .page-shell-content > .container,
  .page-shell .container {
    max-width: none;
    padding-inline: 0;
  }

  .page-outline {
    display: none;
  }

  @media (min-width: 1200px) {
    .page-shell {
      grid-template-columns: 168px minmax(0, 1fr);
      gap: 0 var(--space-16);
      max-width: calc(var(--col-wide) + 168px + var(--space-16));
    }

    .page-outline {
      display: block;
      position: sticky;
      top: calc(var(--nav-clear) + var(--space-12));
      align-self: start;
      max-height: calc(100vh - var(--nav-clear) - var(--space-16));
      overflow-y: auto;
    }
  
}

  .page-outline-top {
    display: block;
    margin-bottom: var(--space-5);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--rule);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    text-decoration: none;
  }

  .page-outline ul {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .page-outline a {
    display: block;
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.04em;
    text-decoration: none;
    text-transform: uppercase;
    transition: color var(--dur-fast) var(--ease);
  }

  .page-outline a::before {
    content: none;
  }

  .page-outline a:hover { color: var(--ink-secondary); }
  .page-outline a.is-active { color: var(--ink); }

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

  .case-study-prep .hero,
  .about-page .hero {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-5);
  }

  .case-study-prep .hero .back-link,
  .about-page .hero .back-link {
    margin-bottom: var(--space-6);
  }

  .case-study-prep .hero {
    gap: var(--space-4);
  }

  .case-study-prep .hero .back-link {
    margin-bottom: clamp(var(--space-8), 5vw, var(--space-12));
  }

  .case-study-prep .hero .eyebrow {
    margin-bottom: var(--space-6);
  }

  /* opsz 40 — the text cut, matching the homepage h2. .display-1's default
     96 is Fraunces' display cut and its hairlines get thin at this size. */
  .case-study-prep .hero .display-1 {
    font-variation-settings: "opsz" 40;
    max-width: 14ch;
    font-size: clamp(2.65rem, 1.95rem + 3.45vw, 4.1rem);
    line-height: 1.02;
  }

  .case-study-prep .hero .metric-line {
    max-width: 24ch;
    color: var(--ink);
    font-size: clamp(1.45rem, 1.12rem + 1.05vw, 1.85rem);
    line-height: 1.26;
    letter-spacing: -0.018em;
  }

  .case-study-prep .hero .subtitle {
    max-width: 39ch;
    color: color-mix(in srgb, var(--ink-secondary) 88%, var(--ink-muted));
    font-size: var(--text-md);
    line-height: 1.62;
  }

  .hero-process {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding-top: var(--space-3);
  }

  .hero-process-step {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 30px;
    padding: 0 var(--space-3);
    border: 1px solid var(--rule);
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--surface) 84%, transparent);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-medium);
    letter-spacing: calc(var(--tracking-label) * 0.9);
    line-height: 1;
    text-transform: uppercase;
    white-space: normal;
  }

  .hero-process-step i {
    color: var(--ink-secondary);
    font-style: normal;
  }

  /* --- Section chrome ------------------------------------------------------ */

  .section-head h3,
  .section-head h2 {
    font-size: var(--text-2xl);
  }

  /* --- At-a-glance / context cards ----------------------------------------- */

  .context-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1px;
    padding: 0;
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    background: var(--rule);
    overflow: hidden;
  }

  .context-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-4);
    margin: 0;
    padding: clamp(1.75rem, 1.35rem + 1.1vw, 2.35rem);
    border: 0;
    border-radius: 0;
    background: var(--bg);
    box-shadow: none;
    color: inherit;
    text-decoration: none;
    transition:
      background-color var(--dur) var(--ease),
      transform var(--dur) var(--ease),
      color var(--dur-fast) var(--ease);
  }

  .context-card .icon {
    font-size: var(--text-md);
    line-height: 1;
    filter: grayscale(1);
    opacity: 0.62;
    transition: opacity var(--dur-fast) var(--ease), transform var(--dur) var(--ease);
  }

  .context-card h4 {
    font-family: var(--font-body);
    max-width: 22ch;
    font-size: var(--text-md);
    font-weight: var(--weight-semi);
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-tight);
  }

  .context-card p {
    color: var(--ink-secondary);
    max-width: 36ch;
    font-size: var(--text-md);
    line-height: 1.7;
  }

  .context-card:hover,
  .context-card:focus-within {
    background: color-mix(in srgb, var(--bg) 88%, var(--accent-wash));
    transform: translateY(-1px);
  }

  .context-card:hover .icon,
  .context-card:focus-within .icon {
    opacity: 0.82;
    transform: translateY(-1px);
  }

  @media (max-width: 640px) {
    .context-grid { grid-template-columns: minmax(0, 1fr); }
  
}

  /* --- Guided reveal for scan-heavy groups -------------------------------- */

  [data-reveal][data-stagger].is-stagger-ready > * {
    opacity: 0;
    transform: translateY(16px);
    transition:
      opacity calc(var(--dur-slow) + 40ms) var(--ease),
      transform calc(var(--dur-slow) + 40ms) var(--ease);
    transition-delay: calc(var(--stagger-index, 0) * 90ms);
  }

  [data-reveal][data-stagger].is-stagger-ready.is-visible > * {
    opacity: 1;
    transform: none;
  }

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

  .stats-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--space-10) var(--space-8);
    padding: 0;
    border: 0;
  }

  .stat-cell {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-5) 0 0;
    border: 0;
    border-top: 1px solid var(--rule);
    background: none;
  }

  .stat-figure {
    color: var(--ink);
    font-family: var(--font-display);
    font-size: var(--text-3xl);
    font-weight: var(--weight-normal);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-display);
  }

  .stat-label {
    max-width: 32ch;
    color: var(--ink-muted);
    font-family: var(--font-body);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    letter-spacing: var(--tracking-tight);
    text-transform: none;
  }

  /* --- Core problem -------------------------------------------------------- */

  .problem-block {
    display: grid;
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
    gap: var(--space-16);
    align-items: start;
  }

  .core-problem h3 {
    max-width: 16ch;
    font-size: var(--text-2xl);
    font-style: italic;
    line-height: var(--leading-snug);
  }

  .sub-problems {
    display: flex;
    flex-direction: column;
    gap: 0;
  }

  .sub-problem {
    display: grid;
    grid-template-columns: 3ch minmax(0, 1fr);
    gap: var(--space-5);
    padding-block: var(--space-6);
    border-top: 1px solid var(--rule);
  }

  .sub-problems > :first-child {
    border-top: 0;
    padding-top: 0;
  }

  .sub-problem-index {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-label);
    line-height: var(--leading-index);
  }

  .sub-problem h5 {
    margin-bottom: var(--space-2);
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-weight: var(--weight-semi);
    letter-spacing: var(--tracking-tight);
  }

  .sub-problem p {
    color: var(--ink-secondary);
    font-size: var(--text-base);
    line-height: var(--leading-relaxed);
  }

  @media (max-width: 900px) {
    .problem-block { grid-template-columns: minmax(0, 1fr); gap: var(--space-10); }
  
}

  /* --- Flow ---------------------------------------------------------------- */

  .flow-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 1px;
    padding: 0;
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    background: var(--rule);
    overflow: hidden;
  }

  .flow-step {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
    margin: 0;
    padding: var(--space-6);
    border: 0;
    border-radius: 0;
    background: var(--bg);
    box-shadow: none;
  }

  .flow-step::before,
  .flow-step::after {
    content: none;
  }

  .flow-step-num {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
  }

  .flow-step-label {
    color: var(--ink);
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-tight);
  }

  .flow-step-snippet {
    color: var(--ink-muted);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
  }

  .flow-step.is-failure {
    background: var(--status-bad-wash);
  }

  .flow-step.is-failure .flow-step-label {
    color: var(--status-bad-ink);
  }

  /* --- Callouts ------------------------------------------------------------ */

  .callout {
    margin: 0;
    padding: var(--space-10) 0;
    border: 0;
    border-block: 1px solid var(--rule);
    border-radius: 0;
    background: none;
    box-shadow: none;
  }

  .callout::before,
  .callout::after {
    content: none;
  }

  .callout p {
    max-width: 44ch;
    color: var(--ink);
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-style: normal;
    font-weight: var(--weight-normal);
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-display);
  }

  .callout-note {
    padding: var(--space-10) 0 0;
    border-block: 0;
    border-top: 1px solid var(--rule);
  }

  .callout-note-title {
    margin-bottom: var(--space-8);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-style: normal;
    font-weight: var(--weight-semi);
    letter-spacing: var(--tracking-tight);
  }

  .explain-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--space-8);
  }

  .explain-card {
    padding: 0;
    border: 0;
    border-top: 1px solid var(--rule);
    border-radius: 0;
    background: none;
    padding-top: var(--space-5);
  }

  .explain-card h5 {
    margin-bottom: var(--space-2);
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-weight: var(--weight-semi);
    letter-spacing: var(--tracking-tight);
    text-transform: none;
  }

  .explain-card p {
    color: var(--ink-secondary);
    font-size: var(--text-sm);
    font-style: normal;
    line-height: var(--leading-relaxed);
  }

  /* --- Feature blocks ------------------------------------------------------ */

  .feature-block {
    display: flex;
    flex-direction: column;
    gap: var(--space-10);
  }

  .feature-head {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    max-width: var(--measure);
  }

  .feature-head h3 {
    font-size: var(--text-2xl);
  }

  .user-story {
    margin: 0;
    padding: var(--space-6) var(--space-8);
    border: 0;
    border-left: 2px solid var(--accent);
    border-radius: 0;
    background: var(--accent-wash);
    box-shadow: none;
  }

  .user-story p {
    max-width: 60ch;
    color: var(--ink);
    font-size: var(--text-base);
    font-style: normal;
    line-height: var(--leading-relaxed);
  }

  /* --- Version / before-after ---------------------------------------------- */

  .version-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-8);
  }

  .version-row.stacked {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-12);
  }

  .version-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    margin: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
  }

  .version-card-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
  }

  .version-card-head h5 {
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-weight: var(--weight-semi);
    letter-spacing: var(--tracking-tight);
    text-transform: none;
  }

  .version-card-caption {
    max-width: var(--measure);
    color: var(--ink-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
  }

  .pill {
    padding: var(--space-1) var(--space-3);
    border: 0;
    border-radius: var(--radius-pill);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .pill-shipped  { background: var(--status-good-wash); color: var(--status-good-ink); }
  .pill-rejected { background: var(--status-bad-wash);  color: var(--status-bad-ink); }
  .pill-neutral  { background: var(--surface);          color: var(--ink-muted); }

  /* --- Status tags (inline, content-bearing) -------------------------------- */

  .tag-state {
    display: inline-block;
    padding: 2px var(--space-2);
    border: 0;
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-medium);
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  .tag-state.eligible         { background: var(--status-good-wash); color: var(--status-good-ink); }
  .tag-state.steps-away       { background: var(--status-warn-wash); color: var(--status-warn-ink); }
  .tag-state.notice-mismatch  { background: var(--status-info-wash); color: var(--status-info-ink); }
  .tag-state.ineligible       { background: var(--status-bad-wash);  color: var(--status-bad-ink); }

  /* --- Edge case ------------------------------------------------------------ */

  .edge-case {
    padding: var(--space-6) var(--space-8);
    border: 0;
    border-left: 2px solid var(--status-warn-ink);
    border-radius: 0;
    background: var(--status-warn-wash);
  }

  .edge-case p.copy {
    max-width: 62ch;
    color: var(--ink-secondary);
    font-size: var(--text-base);
    line-height: var(--leading-relaxed);
  }

  /* --- Program table --------------------------------------------------------- */

  .program-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1px;
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    background: var(--rule);
    overflow: hidden;
  }

  .program-cell {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-6);
    border: 0;
    background: var(--bg);
  }

  .program-cell.is-standout {
    background: var(--accent-wash);
  }

  .program-name {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .program-delta {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    color: var(--ink);
    font-family: var(--font-display);
    font-size: var(--text-xl);
    letter-spacing: var(--tracking-display);
  }

  .program-delta .from  { color: var(--ink-muted); }
  .program-delta .arrow { color: var(--ink-muted); font-size: var(--text-base); }

  /* --- Impact ---------------------------------------------------------------- */

  .impact-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--space-10) var(--space-8);
  }

  .impact-cell {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-5) 0 0;
    border: 0;
    border-top: 1px solid var(--rule);
    border-radius: 0;
    background: none;
    box-shadow: none;
  }

  .impact-cell:first-child {
    grid-column: auto;
  }

  .impact-figure {
    color: var(--ink);
    font-family: var(--font-display);
    font-size: var(--text-3xl);
    font-weight: var(--weight-normal);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-display);
  }

  .impact-label {
    max-width: 32ch;
    color: var(--ink-muted);
    font-family: var(--font-body);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
  }

  .impact-subline {
    max-width: var(--measure);
    margin-top: var(--space-6);
    color: var(--ink-secondary);
    font-size: var(--text-base);
    line-height: var(--leading-relaxed);
  }

  .impact-read {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin-top: var(--space-12);
  }

  .impact-read-item {
    display: grid;
    grid-template-columns: minmax(0, 0.4fr) minmax(0, 1fr);
    gap: var(--space-8);
    padding-block: var(--space-6);
    border-top: 1px solid var(--rule);
  }

  .impact-read-label {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .impact-read-item p {
    color: var(--ink-secondary);
    font-size: var(--text-base);
    line-height: var(--leading-relaxed);
  }

  .impact-read-item--emphasis p {
    color: var(--ink);
  }

  @media (max-width: 640px) {
    .impact-read-item { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }
  
}

  /* --- Reflections ------------------------------------------------------------ */

  .reflection-list {
    display: flex;
    flex-direction: column;
    gap: 0;
    border: 0;
    background: none;
  }

  .reflection-item {
    display: grid;
    grid-template-columns: 3ch minmax(0, 1fr);
    gap: var(--space-6);
    margin: 0;
    padding: var(--space-8) 0;
    border: 0;
    border-top: 1px solid var(--rule);
    background: none;
  }

  .reflection-list > :first-child {
    border-top: 0;
    padding-top: 0;
  }

  .reflection-num {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-label);
    line-height: var(--leading-index);
  }

  .reflection-item p {
    max-width: var(--measure);
    color: var(--ink-secondary);
    font-size: var(--text-md);
    font-weight: var(--weight-normal);
    line-height: var(--leading-relaxed);
  }

  /* --- Figures ----------------------------------------------------------------- */

  .screen-figure {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
  }

  .screen-figure figcaption,
  .step-figure-caption {
    color: var(--ink-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
  }

  .figure-caveat {
    max-width: var(--measure);
    margin-top: var(--space-4);
    padding: var(--space-4) var(--space-5);
    border: 0;
    border-left: 2px solid var(--rule-strong);
    border-radius: 0;
    background: var(--surface);
    color: var(--ink-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
  }

  .view-pair {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-8);
  }

  .view-pair-label {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .view-pair-role {
    color: var(--ink);
  }

  .step-filmstrip {
    display: flex;
    gap: var(--space-6);
    overflow-x: auto;
    padding: 0 0 var(--space-3);
    -webkit-overflow-scrolling: touch;
    scrollbar-color: var(--rule-strong) transparent;
    scrollbar-width: thin;
  }

  .step-filmstrip::-webkit-scrollbar { height: 8px; }
  .step-filmstrip::-webkit-scrollbar-thumb {
    background: var(--rule-strong);
    border-radius: 8px;
  }
  .step-filmstrip::-webkit-scrollbar-track { background: transparent; }

  .step-figure {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin: 0;
    flex: 0 0 auto;
    width: min(360px, 80vw);
  }

  .step-filmstrip--large .step-figure {
    width: min(440px, 85vw);
  }

  .step-figure img {
    border: 1px solid var(--rule);
    border-radius: var(--radius-sm);
  }

  .step-figure-num {
    display: block;
    margin-bottom: var(--space-1);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .page-scroll-hint {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

}

@layer components {
  /* Legacy drew fake browser chrome around the hero video and cropped it to
     a fixed ratio. Show the asset at its own aspect instead. */
  .cs2-hero-frame,
  .cs1-hero-frame {
    aspect-ratio: auto;
    min-height: 0;
    padding: 0;
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    background: var(--tint);
  }

  .cs2-hero-frame::before,
  .cs2-hero-frame::after,
  .cs1-hero-frame::before,
  .cs1-hero-frame::after {
    content: none;
  }

  .cs2-hero-frame video,
  .cs1-hero-frame video {
    height: auto;
    object-fit: contain;
  }

  .case-study-prep .hero {
    gap: var(--space-6);
  }

}

/* ==========================================================================
   WORKING-WITH-AI ENTRIES + ABOUT
   ========================================================================== */

@layer components {

  /* --- Entry framing -------------------------------------------------------- */

  .ai-framing {
    max-width: var(--measure);
    margin-top: var(--space-4);
    padding: var(--space-5) var(--space-6);
    border: 0;
    border-left: 2px solid var(--rule-strong);
    border-radius: 0;
    background: var(--surface);
    color: var(--ink-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
  }

  .entry-hero {
    margin-top: var(--space-10);
  }

  /* --- Narrative beats ------------------------------------------------------- */

  .beat {
    display: grid;
    grid-template-columns: 4ch minmax(0, 1fr);
    gap: var(--space-6);
    max-width: calc(var(--measure) + 4ch + var(--space-6));
    padding: 0;
    border: 0;
    background: none;
  }

  .beat-index {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    line-height: var(--leading-index);
    text-transform: uppercase;
  }

  .beat p {
    margin-bottom: var(--space-5);
    color: var(--ink-secondary);
    font-size: var(--text-md);
    line-height: var(--leading-relaxed);
  }

  .beat p:last-child { margin-bottom: 0; }

  .beat-with-aside {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
    gap: var(--space-12);
    align-items: start;
  }

  @media (max-width: 900px) {
    .beat-with-aside { grid-template-columns: minmax(0, 1fr); gap: var(--space-8); }
    .beat { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }
  
}

  /* --- Plan excerpt ----------------------------------------------------------- */

  .plan-excerpt {
    border: 1px solid var(--rule);
    border-radius: var(--radius);
    background: var(--surface);
    overflow: hidden;
  }

  .plan-excerpt-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-5);
    border-bottom: 1px solid var(--rule);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.04em;
  }

  .plan-excerpt-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--rule-strong);
  }

  .plan-excerpt pre {
    margin: 0;
    padding: var(--space-5);
    overflow-x: auto;
    color: var(--ink-secondary);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-relaxed);
  }

  .plan-excerpt-foot {
    padding: var(--space-3) var(--space-5);
    border-top: 1px solid var(--rule);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.04em;
  }

  /* --- Screen sequence --------------------------------------------------------- */

  .screen-sequence {
    display: flex;
    flex-direction: column;
    gap: 0;
  }

  .sequence-step {
    display: grid;
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
    gap: var(--space-8);
    align-items: start;
    padding-block: var(--space-10);
    border-top: 1px solid var(--rule);
  }

  .screen-sequence > :first-child {
    border-top: 0;
    padding-top: 0;
  }

  .sequence-step-label {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .sequence-step-num {
    display: block;
    margin-bottom: var(--space-2);
    color: var(--ink);
  }

  .sequence-step-note {
    max-width: var(--measure);
    color: var(--ink-secondary);
    font-size: var(--text-base);
    line-height: var(--leading-relaxed);
  }

  @media (max-width: 900px) {
    .sequence-step { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
  
}

  /* --- Exploration Q&A ---------------------------------------------------------- */

  .exploration {
    margin-top: var(--space-16);
    padding-top: var(--space-10);
    border-top: 1px solid var(--rule);
  }

  .exploration-head {
    margin-bottom: var(--space-6);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .exploration-list {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .exploration-item {
    display: grid;
    grid-template-columns: 2ch minmax(0, 1fr);
    gap: var(--space-5);
    padding-block: var(--space-6);
    border: 0;
    border-top: 1px solid var(--rule);
    background: none;
  }

  .exploration-list > :first-child {
    border-top: 0;
    padding-top: 0;
  }

  .exploration-marker {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: var(--leading-index);
  }

  .exploration-q {
    display: block;
    margin-bottom: var(--space-2);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-weight: var(--weight-semi);
    letter-spacing: var(--tracking-tight);
  }

  .exploration-a {
    display: block;
    max-width: var(--measure);
    color: var(--ink-secondary);
    font-size: var(--text-base);
    line-height: var(--leading-relaxed);
  }

  .exploration-item.is-open .exploration-q {
    color: var(--accent);
  }

  .screen-figure--inset {
    max-width: 42rem;
  }

  code {
    padding: 2px var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--surface);
    font-family: var(--font-mono);
    font-size: var(--text-base);
  }

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

  .about-intro {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-6);
  }

  .about-identity {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-4);
  }

  .about-framer-title {
    color: var(--hero-pink);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .about-framer-note {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .about-framer-copy {
    max-width: 46ch;
  }

  /* --- About: the display scale the rest of the site moved to -------------
     This page kept the older, smaller scale: its h1 measured 38px against the
     47px the case-study heroes run, and its section heads 28px against 33px.
     Same three typefaces throughout — Fraunces, Inter, Geist Mono — so this is
     the scale, the tracking and the leading catching up, not a font swap.

     Scoped to .about-page. styles.css is shared with the homepage, whose hero
     is a typewriter composition tuned to its own measure and must not move. */
  .about-page .display-1 {
    font-size: var(--text-display-lg);
    line-height: 1;
    letter-spacing: -0.022em;
    font-variation-settings: "opsz" 40;
  }

  .about-page .display-3 {
    font-size: var(--text-display-md);
    line-height: 1.05;
    letter-spacing: -0.022em;
    font-variation-settings: "opsz" 40;
  }

  /* The section label and its heading were set the same distance apart as the
     heading and its body, so the pair did not read as one unit. */
  .about-page .section-head > .eyebrow + .display-3 {
    margin-top: var(--space-3);
  }

  .about-page .about-lede > p,
  .about-page .subtitle {
    max-width: 62ch;
  }

  /* --- Writing list ------------------------------------------------------
     Three essays, each one row. The date leads in mono because it is the one
     field that is the same shape every time, so it makes a column the eye can
     skip down; the title and its blurb sit in one stack beside it.

     The rule is drawn on the row's top edge rather than between siblings, so
     the list reads as a set of entries under the heading rather than a fenced
     table, and the last row leaves no trailing line above the link out. */
  .writing-list {
    /* Capped, not full-container: at the container's own width the trailing
       arrow ends up ~650px clear of the blurb it belongs to, and the row stops
       reading as one object. */
    max-width: 46rem;
    margin: var(--space-6) 0 var(--space-6);
    padding: 0;
    list-style: none;
  }

  .writing-item {
    display: grid;
    grid-template-columns: 5.5rem minmax(0, 1fr) auto;
    align-items: baseline;
    gap: var(--space-4);
    padding-block: var(--space-4);
    border-top: 1px solid var(--rule);
    color: inherit;
    text-decoration: none;
    transition: background var(--dur) var(--ease);
  }

  /* Bleeds the hover past the text so the whole row reads as the target, which
     is what the pointer is already over. */
  .writing-item:hover,
  .writing-item:focus-visible {
    background: var(--surface-raised);
    box-shadow: 0 0 0 var(--space-3) var(--surface-raised);
    border-radius: 2px;
  }

  .writing-date {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--ink-muted);
    white-space: nowrap;
  }

  .writing-copy {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
  }

  .writing-title {
    font-family: var(--font-display);
    font-size: var(--text-xl);
    line-height: 1.15;
    letter-spacing: -0.018em;
    font-variation-settings: "opsz" 40;
    color: var(--ink);
  }

  .writing-blurb {
    font-size: var(--text-base);
    line-height: 1.5;
    color: var(--ink-secondary);
  }

  .writing-arrow {
    font-family: var(--font-mono);
    color: var(--ink-muted);
    transition: transform var(--dur) var(--ease), color var(--dur) var(--ease);
  }

  .writing-item:hover .writing-arrow,
  .writing-item:focus-visible .writing-arrow {
    transform: translateX(3px);
    color: var(--hero-pink);
  }

  @media (prefers-reduced-motion: reduce) {
    .writing-item,
    .writing-arrow { transition: none; }
    .writing-item:hover .writing-arrow { transform: none; }
  }

  /* On a phone the 5.5rem date column costs the title a third of its measure
     for a five-character string, so the date moves above the title instead. */
  @media (max-width: 640px) {
    .writing-item {
      grid-template-columns: minmax(0, 1fr) auto;
      gap: var(--space-2) var(--space-3);
    }

    .writing-date { grid-column: 1 / -1; }
  }

  .about-contact-copy {
    max-width: var(--measure);
    color: var(--ink-secondary);
    font-size: var(--text-md);
    line-height: var(--leading-relaxed);
  }

  /* Contact tiles are links; give them a hover. */
  a.context-card {
    transition: background var(--dur-fast) var(--ease);
  }

  a.context-card:hover {
    background: var(--surface);
  }

  a.context-card h4 {
    color: var(--ink);
  }

}

@layer components {
  /* Keep every page on the same left edge — the About page was the only
     one using a centred narrow container. */
  .container--narrow {
    max-width: var(--col-wide);
  }

  .about-page .context-grid {
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  }

  .about-page .section-head {
    margin-bottom: var(--space-10);
  }

  .about-page .about-intro > .eyebrow,
  .about-page .section-head > .eyebrow,
  .about-page .about-reads-copy > .eyebrow {
    color: var(--hero-pink);
  }

  .about-page .context-card:hover .icon,
  .about-page .context-card:focus-within .icon {
    color: var(--hero-pink);
  }

}

@layer components {
  /* Full-page screenshots (up to 7600px tall) are shown as a fixed-height
     scrollable window, not at full height. `.page-scroll-hint` next to them
     is the affordance. */
  .img-placeholder.page-scroll {
    aspect-ratio: auto;
    height: clamp(320px, 58vh, 560px);
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    border: 1px solid var(--rule);
  }

  .img-placeholder.scrollable {
    aspect-ratio: auto;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    border: 1px solid var(--rule);
  }

  .img-placeholder.page-scroll img,
  .img-placeholder.scrollable img {
    height: auto;
    max-width: none;
    object-fit: contain;
  }

  .img-placeholder.page-scroll img {
    width: 100%;
  }

  .img-placeholder.page-scroll::-webkit-scrollbar,
  .img-placeholder.scrollable::-webkit-scrollbar {
    width: 8px;
    height: 8px;
  }

  .img-placeholder.page-scroll::-webkit-scrollbar-thumb,
  .img-placeholder.scrollable::-webkit-scrollbar-thumb {
    border-radius: var(--radius-pill);
    background: var(--rule-strong);
  }

  .img-placeholder.page-scroll::-webkit-scrollbar-track,
  .img-placeholder.scrollable::-webkit-scrollbar-track {
    background: transparent;
  }

}

@layer components {
  @media (max-width: 640px) {
    /* Wordmark wraps to two lines at 375; drop the suffix instead. */
    .nav-logo span { display: none; }
    .nav > .container {
      gap: var(--space-4);
      padding-inline: var(--space-4);
    }
    .nav-actions { gap: var(--space-3); }
    .theme-toggle-text { display: none; }
    .theme-toggle { padding: 0 var(--space-1); }
  
}

}

/* ==========================================================================
   UTILITIES
   --------------------------------------------------------------------------
   The vertical rhythm that used to live in 51 inline style="" attributes.
   Off-scale values (18, 28, 34, 44, 76px) snapped to the nearest step.
   ========================================================================== */

@layer utilities {
  .u-mt-4  { margin-top: var(--space-4); }
  .u-mt-8  { margin-top: var(--space-8); }
  .u-mt-10 { margin-top: var(--space-10); }
  .u-mt-12 { margin-top: var(--space-12); }
  .u-mt-16 { margin-top: var(--space-16); }
  .u-mt-20 { margin-top: var(--space-20); }

  .u-mb-3  { display: block; margin-bottom: var(--space-3); }
  .u-mb-6  { margin-bottom: var(--space-6); }
  .u-mb-8  { margin-bottom: var(--space-8); }
  .u-mb-24 { margin-bottom: var(--space-24); }

}

@layer components {
  /* Inline status chips inside prose — spacing came from inline styles. */
  p .tag-state {
    margin-inline: var(--space-1);
  }

}

/* ==========================================================================
   LANDING PAGE
   --------------------------------------------------------------------------
   Three tiers, descending in size and ascending in colour:

     1  Scaler          full-width rows, large video, --text-2xl title
     2  Working with AI 2-up on a tinted ground, --text-xl title
     3  Side projects   2-up solid colour tiles, no media, --text-lg title

   Dividers: the only rules on this page are the two dashed lines between
   groups. Everything else separates with space or colour. Card internals use
   explicit margins, not one shared gap, so the rhythm is readable in the
   source.

   The hand: --font-hand appears exactly three times — the hero greeting, one
   margin note and the sign-off. That is the budget. A fourth mark means one of
   these goes.

   Every selector here is home-only; nothing leaks to the case-study pages.
   ========================================================================== */

@layer components {

  /* --- Hero -----------------------------------------------------------------
     One column until 980, then two: the lede keeps the left, the contents
     index takes the right margin that was previously empty.

     The hero fills the viewport and centres its content in it, overriding
     the large asymmetric padding `.hero` gives every other page. The top
     pad clears the fixed nav and the bottom pad matches it, so the two
     cancel and the content sits on the true viewport centre — the nav is
     translucent, and centring below it instead pushed the block visibly
     low. min-height, not height: if the copy ever outgrows the viewport it
     expands rather than clipping. */

  .hero-home {
    display: grid;
    align-items: start;
    align-content: center;
    row-gap: var(--space-10);
    min-height: 100vh;
    min-height: 100svh;
    max-width: var(--col-wide);
    padding-top: var(--nav-clear);
    padding-bottom: var(--space-16);
  }

  .hero-lede {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    /* One gap governs the whole stack. Was space-6 (24px), which the role
       line's own margins then pushed to 32px and 39px — three different gaps
       in four elements. */
    gap: var(--space-4);
  }

  /* opsz 40 — the text cut, same as the case studies and the section heads.
     The display cut's hairlines thinned out too far against the paper. */
  /* 13ch, not 12 — `ch` measures the "0" advance, and Fraunces' text cut
     (opsz 40) widens lowercase ~21% while "0" widens only ~9%. The old 12ch
     therefore squeezed the typed lines and forced a 4th line. */
  /* Stepped down from clamp(2.05rem, 3.2vw, 3.25rem). It was the biggest thing
     on the page by a wide margin; the job line matters more, so this comes down
     to leave it room. */
  .hero-home .display-1 {
    max-width: min(17ch, 100%);
    font-size: clamp(1.5rem, 2vw, 2.125rem);
    line-height: 1.05;
    font-variation-settings: "opsz" 40;
  }

  /* Steps down from the headline instead of matching it. */
  .hero-emphasis {
    display: block;
    margin-top: 0.68em;
    max-width: min(13ch, 100%);
    color: var(--hero-pink);
    font-size: clamp(1.35rem, 1.95vw, 2.15rem);
    font-style: normal;
    font-weight: var(--weight-normal);
    letter-spacing: -0.02em;
    line-height: 1.04;
  }

  /* A small ampersand joining the identity line to the job line. Italic and
     muted so it reads as a mark between two statements rather than a third
     one. The negative margins pull it inside the column's 16px gap, so it sits
     between the two lines instead of adding a third full step to the stack. */
  .hero-amp {
    margin: -7px 0;
    padding-left: 0.06em;
    color: var(--ink-muted);
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-style: italic;
    line-height: 1;
    font-variation-settings: "opsz" 40;
  }

  /* The job line. It was mono uppercase at 12px, which made it the quietest
     thing in the hero — the opposite of what it should be, and it put a third
     typeface on the fold for one line of text. Now it is the display face at
     full ink, so the hero runs on two families (Fraunces and Inter) plus the
     handwritten aside, and the employer reads before the rest of the copy. */
  .role-tag {
    /* No margins: the column's gap spaces this now, so the stack stays even. */
    margin: 0;
    /* No ch cap: the logo is ~7ch wide on its own, so a measure meant for text
       broke the line into three and stranded ", Bengaluru" on its own. */
    max-width: none;
    color: var(--ink);
    font-family: var(--font-display);
    font-size: clamp(1.375rem, 1.9vw, 1.75rem);
    font-weight: 400;
    line-height: 1.2;
    letter-spacing: -0.015em;
    font-variation-settings: "opsz" 40;
  }

  /* Sized off the line's own font-size, so the logo tracks the type at every
     breakpoint rather than needing a pixel value per step. The viewBox is
     trimmed to the ink, so 1em is the cap height; the small negative
     vertical-align seats it on the baseline of the surrounding text. */
  /* The role itself in the theme pink — the same one the nav CTA turns on
     hover, so the accent is consistent. The employer and city stay in ink, so
     the logo is still the anchor of the line. */
  .role-tag-role {
    color: var(--hero-pink);
  }

  /* The employer and the city stay together; if the line has to break it breaks
     before the @, giving two clean lines rather than a stranded ", Bengaluru".
     Forcing the whole line to nowrap instead ran it under the portrait. */
  .role-tag-at {
    white-space: nowrap;
  }

  .role-tag-logo {
    /* inline-block, against the global `img { display: block }` in base — as a
       block it forced a line break either side of itself and broke this one
       line into three. */
    display: inline-block;
    /* Sized well under the cap height: the full lockup is ~7em wide, so it
       carries plenty of presence at 0.62em and stops competing with the text
       it sits inside. */
    height: 0.62em;
    width: auto;
    vertical-align: 0.02em;
    margin-inline: 0.1em 0.02em;
  }

  .home-hero-copy {
    max-width: 44ch;
    color: var(--ink-secondary);
    font-family: var(--font-body);
    /* Back up a step from 1rem: at 16px against a 27px job line the statement
       was reading as a caption rather than the third level. */
    font-size: clamp(1rem, 0.2vw + 0.9rem, 1.125rem);
    line-height: 1.55;
    letter-spacing: -0.012em;
    /* Neither `pretty` nor `balance` moves this: Chromium will not rebalance a
       block that contains a forced <br>. So the fix is in the markup instead —
       a non-breaking space binds "product take-off", which stops the wrap
       leaving "take-off" alone on a 63px line. */
    text-wrap: pretty;
  }

  /* Utility row. Vertical dashes group the items; no horizontal rule. */
  .hero-rail {
    display: flex;
    flex-wrap: wrap;
    margin-top: var(--space-3);
  }

  .hero-rail-item {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-6);
    border-left: 1px dashed var(--rule-dashed);
    color: var(--ink-secondary);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    text-decoration: none;
    transition:
      color var(--dur-fast) var(--ease),
      gap var(--dur) var(--ease),
      background-color var(--dur-fast) var(--ease),
      border-color var(--dur-fast) var(--ease),
      transform var(--dur-fast) var(--ease);
  }

  .hero-rail-item:first-child {
    padding-left: 0;
    border-left: 0;
  }

  .hero-rail-item:hover {
    gap: var(--space-3);
    color: var(--ink);
  }

  .hero-rail-item span { color: var(--ink-muted); }
  .hero-rail-item:hover span { color: var(--hero-pink); }

  .hero-rail-item--primary {
    margin-left: var(--space-2);
    padding-inline: var(--space-5);
    border: 1px solid var(--ink);
    border-radius: 999px;
    background: var(--ink);
    color: var(--surface-raised);
  }

  /* Leading the rail, the pill must keep its own padding: the :first-child rule
     above zeroes padding-left for the plain links and would flatten it. The
     link after it also drops its dashed rule, which would otherwise read as a
     divider hanging off the pill rather than separating the two text links. */
  .hero-rail-item--primary:first-child {
    margin-left: 0;
    margin-right: var(--space-4);
    padding-inline: var(--space-5);
  }

  .hero-rail-item--primary + .hero-rail-item {
    padding-left: 0;
    border-left: 0;
  }

  .hero-rail-item--primary span,
  .hero-rail-item--primary:hover span {
    color: currentColor;
  }

  .hero-rail-item--primary:hover {
    gap: var(--space-3);
    border-color: var(--ink-secondary);
    background: var(--ink-secondary);
    color: var(--surface-raised);
    transform: translateY(-1px);
  }

  /* --- Hand-marked phrase -------------------------------------------------- */

  .marked {
    position: relative;
    white-space: nowrap;
  }

  /* An SVG is a replaced element: with width:auto it takes its intrinsic
     aspect ratio and ignores `right`, so the stroke has to be sized off the
     word explicitly rather than pinned to both edges. */
  .mark-underline {
    position: absolute;
    bottom: -0.24em;
    left: -0.14em;
    width: calc(100% + 0.28em);
    height: 0.4em;
    overflow: visible;
    fill: none;
    stroke: var(--hero-pink);
    stroke-width: 2;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }

  /* The stroke is drawn by default so reduced motion inherits a finished
     mark; only the dash properties that animate it live behind the query. */
  @media (prefers-reduced-motion: no-preference) {
    .mark-underline path {
      stroke-dasharray: 140;
      stroke-dashoffset: 140;
      animation: mark-draw var(--dur-slow) var(--ease) 700ms forwards;
    }
  }

  @keyframes mark-draw {
    to { stroke-dashoffset: 0; }
  }

  /* --- Contents index --------------------------------------------------------
     A magazine table of contents in the hero's right margin. Rows separate
     with space, like everything else on this page. */

  .hero-index {
    display: none;
  }

  .hero-index a {
    display: flex;
    gap: var(--space-3);
    color: var(--ink-secondary);
    align-items: baseline;
    font-family: var(--font-body);
    font-size: clamp(0.875rem, 0.2vw + 0.82rem, 0.98rem);
    font-weight: var(--weight-medium);
    letter-spacing: -0.01em;
    line-height: var(--leading-snug);
    text-decoration: none;
    transition: color var(--dur-fast) var(--ease);
  }

  .hero-index a:hover { color: var(--ink); }

  .hero-index-num {
    display: inline-block;
    min-width: 2ch;
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: 0.8em;
    letter-spacing: var(--tracking-label);
    transition: color var(--dur-fast) var(--ease);
  }

  .hero-index a:hover .hero-index-num { color: var(--hero-pink); }

  /* --- Section head -------------------------------------------------------- */

  .section-head--home {
    gap: var(--space-5);
    margin-bottom: var(--space-24);
  }

  .section-head--home > .eyebrow {
    color: var(--hero-pink);
  }

  .section-head--home .display-2 {
    max-width: 20ch;
    font-size: var(--text-3xl);
    line-height: var(--leading-tight);
  }

  /* --- Groups --------------------------------------------------------------
     The two dashed rules are the page's entire divider budget. */

  .portfolio-group {
    padding-top: 0;
  }

  .portfolio-group + .portfolio-group {
    margin-top: var(--space-24);
    padding-top: var(--space-24);
    border-top: 1px dashed var(--rule-dashed);
  }

  .portfolio-group-head {
    display: grid;
    grid-template-columns: 4ch minmax(0, 1fr);
    gap: var(--space-6);
    align-items: start;
    max-width: var(--measure);
    margin-bottom: var(--space-16);
    padding-bottom: 0;
    border-bottom: 0;
  }

  .portfolio-group-index {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    line-height: var(--leading-index);
  }

  .portfolio-group-copy h3 {
    margin-bottom: var(--space-3);
    font-size: var(--text-xl);
  }

  .portfolio-group-copy p {
    max-width: 52ch;
    color: var(--ink-muted);
    font-size: var(--text-base);
    line-height: var(--leading-relaxed);
  }

  /* --- Marginalia ------------------------------------------------------------
     Groups 01 and 03 only. Annotating all three would read as a template
     field; leaving 02 bare is what makes these read as notes.

     Below 980 there is no margin to write in, and stacked under the group
     copy they'd read as body text in an odd face — so they are dropped
     outright rather than reflowed. */

  .margin-note {
    display: none;
    padding-top: var(--space-2);
    color: var(--ink-muted);
    font-family: var(--font-hand);
    font-size: var(--text-xl);
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-normal);
    text-wrap: balance;
    transform: rotate(-1.5deg);
    transform-origin: left top;
  }

  /* --- Shared card parts ----------------------------------------------------- */

  .case-card,
  .support-card {
    display: flex;
    flex-direction: column;
    /* legacy still draws a solid border-top on .case-card; this page
       separates with space, so reset it explicitly. */
    border: 0;
    padding: 0;
    background: none;
    box-shadow: none;
    color: inherit;
    text-decoration: none;
  }

  .case-card-label {
    margin-bottom: var(--space-5);
  }

  .case-card:hover .case-card-label {
    color: var(--hero-pink);
  }

  /* Lets `margin-top: auto` on the arrow bottom-align it across a row of
     cards whose copy runs to different lengths. */
  .case-card-body,
  .support-card-body {
    display: flex;
    flex: 1;
    flex-direction: column;
    align-items: flex-start;
  }

  .case-card-title,
  .support-card-title {
    margin-bottom: var(--space-3);
    line-height: var(--leading-snug);
    text-wrap: balance;
    transition: color var(--dur-fast) var(--ease);
  }

  /* pretty, not balance. Tier 1's titles are long enough to wrap, and balance
     evens the lines rather than filling them — "65 more eligible applications in
     3 weeks, on the careers application portal" came out 397px + 480px inside a
     765px box, leaving a ragged gap the width of a thumbnail on the right of a
     card whose whole point is that its edges line up. pretty fills line one (661px)
     and only steps in to stop a one-word last line.

     Tier 2 keeps balance above: those titles are short enough to sit on one or two
     near-even lines already, where balance reads as deliberate rather than short. */
  .case-card-title {
    text-wrap: pretty;
  }

  /* The hover state is the theme pink, matching the card label above it, which
     already used --hero-pink. Contrast holds: 7.11:1 on the card in light
     (--accent-ink was 7.23) and 6.30:1 in dark, both past the 3.0 floor for
     large text by a wide margin. */
  .case-card:hover .case-card-title,
  .support-card:hover .support-card-title {
    color: var(--hero-pink);
  }

  .case-card-metric,
  .support-card-metric {
    margin-bottom: var(--space-5);
    color: var(--ink-secondary);
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-style: normal;
    font-weight: var(--weight-normal);
    line-height: var(--leading-relaxed);
    letter-spacing: var(--tracking-tight);
  }

  /* The tool marks are one set, so they sit as a set: 4px apart, not 12px. The
     label keeps its own margin so TOOLS still reads as a label rather than as
     another chip in the run. */
  .card-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-1);
    margin-bottom: var(--space-6);
  }

  .card-tools-label {
    margin-right: var(--space-1);
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
  }

  .card-tool {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 1.9rem;
    /* Tighter on the left: a glyph reads lighter than a cap-height letter. */
    padding: 0.38rem 0.72rem 0.38rem 0.58rem;
    border: 1px solid color-mix(in srgb, var(--rule-strong) 70%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, var(--surface-raised) 90%, transparent);
    color: var(--ink-secondary);
    font-family: var(--font-body);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    letter-spacing: -0.01em;
    line-height: 1;
    white-space: nowrap;
  }

  /* Brand marks and UI glyphs share one 24px grid, so a single size works for
     both. Knocked back so the logo never out-weighs its own label. */
  .card-tool svg {
    width: 1rem;
    height: 1rem;
    flex: none;
    fill: currentColor;
  }



  /* Bolder marks. Several of these logos are intrinsically fine-lined — the
     Claude starburst, the OpenAI knot, the Metabase dot matrix — and at pill
     size they thin out to texture. Stroking each glyph in its own colour and
     painting the stroke *under* the fill thickens it without distorting the
     shape. stroke and paint-order are inheritable, which is what lets them
     reach inside the <use> shadow tree. */
  .card-tool svg {
    stroke: currentColor;
    stroke-width: 0.55;
    stroke-linejoin: round;
    paint-order: stroke fill;
  }

  /* Figma carries its own five fills and is already solid, so an ink-coloured
     stroke would outline it in the wrong colour. */
  .card-tool[data-tool="figma"] svg {
    stroke: none;
  }

  /* Per-mark tuning: ink coverage differs enormously between these logos, so a
     single stroke width leaves the fine ones weak and the solid ones blobby.
     Vercel and VS Code are already solid shapes and get none. */
  .card-tool[data-tool="claude"]    svg { stroke-width: 1.05; }
  .card-tool[data-tool="metabase"]  svg { stroke-width: 0.8; }  /* dots merge past ~1 */
  .card-tool[data-tool="gpt"]       svg { stroke-width: 0.85; }
  .card-tool[data-tool="mixpanel"]  svg { stroke-width: 0.7; }
  .card-tool[data-tool="storyblok"] svg { stroke-width: 0.5; }
  .card-tool[data-tool="vercel"]    svg,
  .card-tool[data-tool="github"]    svg,
  .card-tool[data-tool="vscode"]    svg { stroke-width: 0; }


  /* Official brand colours. Figma is excluded on purpose: its mark is five
     colours and carries its own fills inside the symbol. Vercel and VS Code are
     pure black, so they follow the ink token and flip with the theme instead
     of disappearing on a dark ground. */
  [data-tool="claude"]    svg { color: #D97757; }
  [data-tool="gpt"]       svg { color: #412991; }
  [data-tool="storyblok"] svg { color: #09B3AF; }
  [data-tool="mixpanel"]  svg { color: #7856FF; }
  [data-tool="metabase"]  svg { color: #509EE3; }
  [data-tool="vscode"]    svg { color: #0A84C9; }
  /* GitHub, Vercel and VS Code are all near-black, so they follow the ink token
     and flip with the theme rather than vanishing on a dark ground. */
  [data-tool="vercel"]    svg,
  [data-tool="github"]    svg,
  [data-tool="cursor"]    svg { color: var(--ink); }

  :root[data-theme="dark"] [data-tool="gpt"] svg { color: #9B8AF0; }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) [data-tool="gpt"] svg { color: #9B8AF0; }
  }

  /* Name on hover only: collapsed the pill is an icon chip, so a row of tools
     reads as marks rather than a wall of text. The label stays in the DOM
     (clipped, not display:none) so screen readers still announce it, and the
     title attribute covers the pointer case. */
  .card-tool {
    gap: 0;
  }

  .card-tool .tool-name {
    max-width: 0;
    margin-left: 0;
    opacity: 0;
    overflow: hidden;
    white-space: nowrap;
    transition:
      max-width 0.3s var(--ease, ease),
      margin-left 0.3s var(--ease, ease),
      opacity 0.18s var(--ease, ease);
  }

  .card-tool:hover .tool-name,
  .card-tool:focus-visible .tool-name {
    max-width: 10rem;
    margin-left: 0.42rem;
    opacity: 1;
  }

  /* No hover on touch, so the names stay out permanently. */
  @media (hover: none) {
    .card-tool .tool-name {
      max-width: 10rem;
      margin-left: 0.42rem;
      opacity: 1;
    }
  }

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

  /* "AI synthesis" and "AI review" are methods, not products, so they have no
     brand colour of their own — the accent on the glyph is what marks them.
     No tinted ground, so every chip stays the same material. */
  .card-tool[data-tool="ai"] svg { color: var(--accent); }

  /* A primary CTA, the same filled capsule as the hero's Resume button.
     --ink/--surface-raised rather than a literal black, so it inverts with the
     theme instead of going black-on-black in dark mode. */
  /* Entry cards carry a read time beside their tools, on one row, so the eye
     picks the cost of reading off the same line it picks the stack off. The row
     gap here is 12px against the 4px between tool marks, which is what keeps the
     time reading as its own item rather than as a fourth chip. Wraps to its own
     line when the row runs out of width. */
  .card-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-3);
    margin-bottom: var(--space-6);
  }

  .card-meta .card-tools {
    margin-bottom: 0;
  }

  .card-readtime {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    white-space: nowrap;
  }

  .case-card-arrow {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: auto;
    padding: var(--space-2) var(--space-5);
    border: 1px solid var(--ink);
    border-radius: 999px;
    background: var(--ink);
    color: var(--surface-raised);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-weight: var(--weight-semi);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    transition:
      gap var(--dur) var(--ease),
      color var(--dur-fast) var(--ease),
      background-color var(--dur-fast) var(--ease),
      border-color var(--dur-fast) var(--ease),
      transform var(--dur-fast) var(--ease);
  }

  /* Filled pink on hover. The label stays --surface-raised, which is the one
     value that clears contrast on pink in both themes: cream on pink is 7.88:1
     in light, and in dark the token flips to near-black for 5.93:1. Cream on
     the dark-mode pink would have been 2.78:1, a fail. */
  .case-card:hover .case-card-arrow,
  .support-card:hover .case-card-arrow {
    gap: var(--space-3);
    border-color: var(--hero-pink);
    background: var(--hero-pink);
    color: var(--surface-raised);
    transform: translateY(-1px);
  }

  .case-card .img-placeholder,
  .support-card .img-placeholder {
    aspect-ratio: 16 / 9;
    background: var(--tint);
  }

  .case-card .img-placeholder img,
  .case-card .img-placeholder video,
  .support-card .img-placeholder img,
  .support-card .img-placeholder video {
    transition: transform var(--dur-slow) var(--ease);
  }

  .case-card:hover .img-placeholder img,
  .case-card:hover .img-placeholder video,
  .support-card:hover .img-placeholder img,
  .support-card:hover .img-placeholder video {
    transform: scale(1.015);
  }

  /* --- Tier 1 · Scaler -------------------------------------------------------- */

  .case-grid {
    display: flex;
    flex-direction: column;
    gap: var(--space-20);
  }

  .case-card-media {
    margin-bottom: var(--space-6);
  }

  /* Title, metric and media all read from one width so their left AND right edges
     line up — previously the title stopped at 40ch and the metric at 56ch, both
     well short of the media above them, which made the card look ragged.

     92%, not 100%: the ask was to shrink the media a little to cut the scroll, and
     since the media's aspect ratio is fixed the only way to lose height WITHOUT
     cropping the UI is to lose a little width. 8% off the width takes 8% off the
     height, and the interface inside stays entirely visible. */
  .case-card,
  .support-card {
    --case-w: 92%;
  }

  /* legacy.css still insets the body by 30px, left over from when these cards had
     a visible box. They do not any more, and that inset is exactly what stopped the
     copy lining up with the media above it. */
  .case-card-body,
  .support-card-body {
    padding-inline: 0;
  }

  /* The body was reading as five unrelated lines rather than one block: legacy's
     28px top padding and 10px flex gap were stacking on top of every element's own
     margin, so each step down the card cost 22-34px. Zeroing the gap puts the
     rhythm back under the per-element margins below, where it can be tuned:
     tight inside the title/metric/meta group, one clear step before the CTA. */
  .case-card-body,
  .support-card-body {
    gap: 0;
    padding-top: var(--space-4);
    padding-bottom: 0;
  }

  .case-card-body > .case-card-title,
  .support-card-body > .support-card-title {
    margin-bottom: var(--space-2);
  }

  .case-card-body > .case-card-metric,
  .support-card-body > .support-card-metric {
    margin-bottom: var(--space-4);
  }

  .case-card-body > .card-meta,
  .support-card-body > .card-meta,
  .case-card-body > .card-tools,
  .support-card-body > .card-tools {
    margin-bottom: var(--space-5);
  }

  /* width, not max-width: the body is a column flex with align-items:flex-start, so
     the children size to their content and a max-width can only ever shrink them —
     it cannot make them reach. The arrow keeps shrink-to-fit, which is why this is
     set per element rather than by flipping the body to align-items:stretch. */
  .case-card-title,
  .support-card-title {
    width: var(--case-w);
    max-width: none;
    font-size: var(--text-2xl);
  }

  .case-card-metric,
  .support-card-metric {
    width: var(--case-w);
    max-width: none;
  }

  .case-card-media .img-placeholder,
  .support-card-media .img-placeholder {
    width: var(--case-w);
  }

  /* --- Tier 2 · Working with AI ------------------------------------------------ */

  .support-grid--entries {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-8);
  }

  /* Back to full width here. The 92% --case-w above buys Tier 1 some height back
     on media that runs the whole container; Tier 2's media is already small — a
     3:2 box in a 2-up grid — so the same trade costs alignment and saves nothing.

     And these cards, unlike Tier 1's, have a visible panel. The leftover 8% is
     therefore not neutral space: it reads as a ragged right edge, with the copy
     and the capture both stopping short of a border the eye can actually see,
     while the left edge sits flush. At 100% the content and the card share one
     right edge. The tools row and the CTA stay shrink-to-fit, which is why this
     is a --case-w change rather than align-items: stretch on the body. */
  .support-card--entry {
    --case-w: 100%;
  }

  /* Sheets of paper, matching the hero: the same near-square corner, the same
     top edge line and lift shadow. A tinted panel read as a UI card; paper ties
     this tier to the typewriter fold above it. */
  /* Glass, not paper. The rest of the page is stock and fibre; these two are
     the AI entries, so they read as a translucent pane over it with a soft
     blue bloom. Deliberately quiet — the tint is a few percent, the glow is
     wide and dim, and it is the only place on the page that uses either. */
  .support-card--entry {
    position: relative;
    /* One step in from --space-5. The card frame is not the point of these two
       tiles — the capture inside is — and every pixel taken off the frame goes
       straight into the width of the interface on show. */
    padding: var(--space-4);
    border-radius: 10px;
    border: 1px solid var(--ai-edge);
    background:
      /* faint cool wash from the top-left corner */
      radial-gradient(120% 90% at 8% 0%, var(--ai-wash) 0%, transparent 62%),
      var(--ai-pane);
    -webkit-backdrop-filter: blur(26px) saturate(125%);
    backdrop-filter: blur(26px) saturate(125%);
    box-shadow:
      inset 0 1px 0 var(--ai-sheen),
      inset 0 -1px 0 var(--ai-underline),
      0 0 0 1px var(--ai-ring),
      /* a hint of blue inside the glass, not a glow around it */
      inset 0 0 60px -28px var(--ai-glow),
      0 6px 16px -16px var(--ai-shadow);
    transition:
      transform var(--dur) var(--ease),
      box-shadow var(--dur) var(--ease),
      border-color var(--dur) var(--ease);
  }

  /* Lifts and the inner bloom deepens a touch — no colour change. */
  .support-card--entry:hover {
    transform: translateY(-2px);
    border-color: var(--ai-edge-hover);
    box-shadow:
      inset 0 1px 0 var(--ai-sheen),
      inset 0 -1px 0 var(--ai-underline),
      0 0 0 1px var(--ai-ring),
      inset 0 0 70px -26px var(--ai-glow-hover),
      0 10px 20px -16px var(--ai-shadow);
  }

  .support-card--entry .support-card-media {
    margin-bottom: var(--space-6);
  }

  /* The other tiers show device mocks composited into their own footage, so the
     recording arrives with a ground already under it. These two are bare screen
     captures, which left them floating on the card. The gradient field puts them
     on something — the same move the entry pages make behind their hero video.
     The field and the tier's tints are in the theme's pink now, not the slate blue
     they started as: the blue was the only cool accent left on a warm page. The
     backdrop asset itself was hue-rotated (215° -> 333°) to land on --hero-pink
     rather than being redrawn. */
  .support-card--entry .img-placeholder {
    /* Both cards must share one box or their titles stop aligning across the
       row. 1.42 rather than 3:2 because the two sources are 1.40 (MountBlue)
       and 1.60 (Conversations): sitting just under the narrower of the two
       means MountBlue — the taller, denser capture, and the one that was
       rendering smallest — is limited by width rather than height, and gains
       about a fifth in linear size. The mat is a contrast device, not a frame,
       so its padding comes down with it. */
    aspect-ratio: 1.42;
    padding: clamp(6px, 0.8vw, 10px);
    border-radius: 6px;
    background-color: var(--surface-raised);
    background-image: url("../assets/shared/entry-card-sky.webp");
    background-repeat: no-repeat;
    background-position: center;
    background-size: cover;
    box-shadow: inset 0 0 0 1px var(--ai-edge);
  }

  /* The capture sits above the field rather than being cut into it, and it is
     shown whole: `cover` was cropping the flow at the edges — the two source
     recordings are 16:9 and 4:3, and neither matches the padded box. `contain`
     letterboxes instead, and the field fills whatever is left over. */
  .support-card--entry .img-placeholder img,
  .support-card--entry .img-placeholder video {
    object-fit: contain;
    /* drop-shadow, not box-shadow: under `contain` the element box is the whole
       padded area and the letterboxed remainder is transparent, so a box-shadow
       would draw around the mat instead of around the capture. A filter follows
       what is actually painted. */
    filter:
      drop-shadow(0 1px 2px rgb(12 20 34 / 0.3))
      drop-shadow(0 14px 26px rgb(12 20 34 / 0.28));
  }

  @media (max-width: 720px) {
    .support-card--entry .img-placeholder {
      padding: 6px;
    }
  }

  .support-card--entry .support-card-title {
    font-size: var(--text-xl);
  }

  /* --- Tier 3 · Side projects --------------------------------------------------- */

  .support-grid--tiles {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-8);
  }

  /* Both cards in a 2-up row cross the observer threshold together, so the
     second waits a beat. Tier 1's full-width rows arrive one at a time and
     a delay there would just read as lag. */
  .support-grid > [data-reveal]:nth-child(2) {
    transition-delay: 90ms;
  }

  .support-card--tile .support-card-media {
    margin-bottom: var(--space-6);
  }

  .support-card--tile .support-card-title {
    max-width: 26ch;
    font-size: var(--text-lg);
  }

  .project-panel {
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    /* The blurb sits in the middle band and holds its space whether or not it
       is revealed, so the gaps around it are what set the tile's height. They
       were doing most of the work of making these tiles taller than they read
       — space-5 keeps the band legible without the slab of empty colour. */
    gap: var(--space-5);
    aspect-ratio: 2 / 1;
    padding: var(--space-6);
    border: 0;
    border-radius: var(--radius);
    transition: transform var(--dur) var(--ease);
  }

  /* A single line mark per tile, drawn in the tile's own ink so it stays black
     on the colour rather than introducing a third hue. It is absolutely placed
     so it costs the panel no height, and sits opposite the CTA on the base
     line rather than crowding the title. */
  .project-panel-mark {
    position: absolute;
    right: var(--space-6);
    bottom: var(--space-6);
    width: 30px;
    height: 30px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.3;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity: 0.42;
    transition: opacity var(--dur) var(--ease);
  }

  .support-card--tile:hover .project-panel-mark,
  .support-card--tile:focus-visible .project-panel-mark {
    opacity: 0.72;
  }

  .support-card--tile:hover .project-panel {
    transform: translateY(-3px);
  }

  .project-panel--kindle {
    background: var(--tile-slate);
    color: var(--tile-slate-ink);
  }

  .project-panel--pal {
    background: var(--tile-ochre);
    color: var(--tile-ochre-ink);
  }

  /* Carries the full project title now, not a one-word mark, so it steps down
     a size and takes a measure — 3xl set a whole sentence far too large. */
  .project-panel-title {
    max-width: 20ch;
    margin: 0;
    /* As an <h3> it would otherwise take the global heading colour, which is
       --ink and inverts with the theme. The tile sets its own ink. */
    color: inherit;
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: var(--weight-normal);
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-display);
    text-wrap: balance;
  }

  /* Sits where the tags were, in the tile's own ink so it reads as part of the
     panel rather than a second link floating on the colour. */
  .project-panel-cta {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    align-self: flex-start;
    padding-bottom: 2px;
    border-bottom: 1px solid color-mix(in srgb, currentColor 35%, transparent);
    color: currentColor;
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    transition:
      gap var(--dur) var(--ease),
      border-color var(--dur-fast) var(--ease);
  }

  .support-card--tile:hover .project-panel-cta,
  .support-card--tile:focus-visible .project-panel-cta {
    gap: var(--space-3);
    border-bottom-color: currentColor;
  }

  /* --- Footer reveal ------------------------------------------------------
     The closing band and footer sit one layer behind the page and are uncovered
     as it scrolls off them. .fold-stage is the cover: it holds every content
     section, so making it opaque and giving it the higher z-index is all that
     is needed above. Everything else is driven by --footer-reveal-h, which
     footer-reveal.js sets to the measured height of the block and removes when
     the effect is off — so with no JS, or on a short window, all of these rules
     collapse to normal flow. */

  .portfolio-home .fold-stage {
    z-index: 1;
    background: var(--bg);
  }

  .portfolio-home .footer-stage {
    position: relative;
    z-index: 0;
  }

  /* fixed, not sticky. Sticky with bottom:0 holds an element that is scrolling
     up INTO view from below; here the block has to hold still while the layer
     above scrolls off it, which is what fixed does. Sticky was measured parking
     the footer 799px above the viewport bottom at the end of the page. */
  .portfolio-home .footer-stage.is-revealing .footer-reveal {
    position: fixed;
    inset: auto 0 0 0;
  }

  .portfolio-home .footer-stage-tail { height: 0; }

  .portfolio-home .footer-stage.is-revealing .footer-stage-tail {
    /* Gives back the height the negative margin took. This is the scroll
       distance the reveal plays over. */
    height: var(--footer-reveal-h);
  }

  /* --- Closing strip -------------------------------------------------------------
     Inverts in both themes: --ink on --bg reads dark-on-light in light mode
     and light-on-dark in dark mode. */

  .closing {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    /* Rides up over the section above it with its own top edge and an upward
       shadow — the same construction .portfolio-home #work uses to feed over
       the hero, so both seams on the page are made the same way. */
    z-index: 5;
    margin-top: calc(var(--space-16) * -1);
    padding-block: calc(var(--space-16) + var(--space-16)) 0;
    border-radius: 2px 2px 0 0;
    background:
      linear-gradient(rgb(28 27 25 / 0.022), rgb(28 27 25 / 0) 10px),
      var(--foot-bg);
    box-shadow:
      0 -1px 0 rgb(0 0 0 / 0.05),
      0 -8px 18px -14px rgb(0 0 0 / 0.18);
    color: var(--foot-ink);
    text-align: center;
  }

  /* The aurora: three soft lobes, blurred rather than feathered by gradient
     stops alone, so the colour diffuses the way Craft's does instead of
     reading as three discs. Sits under the wordmark. */
  /* Cloud drift. Two banks of soft lobes travelling one way at different
     speeds — never reversing, which is what separates weather from a pulse.

     The seam: each bank is twice the container's width and its lobe field is
     tiled at half that (so one tile == one container width). Translating a bank
     by -50% of its own width therefore advances it exactly one tile, and the
     loop closes with nothing to see. That is the whole reason for the 200%/50%
     pair — it is not padding. */
  .closing-aurora {
    position: absolute;
    inset: -34% 0;
    z-index: -2;
    /* Clips the double-width banks so they cannot widen the page. The band's
       own edges sit well inside this box, so the clip is never visible. */
    overflow: hidden;
    pointer-events: none;
  }

  .closing-aurora::before,
  .closing-aurora::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 200%;
    background-repeat: repeat-x;
    background-size: 50% 100%;
    filter: blur(74px);
    will-change: transform;
  }

  /* The near bank: larger lobes, low in the frame, moving faster. */
  .closing-aurora::before {
    background-image:
      radial-gradient(30% 40% at 18% 74%, var(--foot-glow-pink) 0%, transparent 74%),
      radial-gradient(34% 44% at 62% 92%, var(--foot-glow-hot) 0%, transparent 76%);
    animation: cloud-drift-near 82s linear infinite;
  }

  /* The far bank: smaller, higher, slower — parallax, so the two separate. */
  .closing-aurora::after {
    background-image:
      radial-gradient(24% 32% at 34% 58%, var(--foot-glow-hot) 0%, transparent 74%),
      radial-gradient(26% 30% at 78% 78%, var(--foot-glow-pink) 0%, transparent 74%);
    animation: cloud-drift-far 137s linear infinite;
  }

  /* -50% of a double-width bank is exactly one tile. Linear, because clouds do
     not ease. */
  @keyframes cloud-drift-near {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(-50%, 0, 0); }
  }

  @keyframes cloud-drift-far {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(-50%, 0, 0); }
  }

  @media (prefers-reduced-motion: reduce) {
    .closing-aurora::before,
    .closing-aurora::after { animation: none; }
  }

  /* The same painted sky the first fold uses, so the page opens and closes on
     one backdrop. It sits at z-index -3, below the aurora (-2) and the dot grid
     (-1): a negative-z child paints above its own element's background, so the
     cream still grounds it. Same --sky-strength as the fold, and the same 2560
     master on large or hi-dpi screens. */
  .closing::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -3;
    pointer-events: none;
    opacity: var(--sky-strength);
    background-image: url("../assets/shared/painted-sky.webp");
    background-size: cover;
    background-position: center top;
    background-repeat: no-repeat;
  }

  @media (min-width: 1600px), (min-width: 1024px) and (min-resolution: 1.5dppx) {
    .closing::after {
      background-image: url("../assets/shared/painted-sky-2560.webp");
    }
  }

  /* The pattern: a fine dot grid, fading out towards the bottom so the footer
     row below it stays quiet. Decorative only — it carries no contrast. */
  .closing::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background-image: radial-gradient(var(--foot-dot) 1px, transparent 1px);
    background-size: 22px 22px;
    background-position: 0 0;
    -webkit-mask-image: linear-gradient(to bottom, rgb(0 0 0 / 0.9), rgb(0 0 0 / 0.15));
    mask-image: linear-gradient(to bottom, rgb(0 0 0 / 0.9), rgb(0 0 0 / 0.15));
    pointer-events: none;
  }

  .closing-inner {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-10);
  }

  /* Same construction as the nav's Resume CTA: an ink-filled pill, sentence
     case in the body face, arrow nudging on hover, hover turning to
     --hero-pink. Scaled up for a page-level action, nothing else changed. */
  .closing-cta-row { display: flex; justify-content: center; }
  .closing-cta,
  .closing-links a {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    padding: 0 var(--space-6);
    border: 1px solid var(--ink);
    border-radius: var(--radius-pill);
    background: var(--ink);
    color: var(--surface-raised);
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    /* Matches the tracking the nav CTA inherits at 13px, so the two pills set
       identically rather than just looking similar. */
    letter-spacing: 0.01em;
    text-transform: none;
    text-decoration: none;
    opacity: 1;
    transition:
      background-color var(--dur-fast) var(--ease),
      border-color var(--dur-fast) var(--ease),
      color var(--dur-fast) var(--ease);
  }

  .closing-cta span,
  .closing-links a span {
    transition: transform var(--dur-fast) var(--ease);
  }

  /* A mark ahead of each label so the three read at a glance. currentColor, so
     they invert with the pill on hover like the arrow does. */
  .closing-icon {
    width: 15px;
    height: 15px;
    flex: none;
    margin-right: 2px;
    fill: currentColor;
  }

  .closing-cta:hover,
  .closing-cta:focus-visible,
  .closing-links a:hover,
  .closing-links a:focus-visible {
    border-color: var(--hero-pink);
    background: var(--hero-pink);
    color: var(--surface-raised);
  }

  .closing-cta:hover span,
  .closing-cta:focus-visible span,
  .closing-links a:hover span,
  .closing-links a:focus-visible span {
    transform: translateX(2px);
  }

  /* The three secondary actions are the same pill, unfilled — the CTA above
     stays the only solid one. */
  .closing-links a {
    background: transparent;
    color: var(--ink);
  }

  /* Eyebrow and line are one unit; the container's space-10 gap is meant to
     separate the block from the links, not the label from its own sentence. */
  .closing-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
  }

  /* The label is the serif here and the sentence below it is not — the reverse
     of every other eyebrow on the site, and deliberate: it makes "let's get in
     touch" the voice and leaves the sentence to read plainly. */
  .closing-eyebrow {
    color: var(--foot-eyebrow);
    opacity: 1;
    font-family: var(--font-display);
    font-size: 0.9375rem;
    font-weight: 500;
    letter-spacing: 0.14em;
    font-variation-settings: "opsz" 40;
  }

  .closing-line {
    margin: 0;
    max-width: 26ch;
    color: var(--foot-ink);
    /* Inter, not the display serif. Its x-height is larger, so the same
       nominal size reads bigger — stepped down to --text-2xl to compensate. */
    font-family: var(--font-body);
    font-size: var(--text-2xl);
    line-height: 1.15;
    letter-spacing: var(--tracking-display);
    text-wrap: balance;
  }

  /* The offset italic second line, as Craft sets "SINCE 2014". */
  .closing-line--alt {
    align-self: center;
    margin-left: 4.5rem;
    font-style: italic;
    color: var(--foot-ink-2);
  }

  .closing-eyebrow { margin-bottom: var(--space-2); }

  .closing-links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-8);
  }

  .closing-links {
    gap: var(--space-4);
  }



  /* Signs off the page like a letter. Sits bottom-right once there is room. */
  /* Craft crops its logotype on the band's bottom edge; hers is the signature.
     Oversized, low-contrast, and clipped by the band's overflow so it reads as
     a ground element rather than a heading. */
  .closing-wordmark {
    position: relative;
    z-index: -1;
    margin: var(--space-12) 0 0;
    color: var(--foot-wordmark);
    font-family: var(--font-hand);
    font-size: clamp(8rem, 26vw, 24rem);
    font-weight: 400;
    line-height: 0.74;
    letter-spacing: -0.02em;
    text-align: center;
    transform: none;
    opacity: 1;
    user-select: none;
    /* Negative bottom margin pulls the footer row up over the letters, so the
       band's edge cuts through them instead of clearing them. */
    margin-bottom: -0.11em;
  }

  /* The footer row joins the band rather than sitting on cream below it, so
     the whole thing reads as one footer. */
  .site-footer--home {
    position: relative;
    isolation: isolate;
    border-top: 1px solid var(--foot-rule);
    padding-block: var(--space-8) var(--space-10);
    background: var(--foot-bg);
    color: var(--foot-ink-2);
  }

  /* The row is a separate element, so it needs its own slice of the same sky or
     the backdrop would stop dead at the divider. background-position keeps it
     continuous with the band above. */
  .site-footer--home::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: var(--sky-strength);
    /* A cream wash over the sky, only in this row. The band above sets its text
       in full ink, which clears AA on the darkest cloud at 6.9:1; the row's
       secondary ink measured 4.11:1 there, under the floor. The wash lifts the
       darkest patches without hiding the backdrop. */
    background-image:
      linear-gradient(rgb(247 243 235 / 0.42), rgb(247 243 235 / 0.42)),
      url("../assets/shared/painted-sky.webp");
    background-size: cover;
    background-position: center bottom;
    background-repeat: no-repeat;
  }

  @media (min-width: 1600px), (min-width: 1024px) and (min-resolution: 1.5dppx) {
    .site-footer--home::before {
      background-image:
        linear-gradient(rgb(247 243 235 / 0.42), rgb(247 243 235 / 0.42)),
        url("../assets/shared/painted-sky-2560.webp");
    }
  }

  .site-footer--home .footer-note { color: var(--foot-ink-2); }
  .site-footer--home .footer-links a {
    color: var(--foot-ink-2);
    padding-bottom: 2px;
    border-bottom: 1px solid transparent;
    transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
  }
  .site-footer--home .footer-links a:hover,
  .site-footer--home .footer-links a:focus-visible {
    color: var(--foot-ink);
    border-bottom-color: var(--foot-eyebrow);
  }

  /* --- Landing responsive -----------------------------------------------------
     980 is where a right margin actually exists: 916px of inner container,
     less the 224px index column and its 48px gutter, still leaves 644px for
     a lede whose headline caps at ~468px. Below it, everything that lives in
     the margin goes away rather than reflowing into the main column. */

  @media (min-width: 980px) {
    .hero-home {
      grid-template-columns: minmax(0, 1fr) 14rem;
      column-gap: var(--space-12);
    }

    /* One column. The two-column split moved down onto the sheet itself, so the
       portrait and the contents sit ON the paper rather than beside the machine —
       everything that belongs to the page is on the page. */
    .hero-home[data-typewriter] {
      grid-template-columns: minmax(0, 1fr);
    }

    .hero-index {
      display: flex;
      flex-direction: column;
      gap: var(--space-3);
    }

    .hero-index .eyebrow {
      margin-bottom: var(--space-1);
    }

    /* 52ch matches the cap already on .portfolio-group-copy p, so opening a
       third column doesn't change the copy measure. */
    .portfolio-group-head {
      grid-template-columns: 4ch minmax(0, 52ch) minmax(0, 1fr);
      column-gap: var(--space-16);
      max-width: none;
    }

    .margin-note {
      display: block;
    }

  }

  /* Phone: the band tightens, the arc shrinks so it stays a curve rather than
     a straight line across the corner, and the buttons go full-width-ish so
     they are easy to hit. */
  @media (max-width: 640px) {
    .closing { padding-block: calc(var(--space-12) + var(--space-16)) 0; }
    .closing-inner { gap: var(--space-8); }
    .closing-line { max-width: none; font-size: var(--text-2xl); }
    /* The indent needs a line long enough to hang off; at phone width there
       isn't one, so the two lines simply stack centred. */
    .closing-line--alt { margin-left: 0; }
    .closing-links { gap: var(--space-3); width: 100%; }
    .closing-links a { flex: 1 1 8rem; justify-content: center; padding: 12px 14px; }
    .closing-wordmark { margin-top: var(--space-8); font-size: 30vw; }
    .closing-aurora { filter: blur(58px) saturate(120%); }
    .site-footer--home {
      padding-block: var(--space-6) var(--space-8);
    }
    .site-footer--home > .container {
      flex-direction: column;
      align-items: flex-start;
      gap: var(--space-4);
    }
  }

  @media (max-width: 900px) {
    .support-grid--entries,
    .support-grid--tiles {
      grid-template-columns: minmax(0, 1fr);
      gap: var(--space-10);
    }
  }

  @media (max-width: 640px) {
    .case-grid { gap: var(--space-16); }

    .portfolio-group + .portfolio-group {
      margin-top: var(--space-16);
      padding-top: var(--space-16);
    }

    .portfolio-group-head {
      grid-template-columns: minmax(0, 1fr);
      gap: var(--space-3);
      margin-bottom: var(--space-10);
    }

    .section-head--home { margin-bottom: var(--space-16); }

    .support-card--entry { padding: var(--space-4); }

    /* The blurb is always in the card at this width, so the panel can size to
       its own content — the fixed ratio was only padding it out. */
    .project-panel {
      aspect-ratio: auto;
      padding: var(--space-5);
      gap: var(--space-4);
    }

    .project-panel-mark {
      right: var(--space-5);
      bottom: var(--space-5);
      width: 24px;
      height: 24px;
    }

    .hero-rail { margin-top: var(--space-2); }

    .hero-rail-item {
      padding-inline: var(--space-4);
    }

    .hero-rail-item--primary {
      margin-left: var(--space-1);
    }

    .hero-home { row-gap: var(--space-8); }
  }
}

/* ==========================================================================
   HERO TYPEWRITER (home)
   A sheet fed through a platen. The machine body is off-screen: only the
   platen, its end knobs and the paper bail are drawn, which keeps the
   headline at full display size and the layout simple on small screens.
   ========================================================================== */

@layer components {

  /* Painted sky behind the fold.

     A photograph of an oil painting: 30K at standard density and 85K for
     high-density displays. Brushwork compresses well, so it keeps its texture
     without competing with the keyboard photograph for bandwidth.

     Both files are pure downscales of a 14064px square master (kept out of the
     repo at "PF media/Heavenly Drift Wallpaper 1.png"; a 4096px working copy sits
     in the gitignored .asset-originals/painted-sky-source.jpg). Earlier revisions
     upscaled a 736px source and needed unsharp and grain passes to fake detail —
     none of that applies now and none of it should come back. Downscaled real
     brushwork is sharp on its own.

     Crop to the box's aspect exactly — `cover` throws away any extra height, so a
     taller crop is bytes on the wire that no one ever sees. The band in use is
     14064x7873+0+3822: open sky up top where the headline sits, cloud mass low.

     q50 is deliberate and is the one number worth understanding here. The canvas
     weave is real high-frequency texture across the whole frame, so WebP charges
     far more for this master than it did for the blurry upscale it replaced —
     q62 costs 116K and q80 costs 220K for detail that is invisible under
     --sky-strength opacity. q50 holds the brushstrokes with no blocking.

     Deliberately NOT preloaded. It is a background, and the keyboard photograph
     above it carries fetchpriority="high" — a preload here would only take
     bandwidth from the thing the eye actually lands on.

     It hangs off .fold-stage, not the hero. The hero is `overflow: clip` and only
     1056px wide, so a pseudo inside it was trimmed to that box and the sky ended in
     two hard vertical edges instead of reaching the viewport. .fold-stage is full
     width and unclipped. Height comes from --fold-h so it tracks the hero exactly,
     and z-index 0 keeps it under the hero (z-index 1) — which has no background of
     its own, so the sky reads through it. The work section below is opaque at
     z-index 4 and covers whatever hangs past the fold. */

  /* Position, not overflow — a clipping ancestor here would kill the hero's sticky.
     This only makes .fold-stage the containing block for the backdrop below. */
  .portfolio-home .fold-stage {
    position: relative;
  }

  .portfolio-home .fold-stage::before {
    content: "";
    position: absolute;
    inset: 0 0 auto;
    height: var(--fold-h);
    z-index: 0;
    pointer-events: none;
    opacity: var(--sky-strength);
    background-image: url("../assets/shared/painted-sky.webp");
    background-size: cover;
    background-position: center top;
    background-repeat: no-repeat;
  }

  @media (min-width: 1600px), (min-width: 1024px) and (min-resolution: 1.5dppx) {
    .portfolio-home .fold-stage::before {
      background-image: url("../assets/shared/painted-sky-2560.webp");
    }
  }

  .hero-home[data-typewriter] {
    /* Grid, not flex. This used to be a column flex box because the typewriter
       hero was a single stack; it now has a second column for the portrait and
       the contents, and `display: flex` here silently beat the base rule's grid
       and stacked them instead. */
    display: grid;
    /* start, not center. The content is deliberately taller than the capped fold
       so the machine crops at the bottom edge; centring it splits that overflow
       across both edges and takes the first headline line off the top, behind
       the nav. */
    align-content: start;
    /* A gutter MORE than the standard nav clearance. --nav-clear only accounts
       for the bar's own height and inset; the sheet's top edge was landing flush
       against the bar's underside with nothing between them. */
    padding-top: calc(var(--nav-clear) + var(--space-6));
    align-items: stretch;
    justify-items: stretch;
    position: relative;
    isolation: isolate;
    row-gap: 0;
    /* `clip` (not `hidden`) keeps the ground's parallax overhang inside the
       fold without turning the hero into a scroll container. */
    overflow: clip;
    /* Give back a slice of the fold so the next sheet and its label peek above
       it — a visible edge invites the scroll far better than the arrow alone. */
    min-height: var(--fold-h);
    /* And never more. The machine photograph is 1.19:1 — nearly square — so at a
       width that keeps the headline readable it stands ~620px tall, and sheet
       plus machine overruns the fold by a couple of hundred pixels. Capping the
       height lets `overflow: clip` above crop the machine at the fold instead,
       which is the better composition anyway: the typewriter continues off the
       bottom of the frame rather than being shrunk until the paper in it is too
       narrow to set a headline on. On short viewports it simply crops further,
       degrading towards the platen alone. */
    max-height: var(--fold-h);
    min-height: calc(100svh - 92px);
    /* Holds at the top while Selected Work rides over it, which is what makes
       the handoff readable. Sticky (not fixed) costs no extra scroll length,
       and .fold-stage releases it once the work section has passed. */
    position: sticky;
    top: 0;
    z-index: 1;
  }

  /* The machine: the sheet stacked directly on the drawing, with no gap. The
     sheet's own negative bottom margin is what tucks it behind the platen. */
  .hero-desk {
    /* The keyboard photograph's geometry. --keyboard-aspect is a measurement OF
       THE IMAGE FILE; recrop the source and it has to change with it. */
    --keyboard-w: 40rem;
    --keyboard-aspect: 2.099;
    /* Must mirror .hero-keyboard's own width exactly — the contact shadow is
       derived from it, and a stale 100% here puts the shadow wider than the
       object casting it. */
    --keyboard-actual: min(64%, var(--keyboard-w));
    /* Air between the sheet and the keyboard. This is the whole idea, so it is
       named rather than buried in a margin: the paper is hovering above the thing
       typing it, not fed into it. */
    --float-gap: clamp(var(--space-3), 1.2vw, var(--space-5));
    /* The machine's arrival. --type-start is deliberately shorter than
       --machine-enter so the typing begins while the keyboard is still settling —
       waiting for it to land first leaves the headline blank long enough to read
       as a slow page rather than a sequence. main.js reads both off this element,
       so the timings cannot drift apart. */
    --machine-enter: 760ms;
    --type-start: 430ms;
    /* Declared HERE, on the desk, and not on .hero-sheet where it used to live.
       The paper's surface is .hero-lift::before, a SIBLING of the sheet — so a
       custom property set on the sheet is invisible to it, var(--stock) failed to
       substitute, and the entire background shorthand it sat in was discarded as
       invalid. The paper rendered with no surface at all, and every gradient in
       that shorthand — the backlight included — went down with it. */
    --stock: linear-gradient(
      180deg,
      color-mix(in srgb, var(--surface-raised) 96%, var(--bg)) 0%,
      var(--surface-raised) 100%
    );

    position: relative;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    min-width: 0;
  }

  /* Light out of the keyboard's slot. It rises from the groove, fans out, and is
     cut off by the paper — so what reads is a soft band bridging the gap, plus a
     wash on the paper's underside. z-index puts it over the keyboard (the light
     leaves the object) and under the sheet (it disappears behind the page it is
     lighting).

     It hangs off .hero-slot, a wrapper around the keyboard, for one specific
     reason: --keyboard-actual holds `min(64%, 40rem)`, and a percentage inside a
     custom property is resolved AT THE USE SITE — against width in `width`, but
     against HEIGHT in `bottom`. Anchoring the glow with it put the origin 74px
     off, burying the bright core inside the keyboard's case where nothing could
     see it. Against this wrapper, whose box IS the keyboard's box, `bottom: 94%`
     is exactly 6% down from the keyboard's top edge — where the slot sits — and
     stays exact at every size. */
  .hero-slot {
    position: relative;
    z-index: 1;
    /* Rises into frame on load. The overshoot in the curve is what makes it read
       as a machine being set down rather than a panel sliding up. `both` holds the
       first frame before it starts, so there is no flash of the settled position. */
    animation: machine-enter var(--machine-enter) cubic-bezier(0.22, 1.02, 0.3, 1.04) both;
  }

  @keyframes machine-enter {
    from {
      transform: translateY(46%);
      opacity: 0;
    }
    to {
      transform: translateY(0);
      opacity: 1;
    }
  }

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

  .hero-slot::before {
    content: "";
    position: absolute;
    z-index: 1;
    left: 50%;
    bottom: 94%;
    /* 64% of the keyboard, which is itself 64% of this wrapper. */
    width: 41%;
    /* Tall, not wide. Most of the shaft is hidden behind the paper — that is what
       is being conveyed — so it has to be long enough that the sliver showing in
       the gap reads as a section through a beam rather than a puddle of light. */
    aspect-ratio: 1.15 / 1;
    transform: translateX(-50%);
    /* Warm core FIRST, so it is topmost and owns the centre; the pink and cool
       lobes sit beneath it and offset either side, showing through only where the
       core has fallen away. Listed the other way round the pink simply covered
       everything, since background layers composite source-over. */
    background:
      radial-gradient(
        50% 92% at 50% 100%,
        var(--slot-glow) 0%,
        var(--slot-glow-far) 30%,
        transparent 78%
      ),
      radial-gradient(48% 88% at 36% 100%, var(--slot-pink) 0%, transparent 72%),
      radial-gradient(44% 84% at 65% 100%, var(--slot-cool) 0%, transparent 68%);
    filter: blur(16px);
    opacity: 0.9;
    mix-blend-mode: var(--slot-blend);
    pointer-events: none;
  }

  /* The keyboard's contact shadow — it is the only object here actually resting
     on anything. Derived from the keyboard's real width so it tracks at every
     breakpoint; aspect-ratio rather than a calc'd height, because
     --keyboard-actual holds a percentage and a percentage in a height calc
     resolves against a parent sized by its children. */
  .hero-desk::after {
    content: "";
    position: absolute;
    z-index: 0;
    left: 50%;
    bottom: -1%;
    width: calc(var(--keyboard-actual) * 0.88);
    aspect-ratio: 22 / 1;
    transform: translateX(-50%);
    border-radius: 50%;
    background: radial-gradient(closest-side, var(--machine-cast), transparent 74%);
    pointer-events: none;
    /* Fades in with the machine. It hangs off .hero-desk rather than .hero-slot, so
       it does not inherit the entrance and would otherwise sit on an empty patch of
       desk while the keyboard was still in the air. */
    animation: cast-settle var(--machine-enter) ease-out both;
  }

  @keyframes cast-settle {
    from { opacity: 0; }
    60%  { opacity: 0; }
    to   { opacity: 1; }
  }

  @media (prefers-reduced-motion: reduce) {
    .hero-desk::after {
      animation: none;
    }
  }

  /* --- The floating sheet -------------------------------------------------- */

  /* The float lives here and not on .hero-sheet because the sheet's transform is
     already used by the typewriter controller's carriage return, and two rules
     animating transform on one element cannot compose — the last one simply
     wins. */
  .hero-lift {
    position: relative;
    z-index: 3;
    /* The paper's width belongs HERE, not on .hero-sheet. The surface, the deckle,
       the shadow and the aura are all drawn off this element, so capping the sheet
       instead just made the content narrower inside a still-full-width sheet.
       Narrowing the paper also buys the aura more room before the hero's clip,
       which sits a fixed distance from the hero's edge, not the paper's.

       48rem is not arbitrary: it is the two columns (30rem + 13rem + their gap)
       plus the side padding. Any wider and `justify-content: center` on the sheet
       spreads the surplus into equal margins either side of the content, which is
       what was reading as a wide blank left edge — the padding was only ever half
       of that gap. */
    width: min(100%, 48rem);
    margin-inline: auto;
    /* The gap hangs off the lift, not the sheet. On the sheet, its bottom margin
       was counted inside the lift — making the lift 17px taller than the paper, so
       the surface at inset:0 hung that far past the paper's visible edge and ate
       most of the gap it was meant to be floating above. */
    margin-bottom: var(--float-gap);
    animation: hero-float 7s ease-in-out infinite;
    /* Hints the compositor once rather than every frame. */
    will-change: transform;
    /* The elevation shadow lives here, not on the sheet. The sheet's own box now
       paints no background, and a shadow on the sheet would sit ON TOP of the
       paper surface below it — a dark rectangle across the sheet. On the lift it
       paints behind everything the lift contains. */
    box-shadow: 0 1px 3px -2px rgb(0 0 0 / 0.1),
                0 16px 32px -26px rgb(0 0 0 / 0.24);
    border-radius: 4px;
  }

  /* The shaft continuing past the sheet. The through-light can only ever be seen
     INSIDE the paper's silhouette, so on its own the beam appeared to stop dead at
     the top edge. This sits behind the surface and is taller than it, so the light
     re-emerges in the band between the sheet and the nav — and it leaks through the
     notches in the deckle, which is a happy side effect of the edge being ragged.

     z-index:-1 is doing real work here: .hero-lift has a z-index, so it is a
     stacking context, and -1 puts this behind ::before (the opaque surface) while
     still inside the lift. Being occluded by the paper rather than tinting it is
     also why this layer needs no contrast check — it never sits under any text.

     Two lobes, not one: a narrow one at the source and a broad faint one high up.
     A single ellipse rises without widening, which is the one thing light escaping
     a slot does not do. */
  .hero-lift::after {
    content: "";
    position: absolute;
    z-index: -1;
    left: 50%;
    bottom: -6%;
    /* Only just wider than the paper, and that is a hard constraint rather than a
       taste call. The hero's `overflow: clip` cuts at its padding box, exactly
       32px outside the sheet — and that clip is load-bearing twice over: it crops
       the machine at the fold and it contains the hero parallax, so it cannot be
       widened or dropped. At 116% the aura reached 79px past the paper and the clip
       sliced it mid-gradient, leaving a hard vertical line down each side.
       
       102% keeps the aura's own box within 10px of the sheet, so together with the
       blur's tail it has faded to nothing before it ever reaches the clip. */
    width: 102%;
    height: 138%;
    transform: translateX(-50%);
    background:
      /* Far field: three weak lobes, each a different hue and offset from the next,
         so the light that has travelled furthest is also the most split. This is
         the rainbow — dispersion increases with distance, so it belongs up here and
         not at the slot, and it has to stay far fainter than the source or it reads
         as a coloured panel rather than diffused light. */
      radial-gradient(36% 36% at 34% 4%, var(--aura-pink) 0%, transparent 78%),
      radial-gradient(34% 34% at 50% 1%, var(--aura-warm) 0%, transparent 76%),
      radial-gradient(32% 32% at 66% 5%, var(--aura-cool) 0%, transparent 74%),
      /* Flanks: what wraps round the sheet's long edges. Their centres sit just
         INSIDE the paper, so most of each lobe is hidden behind it and only the
         outer sliver escapes — which is all the room the clip allows, and reads as
         light bending round an edge rather than a halo pinned to it. */
      radial-gradient(5% 54% at 1.5% 84%, var(--aura-pink) 0%, transparent 84%),
      radial-gradient(5% 54% at 98.5% 84%, var(--aura-cool) 0%, transparent 84%),
      /* Near field: the source itself, narrow and brighter. */
      radial-gradient(22% 62% at 50% 100%, var(--aura) 0%, transparent 76%);
    /* Heavier blur than the slot glow. Distance from the source is diffusion. */
    filter: blur(34px);
    mix-blend-mode: var(--slot-blend);
    pointer-events: none;
  }

  /* The paper's actual surface — stock, and a hint of warm light caught along the
     bottom edge from the slot below.
     
     It is a separate layer purely so the rough edge can exist: the deckle is an
     SVG displacement map, and a filter applied to the sheet itself would drag the
     headline through the same turbulence. Filtering only the surface leaves every
     glyph untouched. */
  .hero-lift::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 3px;
    background:
      /* Backlight — the shaft carrying on behind the sheet. The paper is opaque and
         full-width, so the only place that continuation can be seen is through the
         sheet itself.
         
         Tall and weak, not short and strong. An earlier version concentrated it into
         a band across the bottom third, which read as a spotlight thrown ONTO the
         paper. Light coming through from behind has no edge and no hotspot.

         The neutral layer's radius overshoots the sheet (132%) so it is still on its
         ramp at the top edge, under the nav, rather than having faded out mid-page —
         hence the four explicit stops, which stretch a falloff the gradient's own
         curve would otherwise rush. The pink and cool lobes stay low and offset from
         each other: dispersion belongs near the source, so the top of the sheet
         settles to a neutral warm. */
      radial-gradient(
        88% 132% at 50% 106%,
        var(--paper-through) 0%,
        color-mix(in srgb, var(--paper-through) 46%, transparent) 26%,
        color-mix(in srgb, var(--paper-through) 22%, transparent) 48%,
        color-mix(in srgb, var(--paper-through) 9%, transparent) 72%,
        transparent 100%
      ),
      radial-gradient(
        62% 70% at 37% 105%,
        var(--paper-lit-pink) 0%,
        color-mix(in srgb, var(--paper-lit-pink) 36%, transparent) 38%,
        transparent 82%
      ),
      radial-gradient(
        52% 58% at 66% 105%,
        var(--paper-lit-cool) 0%,
        transparent 78%
      ),
      var(--stock);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rule-strong) 60%, transparent);
    /* ±3px of wobble. Enough to stop the edge reading as a cut, not enough to
       look torn. */
    filter: url(#paper-edge);
    pointer-events: none;
  }

  @keyframes hero-float {
    0%,
    100% {
      transform: translateY(0) rotate(-0.14deg);
    }
    50% {
      transform: translateY(-9px) rotate(0.14deg);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .hero-lift {
      animation: none;
      will-change: auto;
    }
  }

  /* --- The sheet in the platen -------------------------------------------- */

  /* The paper. Nothing holds it, so it needs a full border, a radius on all four
     corners and a real elevation shadow: those are the only cues left for where
     its edges stop and how far off the desk it is. */
  .hero-sheet {
    position: relative;
    z-index: 2;
    isolation: isolate;
    --sheet-pad-x: clamp(var(--space-5), 2.4vw, var(--space-8));
    /* Every pixel here is a pixel of keyboard that stops clearing the fold, so
       these are held tighter than a loose sheet would be — but not so tight the
       type sits on the deckle. */
    --sheet-pad-y-top: clamp(var(--space-9), 3.6vw, var(--space-11));
    --sheet-pad-y-bottom: clamp(var(--space-6), 2.4vw, var(--space-8));
    width: 100%;
    margin: 0 auto;
    padding: var(--sheet-pad-y-top) var(--sheet-pad-x) var(--sheet-pad-y-bottom);
    /* No background, border or shadow: the surface and the edge are drawn by
       .hero-lift::before, which is filtered. This element is layout and content. */
    z-index: 1;
    transition: transform var(--dur-fast) var(--ease);
  }

  /* Paper texture. The sheet is a stacking context (position + z-index +
     isolation), so z-index:-1 lands the fibre above its own stock and below all
     of its content — no need to lift the headline. Two layers: fractal noise for
     pulp, and a wide cross-hatch for the laid weave. */
  .hero-sheet::before {
    content: "";
    position: absolute;
    z-index: -1;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: var(--fibre-opacity);
    mix-blend-mode: var(--fibre-blend);
    background-image:
      repeating-linear-gradient(
        90deg,
        var(--fibre-laid) 0 1px,
        transparent 1px 7px
      ),
      repeating-linear-gradient(
        0deg,
        var(--fibre-laid) 0 1px,
        transparent 1px 9px
      ),
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.6' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23f)'/%3E%3C/svg%3E"),
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='320'%3E%3Cfilter id='t'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.14' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='320' height='320' filter='url(%23t)' opacity='0.6'/%3E%3C/svg%3E");
    background-size: auto, auto, 180px 180px, 320px 320px;
  }

  /* The keyboard. Narrower than the paper above it, which is the right way round:
     a keyboard is a smaller object than the page you are typing.

     Held as a PROPORTION of the column rather than a fixed width, which is what
     lets the same rule hold at every size — 64% happens to be exactly the ratio
     the 40rem cap produces on desktop, so the relationship between paper and
     keyboard stays constant from 390px up instead of needing a breakpoint to
     restate it. */
  .hero-keyboard {
    position: relative;
    z-index: 0;
    display: block;
    width: 64%;
    max-width: var(--keyboard-w);
    height: auto;
    margin: 0 auto;
    pointer-events: none;
    user-select: none;
  }

  /* Short viewports have less fold to spend, so spend less of it on air. Only
     the sheet's own padding: trimming the hero's top padding too bought a few
     more percent of keyboard but left the headline 9px under the nav, which is
     a worse trade than a cropped bottom key row. */
  @media (max-height: 960px) and (min-width: 980px) {
    .hero-sheet {
      --sheet-pad-y-top: var(--space-8);
      --sheet-pad-y-bottom: var(--space-6);
    }
  }

  /* Three nested transforms, one job each: the button owns the perspective,
     .portrait-lift owns the paper tilt and the hover-in-the-air arc, and
     .portrait-flip owns the turn. Stacking them on one element would mean
     every animation had to restate the others. */
  /* Written in, not set. Same treatment as .iteration-cue on the case-study pages
     — hand, accent, a couple of degrees off square — so the one marginal voice on
     the site reads the same wherever it appears. The arrow points at the photo
     directly below it. */
  .hero-hand {
    position: relative;
    margin: 0;
    font-family: var(--font-hand);
    font-size: clamp(1.45rem, 1.5vw, 1.75rem);
    font-weight: 500;
    line-height: 1.1;
    /* Pink, not the accent blue the case-study cue uses: on this page the accent is
       reserved for interactive affordances, and this is a voice, not a control. */
    color: var(--hero-pink);
    transform: rotate(-2deg);
    transform-origin: left center;
  }

  /* Curves out from under the text and lands on the photo. currentColor so it can
     never drift from the handwriting it belongs to. */
  .hero-hand-arrow {
    display: block;
    width: 2.6rem;
    height: auto;
    margin: 0.1rem 0.9rem 0 auto;
    overflow: visible;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.6;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .hero-hand-arrow-tail {
    /* Slightly lighter than the head, the way a drawn stroke thins as it travels. */
    stroke-width: 2.2;
    opacity: 0.9;
  }

  /* Stacked layout: the aside sits left-aligned under the copy, so a right-aligned
     arrow points down past the photo instead of at it. */
  @media (max-width: 899px) {
    .hero-hand-arrow {
      margin-inline: 1.6rem auto;
    }
  }

  .hero-portrait-card {
    position: relative;
    z-index: 2;
    width: min(100%, 10rem);
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    aspect-ratio: 4 / 5;
    perspective: 1100px;
    cursor: pointer;
  }

  /* Cast shadow. Lives on the button, outside the 3D context, so it can
     soften and spread as the card rises instead of rotating with it. */
  .hero-portrait-card::after {
    content: "";
    position: absolute;
    left: 8%;
    right: 8%;
    bottom: -6px;
    height: 14px;
    border-radius: 50%;
    background: rgb(0 0 0 / 0.2);
    filter: blur(7px);
    opacity: 0.5;
    transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
    z-index: -1;
  }

  .portrait-lift {
    display: block;
    width: 100%;
    height: 100%;
    transform: rotate(-2.2deg);
    transform-style: preserve-3d;
    transition: transform var(--dur) var(--ease);
  }

  .hero-portrait-card:hover .portrait-lift,
  .hero-portrait-card:focus-visible .portrait-lift {
    transform: rotate(-2.2deg) translateY(-3px);
  }

  .portrait-flip {
    position: relative;
    display: block;
    width: 100%;
    height: 100%;
    transform-style: preserve-3d;
    /* Deliberate: a slow start, a long confident middle, a soft settle. */
    transition: transform 1.15s cubic-bezier(0.62, 0.02, 0.2, 1);
  }

  .hero-portrait-card[aria-pressed="true"] .portrait-flip {
    transform: rotateY(180deg);
  }

  /* The card rises off the paper, turns at the top of the arc, and comes back
     down. Runs for exactly the flip duration; main.js clears the class on
     animationend so hover keeps working afterwards. */
  .hero-portrait-card.is-flipping .portrait-lift {
    animation: portrait-hover 1.15s cubic-bezier(0.45, 0, 0.25, 1);
  }

  .hero-portrait-card.is-flipping::after {
    animation: portrait-shadow 1.15s cubic-bezier(0.45, 0, 0.25, 1);
  }

  @keyframes portrait-hover {
    0%   { transform: rotate(-2.2deg) translateY(0) scale(1); }
    38%  { transform: rotate(-2.2deg) translateY(-26px) scale(1.075); }
    62%  { transform: rotate(-2.2deg) translateY(-26px) scale(1.075); }
    100% { transform: rotate(-2.2deg) translateY(0) scale(1); }
  }

  @keyframes portrait-shadow {
    0%   { opacity: 0.5; transform: scale(1); }
    38%  { opacity: 0.22; transform: scale(1.22) translateY(6px); }
    62%  { opacity: 0.22; transform: scale(1.22) translateY(6px); }
    100% { opacity: 0.5; transform: scale(1); }
  }

  .portrait-face {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border: 1px solid color-mix(in srgb, var(--rule-strong) 72%, transparent);
    border-radius: 12px;
    background: var(--surface-raised);
    box-shadow: 0 12px 24px -24px rgb(0 0 0 / 0.22);
    -webkit-backface-visibility: hidden;
    backface-visibility: hidden;
  }

  /* backface-visibility hides a face visually but leaves it in the
     accessibility tree; visibility is what actually removes it. */
  .hero-portrait-card[aria-pressed="false"] .portrait-face--back,
  .hero-portrait-card[aria-pressed="true"] .portrait-face--front {
    visibility: hidden;
  }

  /* Night sky. Was a dozen CSS radial-gradient pinpricks over a navy vignette;
     it is a real starfield photograph now. The vignette stays, layered ON TOP of
     the image — the stat and caption sit over this, and an unmodified photo is too
     busy behind small type.

     The globe itself is untouched: it is the live SVG that portrait-globe.js
     rotates, sitting above this background. The source image has its own Earth in
     it, and the crop deliberately excludes it — the planet's glow starts at y=700
     of the 2400px original (measured, not guessed), so the asset is a 4:5 portrait
     taken from the clear sky above it. Cropping any lower put a second, static
     Earth in at the bottom of the card behind the turning one. */
  .portrait-face--back {
    gap: var(--space-2);
    padding: var(--space-4);
    transform: rotateY(180deg);
    border-color: rgb(255 255 255 / 0.14);
    background-color: #05070f;
    background-image:
      radial-gradient(
        124% 92% at 50% 24%,
        rgb(6 9 22 / 0.18) 0%,
        rgb(5 7 18 / 0.68) 72%,
        rgb(2 4 10 / 0.88) 100%
      ),
      url("../assets/hero/starfield.webp");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
  }

  /* Both sit over a starfield, so the number that matters is local contrast,
     not the average. Against the median ground the old caption already
     measured 12:1, but at 11px and 72% opacity a bright star behind a glyph
     wiped it out. Full opacity, a size up, and a soft dark halo that only
     shows where a star is actually behind the type. Scoped to the back face,
     so .portrait-caption elsewhere is untouched. */
  .portrait-face--back .portrait-stat {
    color: #fffdf8;
    font-size: 2rem;
    text-shadow: 0 1px 10px rgb(4 6 16 / 0.85);
  }

  .portrait-face--back .portrait-caption {
    color: #f2f1f6;
    font-size: 0.78rem;
    font-weight: var(--weight-medium);
    text-shadow: 0 1px 8px rgb(4 6 16 / 0.9);
  }

  .hero-portrait-card img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* --- Globe ---------------------------------------------------------------
     A pixel planet: portrait-globe.js paints a 72x72 buffer and this scales it
     up with nearest-neighbour, so the grid survives instead of being smoothed
     into a blurry sphere. Every colour lives in the script's palette — there is
     nothing here for CSS to theme, because a four-shade ramp is the artwork. */

  .portrait-globe {
    /* Exactly 2x the 72-cell buffer. A fractional scale would smear some cells
       across three device pixels and leave others on two, which reads as a
       wobble in the grid as the planet turns. */
    width: 144px;
    height: 144px;
    /* The whole point: no interpolation between the buffer's cells. */
    image-rendering: pixelated;
    image-rendering: crisp-edges;
  }

  .portrait-stat {
    color: var(--ink);
    font-family: var(--font-display);
    font-size: 1.6rem;
    font-variation-settings: "opsz" 40;
    line-height: 1;
  }

  .portrait-caption {
    color: var(--ink-secondary);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    line-height: 1.3;
    text-align: center;
    text-transform: uppercase;
  }

  /* --- Pointer tag ----------------------------------------------------------
     Figma-style: an arrow with a label riding just off its tip. JS writes the
     pointer position into --cx/--cy; the element is otherwise inert. */

  .portrait-cursor {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 3;
    display: flex;
    align-items: flex-start;
    gap: 2px;
    opacity: 0;
    transform: translate3d(var(--cx, 0px), var(--cy, 0px), 0) scale(0.86);
    transform-origin: top left;
    transition: opacity 0.14s var(--ease), transform 0.14s var(--ease);
    pointer-events: none;
    will-change: transform;
  }

  .hero-portrait-card:hover .portrait-cursor,
  .hero-portrait-card:focus-visible .portrait-cursor {
    opacity: 1;
    transform: translate3d(var(--cx, 0px), var(--cy, 0px), 0) scale(1);
  }

  /* The real cursor would sit alongside ours; hide it and let the tag be it. */
  @media (hover: hover) and (pointer: fine) {
    .hero-portrait-card { cursor: none; }
  }

  .portrait-cursor-arrow {
    width: 14px;
    height: 14px;
    flex: none;
    fill: var(--accent);
    stroke: #fff;
    stroke-width: 0.9;
    filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.3));
  }

  .portrait-cursor-tag {
    margin-top: 9px;
    padding: 3px 7px 4px;
    border-radius: 5px;
    background: var(--accent);
    color: #fff;
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: var(--weight-medium);
    letter-spacing: 0.04em;
    line-height: 1;
    white-space: nowrap;
    text-transform: uppercase;
    box-shadow: 0 2px 6px rgb(0 0 0 / 0.22);
  }

  /* No hover on touch, so the tag parks in the corner as a static badge. */
  @media (hover: none) {
    .portrait-cursor {
      top: auto;
      left: auto;
      right: 6px;
      bottom: 6px;
      opacity: 1;
      transform: none;
    }
    .portrait-cursor-arrow { display: none; }
    .portrait-cursor-tag { margin-top: 0; }
  }

  @media (prefers-reduced-motion: reduce) {
    .portrait-flip,
    .portrait-lift { transition: none; }
    .hero-portrait-card.is-flipping .portrait-lift,
    .hero-portrait-card.is-flipping::after { animation: none; }
  }

  /* Carriage return: the platen advances a line and the sheet settles. */
  .hero-sheet.is-return {
    transform: translateY(-2px);
  }

  @media (min-width: 900px) {
    /* The paper holds both columns: the lede, and the portrait and contents. */
    .hero-sheet {
      display: grid;
      /* auto, not 1fr. With 1fr the lede column absorbs all the leftover width
         and the two columns are pushed to opposite edges of the paper, leaving a
         canyon of white between the copy and the portrait. Capping the lede and
         centring the pair keeps them adjacent without stranding the aside off to
         one side. */
      /* 30rem, down from 34: the measured gap between the columns was already only
         15px, so what read as a canyon was the lede column's UNUSED width, not the
         gap. Capping the text column tighter is what actually moves the portrait
         in. */
      grid-template-columns: minmax(0, 30rem) 13rem;
      column-gap: var(--space-3);
      justify-content: center;
      /* centre, not start: with the contents index gone the aside is just the
         portrait, and top-aligning it left it stranded up against the headline
         with dead paper underneath. */
      align-items: center;
    }

    .hero-sidebar {
      align-items: stretch;
      padding-top: var(--space-2);
    }

    .hero-portrait-card {
      width: 100%;
      margin-top: 0;
    }
  }

  /* --- The portrait must clear the fold on a phone ------------------------
     The hero is capped at --fold-h and set to `overflow: clip` so the
     typewriter runs off the bottom edge. That crop is the composition on a
     desktop, where the thing being cut is the machine. On a phone the sheet
     stacks and the portrait is the LAST thing in it, so the same crop lands on
     the photo — and inside an in-app browser (Instagram, LinkedIn), where the
     top bar and share bar take 150-200px of viewport, it cut through the face.

     Two rules, because they answer two different questions. The first removes
     the cap so nothing in the sheet is ever clipped, whatever the height. The
     second only fires when the viewport is genuinely short, and buys the room
     back from the sheet's own padding and the card's width rather than from
     the type, so the headline keeps its size. */
  @media (max-width: 640px) and (max-height: 800px) {
    .hero-sheet {
      --sheet-pad-y-top: var(--space-5);
      --sheet-pad-y-bottom: var(--space-3);
    }

    .hero-portrait-card {
      width: min(100%, 8rem);
    }
  }

  /* An in-app browser on a small phone. The hand-written line and its arrow are
     the only things between the copy and the photo, so their gap is what gets
     spent before the card itself is made smaller. */
  @media (max-width: 640px) and (max-height: 680px) {
    .hero-sheet {
      --sheet-pad-y-top: var(--space-4);
      --sheet-pad-y-bottom: var(--space-2);
    }

    .hero-hand {
      margin-block: var(--space-1) 0;
    }

    .hero-portrait-card {
      width: min(100%, 7rem);
    }
  }

  /* --- Language swap ------------------------------------------------------
     The English face stays in normal flow and alone determines the line box,
     so line breaks, baselines and the trailing comma sit exactly where they
     did before this existed. The alternate script is absolutely positioned
     on top — it can never reflow the headline, whatever its metrics.
     They turn on X like a split-flap: one leaves as the other arrives. */

  .lang-swap {
    position: relative;
    display: inline-block;
  }

  /* Only once lang-swap.js has measured both faces. Without the class the box
     stays auto-width, so no-JS and reduced-motion readers are unaffected. */
  .lang-swap.has-measured {
    width: var(--w-en);
    transition: width 0.52s cubic-bezier(0.5, 0, 0.2, 1);
  }

  .lang-swap.has-measured.is-alt {
    width: var(--w-alt);
  }

  /* While the widths are being taken over or re-measured, the box must not
     animate — otherwise the handover reads as a movement of its own. */
  .lang-swap.is-sizing { transition: none; }

  @media (prefers-reduced-motion: reduce) {
    .lang-swap.has-measured { transition: none; }
  }

  .lang-swap > .lang-face {
    display: block;
    transform-origin: center center;
    backface-visibility: hidden;
    transition:
      transform 0.52s cubic-bezier(0.5, 0, 0.2, 1),
      opacity 0.26s var(--ease);
  }

  .lang-face--en { transform: rotateX(0deg); opacity: 1; }

  /* Fraunces has no Bengali or Kannada glyphs, so the alternate faces bring
     their own serif. Set smaller and on its own line-height: Indic ascenders
     and descenders overrun the headline's 0.96 leading at matched size. */
  .lang-face--alt {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    /* 1em — the same size as the serif it replaces, full stop. It started at
       0.78em to keep Indic ascenders inside the headline's leading, but the
       face is position:absolute with inset:0 and align-items:center, so it
       cannot push the line box in the first place; the only thing the smaller
       size bought was a native script that read as a caption next to the
       English. Measured: the marks clear the second line by 12px. */
    font-size: 1em;
    font-weight: 500;
    /* The headline's full -0.02em drags vowel signs off the consonants they
       attach to, but `normal` left the Indic faces visibly looser than the
       English they flip from. -0.008em splits it: tight enough to match, small
       enough that the matras stay attached. */
    letter-spacing: -0.008em;
    line-height: 1;
    white-space: nowrap;
    transform: rotateX(-90deg);
    opacity: 0;
  }

  .lang-swap.is-alt .lang-face--en  { transform: rotateX(90deg); opacity: 0; }
  .lang-swap.is-alt .lang-face--alt { transform: rotateX(0deg);  opacity: 1; }

  /* Anek's per-script cuts, which share one skeleton across Indic scripts, so
     Bengali and Kannada turn over with matching weight and proportion. */
  .lang-face--alt[lang="bn"] { font-family: "Anek Bangla", "Noto Sans Bengali", sans-serif; }
  .lang-face--alt[lang="kn"] { font-family: "Anek Kannada", "Noto Sans Kannada", sans-serif; }

  @media (prefers-reduced-motion: reduce) {
    .lang-swap > .lang-face { transition: none; }
  }

  /* --- Typed lines ------------------------------------------------------- */

  .hero-type .type-line {
    display: block;
  }

  .type-char {
    position: relative;
  }

  /* Only .is-typing hides anything, and only JS adds it — so no-JS, a JS
     error and reduced-motion all land on a fully readable hero. */
  .is-typing .type-char {
    opacity: 0;
  }

  .is-typing .type-char.is-on {
    opacity: 1;
  }

  /* Slim caret, absolutely placed so it never shifts the line. Sized in em so
     it stays proportional as the headline scales down the breakpoints. */
  .is-typing .is-cursor::after {
    content: "";
    position: absolute;
    left: calc(100% + 0.04em);
    top: 0.16em;
    width: 0.05em;
    min-width: 2px;
    height: 0.66em;
    background: currentColor;
    animation: caret-blink 530ms steps(1, end) infinite;
  }

  .is-caret-done .is-cursor::after {
    animation: none;
    opacity: 0;
    transition: opacity var(--dur) var(--ease);
  }

  @keyframes caret-blink {
    0%, 50%   { opacity: 1; }
    50.01%, 100% { opacity: 0; }
  }

  /* Everything that isn't typed waits, then arrives together. */
  .is-typing .hero-amp,
  .is-typing .role-tag,
  .is-typing .home-hero-copy,
  .is-typing .stack-rail,
  .is-typing .hero-hand,
  .is-typing .hero-rail,
  .is-typing .hero-index {
    opacity: 0;
    transform: translateY(6px);
  }

  .is-typed .hero-amp,
  .is-typed .role-tag,
  .is-typed .home-hero-copy,
  .is-typed .stack-rail,
  .is-typed .hero-hand,
  .is-typed .hero-rail,
  .is-typed .hero-index {
    opacity: 1;
    transform: none;
    transition: opacity var(--dur-slow) var(--ease),
                transform var(--dur-slow) var(--ease);
  }

  /* Struck by the same machine as the headline. */
  .hero-home[data-typewriter] .home-hero-copy {
    font-family: var(--font-body);
    font-size: clamp(1rem, 0.2vw + 0.9rem, 1.125rem);
    line-height: 1.55;
    letter-spacing: -0.012em;
  }

  /* --- Mobile: the hero scrolls away instead of being covered ---------------
     On a pointer-sized screen the pinned hero and the sheet riding over it read
     as a handoff, because there is room to watch it happen. On a phone the
     sheet fills the viewport within one flick, so the pinned hero was simply
     hidden underneath it — typewriter, portrait and scribble all still on
     screen geometrically, all covered, with nothing to say they were there.
     Measured: the portrait was covered by 200px of scroll at 390px wide.

     Below 900px the hero goes back into normal flow, so it leaves the screen
     the way a reader expects, and Selected Work stops overlapping it. */
  @media (max-width: 900px) {
    .portfolio-home .hero-home[data-typewriter] {
      position: static;
      z-index: auto;
    }

    .portfolio-home #work {
      margin-top: 0;
      /* The top edge and upward shadow belong to the overlap. With nothing to
         ride over they only draw a line across the middle of the page. */
      box-shadow: none;
      border-radius: 0;
      background: var(--bg);
    }
  }
}

/* --- Selected work: the next sheet feeds over the hero -------------------- */

@layer components {
  /* Rides up over the fold with its own top edge and an upward shadow, so it
     reads as a second sheet sliding in rather than the page simply continuing.
     Uses --bg (not --surface-raised) so everything below it is seamless. */
  .portfolio-home #work {
    position: relative;
    z-index: 4;
    margin-top: calc(var(--space-16) * -1);
    /* Tight enough that the eyebrow lands inside the peeking slice. */
    padding-top: var(--space-16);
    border-radius: 2px 2px 0 0;
    /* The ground now carries the parchment tint, so the sheet only needs a
       hairline and a short lift — the old 40px spread read as a grey band
       smeared across the dot grid rather than an edge. */
    background:
      linear-gradient(rgb(28 27 25 / 0.022), rgb(28 27 25 / 0) 10px),
      var(--bg);
    box-shadow:
      0 -1px 0 rgb(0 0 0 / 0.05),
      0 -8px 18px -14px rgb(0 0 0 / 0.18);
  }

  /* Narrower than the shared --col-wide the rest of the site uses. Scoped to the
     home page's work section rather than changing that token, which 58 other call
     sites read. Brings the work rows closer to the hero paper's measure (48rem) so
     the page has one column edge rather than two. */
  .portfolio-home #work > .container--media {
    max-width: 56rem;
  }

  @media (max-width: 640px) {
    .portfolio-home #work {
      margin-top: calc(var(--space-10) * -1);
      padding-top: var(--space-12);
    }

    /* Less vertical room to spare on a phone, so peek a smaller slice. Declared on
       .portfolio-home, not on the hero: the backdrop that also reads --fold-h hangs
       off .fold-stage, which is the hero's PARENT and could not see a token set on
       the hero itself. */
    .portfolio-home {
      --fold-h: calc(100vh - 64px);
      --fold-h: calc(100svh - 64px);
    }

    /* min only. The cap used to be here too, which is what cropped the portrait:
       the sheet stacks on a phone and the photo is the last thing in it, so
       `overflow: clip` at --fold-h cut through the face on any short viewport —
       most visibly inside an in-app browser, where the top and share bars take
       150-200px. The floor still holds the fold; the hero is now free to grow
       past it rather than clip its own content. */
    .hero-home[data-typewriter] {
      min-height: var(--fold-h);
      max-height: none;
    }
  }

  @media (min-width: 1180px) {
    .hero-home .display-1 {
      max-width: min(18ch, 100%);
      font-size: clamp(1.625rem, 1.9vw, 2.125rem);
    }

    .hero-emphasis {
      max-width: min(14ch, 100%);
      font-size: clamp(1.55rem, 1.7vw, 1.95rem);
    }
  }
}

/* --- Side-project tiles: description lives in the tile ---------------------
   The blurb sits in the panel's empty middle band rather than under it. On a
   pointer device it waits for hover; on touch there is no hover to wait for,
   so it is simply always there. It is never display:none, so it stays in the
   accessibility tree and is read either way. */

@layer components {
  .project-panel-desc {
    max-width: 34ch;
    margin: 0;
    color: color-mix(in srgb, currentColor 78%, transparent);
    font-size: var(--text-base);
    line-height: var(--leading-relaxed);
    text-wrap: pretty;
  }

  @media (hover: hover) and (pointer: fine) {
    .project-panel-desc {
      opacity: 0;
      transform: translateY(6px);
      transition:
        opacity var(--dur) var(--ease),
        transform var(--dur) var(--ease);
    }

    /* Keyboard users get it too — the tile is the focusable element. */
    .support-card--tile:hover .project-panel-desc,
    .support-card--tile:focus-visible .project-panel-desc {
      opacity: 1;
      transform: none;
    }
  }

  /* Belt and braces: some touch devices still report (hover: hover), and a
     narrow window on a laptop is phone-shaped regardless of pointer. At this
     width the blurb is always in the card. Comes after the hover block so it
     wins on equal specificity. */
  @media (max-width: 640px) {
    .project-panel-desc {
      opacity: 1;
      transform: none;
      font-size: var(--text-sm);
      max-width: none;
    }
  }
}

/* ==========================================================================
   ABOUT PAGE
   Portrait, travel grid, reads. Built from existing tokens and the same paper
   surfaces as the landing page so the two read as one site.
   ========================================================================== */

@layer components {

  .about-page .about-intro {
    display: grid;
    align-items: start;
    gap: var(--space-10);
  }

  @media (min-width: 900px) {
    .about-page .about-intro {
      grid-template-columns: minmax(0, 1fr) 15rem;
      column-gap: var(--space-16);
    }
    /* The eyebrow spans both columns so the portrait aligns to the headline. */
    .about-page .about-intro > .eyebrow { grid-column: 1 / -1; }
  }

  .about-portrait {
    margin: 0;
    border-radius: 2px;
    overflow: hidden;
    background: var(--tint);
    box-shadow: 0 -1px 0 rgb(0 0 0 / 0.05), 0 12px 28px -20px rgb(0 0 0 / 0.28);
  }

  .about-portrait img {
    display: block;
    width: 100%;
    height: auto;
  }

  .about-lede {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-5);
    max-width: var(--measure);
  }

  .about-lede p {
    color: var(--ink-secondary);
    font-size: var(--text-md);
    line-height: var(--leading-relaxed);
  }

  /* The one line she calls out on the original, so it gets the display face. */
  .about-pullquote {
    max-width: 20ch;
    color: var(--ink);
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    line-height: var(--leading-snug);
    letter-spacing: var(--tracking-display);
  }

  .about-pullquote em {
    color: var(--hero-pink);
    font-style: normal;
  }

  /* --- Travel grid ------------------------------------------------------- */

  .photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--space-6);
  }

  .photo-card {
    position: relative;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }

  .photo-card img {
    display: block;
    width: 100%;
    height: 100%;
    /* A common ratio keeps a mixed portrait/landscape set on one baseline. */
    aspect-ratio: 4 / 5;
    object-fit: cover;
    border-radius: 2px;
    background: var(--tint);
    box-shadow: 0 -1px 0 rgb(0 0 0 / 0.05), 0 12px 28px -20px rgb(0 0 0 / 0.28);
    transition: transform var(--dur) var(--ease);
  }

  .photo-card:hover img {
    transform: translateY(-3px);
  }

  .photo-card figcaption {
    color: var(--ink-muted);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    transition: color var(--dur-fast) var(--ease);
  }

  .photo-card:hover figcaption {
    color: var(--hero-pink);
  }

  /* --- Reads ------------------------------------------------------------- */

  .about-reads {
    display: grid;
    align-items: center;
    gap: var(--space-10);
  }

  @media (min-width: 760px) {
    .about-reads { grid-template-columns: 16rem minmax(0, 1fr); }
  }

  /* --- The book ---------------------------------------------------------
     Hinged on its spine: the cover swings open a few degrees on hover and
     the shadow deepens as it lifts off the page. Perspective lives on the
     figure so the rotation reads as depth rather than a skew. */

  .photo-card--reads {
    perspective: 1400px;
    perspective-origin: left center;
  }

  .photo-card--reads img {
    aspect-ratio: 3 / 5;
    transform-origin: left center;
    transform-style: preserve-3d;
    will-change: transform;
    transition:
      transform var(--dur-slow) var(--ease),
      box-shadow var(--dur-slow) var(--ease);
  }

  /* The spine: a dark seam down the hinge edge, revealed as it opens. */
  .photo-card--reads::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 10px;
    border-radius: 2px 0 0 2px;
    background: linear-gradient(
      90deg,
      rgb(28 27 25 / 0.28),
      rgb(28 27 25 / 0.05) 60%,
      transparent
    );
    opacity: 0;
    transition: opacity var(--dur-slow) var(--ease);
    pointer-events: none;
    z-index: 1;
  }

  @media (hover: hover) and (pointer: fine) {
    .photo-card--reads:hover img,
    .photo-card--reads:focus-within img {
      transform: rotateY(-22deg) translateZ(6px);
      box-shadow:
        0 -1px 0 rgb(0 0 0 / 0.05),
        0 26px 44px -22px rgb(0 0 0 / 0.45);
    }

    .photo-card--reads:hover::before,
    .photo-card--reads:focus-within::before {
      opacity: 1;
    }
  }

  /* The whole effect is decorative motion — drop it entirely if unwanted. */
  @media (prefers-reduced-motion: reduce) {
    .photo-card--reads img { transition: none; transform: none; }
    .photo-card--reads::before { display: none; }
  }

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

  .about-genres {
    max-width: 30ch;
    color: var(--ink);
    font-family: var(--font-display);
    font-size: var(--text-xl);
    line-height: var(--leading-snug);
  }

  @media (max-width: 640px) {
    .photo-grid { gap: var(--space-5); }
    .about-page .about-intro { gap: var(--space-8); }
  }
}

/* ==========================================================================
   LONG-FORM PROSE — case studies
   Hierarchy inside a beat: the opening paragraph carries the claim at lead
   size, the rest sit at body size. Measure is tightened from 68ch so lines
   break where the eye expects, and body prose moves off --ink-muted, which
   was failing contrast at caption weight.
   ========================================================================== */

@layer components {

  /* The beat is a 4ch label column + a copy column. With more than one
     paragraph, auto-placement drops the second into the 4ch column and it
     renders one word per line — so pin every paragraph to column 2. */
  .beat > p {
    grid-column: 2;
  }

  .beat p {
    max-width: 62ch;
    color: var(--ink-secondary);
    font-size: var(--text-md);
    line-height: var(--leading-relaxed);
    margin-bottom: var(--space-6);
    text-wrap: pretty;
  }

  /* The lead: first paragraph of a beat states the move, so it gets size,
     full ink, and tighter leading to read as a statement not a block. */
  .beat p:first-of-type {
    color: var(--ink);
    font-size: var(--text-lg);
    line-height: var(--leading-snug);
    letter-spacing: -0.005em;
  }

  /* A hairline marks where the lead ends and the detail begins. */
  .beat p:first-of-type:not(:last-child) {
    padding-bottom: var(--space-6);
    border-bottom: 1px solid var(--rule);
  }

  /* Caption-weight prose was --ink-muted at roughly 3:1 — under the 4.5:1
     floor for body text. Muted stays for labels and meta only. */
  .version-card-caption,
  .sequence-step-note,
  .figure-caveat {
    color: var(--ink-secondary);
  }

  .section-intro {
    max-width: 62ch;
    text-wrap: pretty;
  }

  @media (max-width: 640px) {
    .beat p:first-of-type { font-size: var(--text-md); }
    /* Single-column below 640, so there is no column 2 to pin to. */
    .beat > p { grid-column: auto; }
  }
}

/* ==========================================================================
   MOBILE PASS
   Measured at 360 and 390px: every rule below fixes a specific finding, not a
   general tidy. Two kinds of problem — controls too small to hit reliably, and
   micro type set at desktop sizes that phone distance cannot carry.
   ========================================================================== */
@layer components {
  @media (max-width: 720px) {
    /* --- Tap targets --------------------------------------------------
       44px is the floor for a control you hit with a thumb. These measured
       21-32px. Where a control has its own visual size (the theme pill, the
       burger box) the box itself grows; where it is a text link, padding does
       the work so the type stays put and the hit area centres on it. */
    .nav-toggle {
      width: 44px;
      height: 44px;
    }

    .theme-toggle {
      height: 44px;
      padding-inline: var(--space-2);
    }

    .nav-logo,
    .nav-cta,
    .nav-links a {
      display: inline-flex;
      align-items: center;
      min-height: 44px;
    }

    .hero-rail-item,
    .back-link,
    .footer-links a,
    .closing-links a {
      display: inline-flex;
      align-items: center;
      min-height: 44px;
    }

    /* --- Micro type ---------------------------------------------------
       11px and below is the band that fails at phone distance. The home page
       tokens are not shrunk on mobile the way the case-study ones were, so
       this is only the handful of hardcoded sizes that sat under the scale. */
    :root {
      --text-2xs: 0.75rem;   /* 11 -> 12: eyebrows, read time, tool names */
    }

    /* 0.66rem = 10.6px, the smallest type on the site. It labels the stack
       bubbles, which on touch have no hover to reveal them anyway. */
    .stack-pill-label {
      font-size: 0.75rem;
    }

    .portrait-cursor-tag {
      font-size: 0.75rem;
    }

    .project-panel-cta {
      font-size: var(--text-xs);
    }

    /* Sentences rather than labels, so they take the caption step, not the
       label step: the framer note is a full line of prose and the photo
       captions name a place. */
    .about-framer-note,
    .photo-card figcaption {
      font-size: var(--text-sm);
    }

    /* Sentences, not labels — these carry actual prose and were sitting at
       13px, a step below the body copy they support. */
    .project-panel-desc,
    .case-card-arrow {
      font-size: var(--text-base);
    }
  }
}
