/* ── Google Fonts ───────────────────────────────────────────────── */
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@300;400;500&display=swap');

/* ── Reset & Base ───────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  /* True neutrals */
  --black: #0a0a0a;
  --white: #ffffff;
  --white-15: rgba(255, 255, 255, 0.15);
  --white-20: rgba(255, 255, 255, 0.20);
  --white-45: rgba(255, 255, 255, 0.45);
  --white-70: rgba(255, 255, 255, 0.70);
  --white-90: rgba(255, 255, 255, 0.90);
  --black-10: rgba(10, 10, 10, 0.10);
  --black-18: rgba(10, 10, 10, 0.18);
  --off-white: #efece7;
  --mid: #5a5a5a;
  --light: #d0ceca;
  --gray-text: #3a3a3a;
  --body-text: #111827;

  /* Warm neutrals */
  --brown: #7e6c5e;
  --neutral-border: #c3bab0;
  --note: #b0a299;

  /* Other colors */
  --red: #90203a;
  --gold: #d69e1b;
  --gold-accent: #cf9714;
  --green: #798a27;
  --dark-brown: #2f1604;

  /* Hero gradient (homepage only) — the blurred color wash behind the
     hero and its scroll-out transition. --dark-brown and --gold do
     double duty here (hero base / title default); these two are new,
     pastel washes with no equivalent elsewhere in the palette. */
  --grad-wine: #952b54;
  --grad-gold: #c99c53;
  --grad-sage: #d0dbc3;

  /* Case study background blobs (see .bg-blob below) — each is a
     40-50%-opacity color used as one end of a two-color gradient. */
  --blob-sage: rgba(187, 212, 136, 0.4);
  --blob-gold-deep: rgba(206, 150, 19, 0.4);
  --blob-blue: rgba(22, 95, 167, 0.4);
  --blob-wine: rgba(159, 40, 83, 0.4);
  --blob-rust: rgba(153, 82, 26, 0.4);
  --blob-red-orange: rgba(235, 79, 40, 0.5);
  --blob-gold: rgba(255, 178, 0, 0.5);

  --font-serif: 'Instrument Serif', serif;
  --font-sans: 'Inter', sans-serif;
  --nav-h: 64px;
  --side-pad: 64px;

  /* Two page-layout widths, both centred: wide (--max-w) and narrow
     (--content-w). Each names the CONTENT width, inside the side padding.
     A wrapper that carries its own horizontal padding sets
     max-width: var(--max-w-outer) so its content still lands on --max-w;
     one whose padding lives on a parent (e.g. .nav-inner) uses --max-w
     directly. --content-w-md is a component width, not a layout width. */
  --max-w: 1290px;
  --max-w-outer: calc(var(--max-w) + var(--side-pad) * 2);
  --content-w: 800px;
  --content-w-md: 1000px;
}

/* overflow-x: clip on both html and body — the homepage's
   .hero-atmosphere is deliberately wider than the viewport at some
   widths (its blobs are fixed-px sized, not scaled to fit; see its
   own long note), which would otherwise add a horizontal scrollbar/
   let the page pan sideways to reveal it. Belt-and-suspenders on both
   elements since browsers don't agree on which one's overflow governs
   the actual viewport scrollbar. `clip`, not `hidden`: setting only
   overflow-x to a non-visible value forces the other axis's computed
   value to `auto` too (they're coupled per spec) unless the value is
   `clip`/`visible` — and an `auto` body becomes a scroll container,
   which breaks `position: sticky` for every descendant (e.g. the
   cleanhub case study's .ch-annotated-inner). */
html { font-size: 16px; scroll-behavior: smooth; overflow-x: clip; }

body {
  font-family: var(--font-sans);
  background-color: var(--off-white);
  color: var(--black);
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  overflow-x: clip;
}

a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; }

/* ── Narrow content section (reusable — case-study layout) ─────────
   For sections that are just text (no side-by-side media, custom
   grids, or full-bleed elements): centers the section to --content-w
   instead of letting it stretch to the page's full --max-w. Add
   alongside a page's own section class, e.g.
   <section class="ch-section section-narrow"> */
.section-narrow {
  max-width: var(--content-w);
  margin-left: auto;
  margin-right: auto;
}

/* ── Typography: Headings (site-wide — exactly 3 styles) ──────────
   Existing per-page classes (.hero-statement, .mind-title, .ch-title,
   .about-h1, .mind-h2, .ch-h2, .ch-iteration-title, .mind-iteration-title,
   .card-title, .ch-sticky-title) keep only their layout-specific
   concerns (margin, max-width) and pair with one of these for type. */
.h1-display {
  font-family: var(--font-serif);
  font-size: 64px;
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1.19;
  color: var(--black);
}
.h2-section {
  font-family: var(--font-serif);
  font-size: 48px;
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--black);
}
.h3-sub {
  font-family: var(--font-sans);
  font-size: 24px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--black);
}
.h3-serif {
  font-family: var(--font-serif);
  font-size: 32px;
  font-weight: 400;
  line-height: 40px;
  color: var(--black);
}

/* ── Typography: Body copy (site-wide — 2 styles) ─────────────────
   .mind-p and .ch-p keep only their layout concerns (margin, max-width)
   and pair with .body-regular for type. */
.body-big {
  font-family: var(--font-sans);
  font-size: 20px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--black);
}
.body-regular {
  font-family: var(--font-sans);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--black);
}

/* ── Background blobs (case study decoration) ─────────────────────
   Figma node 1325:33550 ("Blobs"): 4 rounded rectangles, each
   1098x936 with a corner radius (542.541px) bigger than half of
   either side, which CSS clamps to a full ellipse — same trick as
   .cursor-blob-inner's circular gradient below. Each is a plain
   top-to-bottom linear gradient between two of the site's brand
   colors at 40-50% opacity, blurred 180.847px (~181px). One color
   pair — red-orange → gold — is the same gradient .cursor-blob-inner
   already uses (style-linear-gradient.css), just at a lower opacity
   and a much heavier blur; the token names below are shared so both
   can reference the same values.
   .bg-blob is layout only (size, shape, blur) — each modifier below
   only sets the gradient, matching the "shared class carries the
   look, instance carries position" split used elsewhere (e.g.
   .hero-atmosphere/.atm-blob). Position, and z-index relative to
   each page's own content, are set per case study, where these get
   placed just above the flat --off-white page background. */
.bg-blob {
  position: absolute;
  width: 1098px;
  height: 936px;
  border-radius: 50%;
  filter: blur(181px);
  pointer-events: none;
  z-index: -1;
}
/* Rectangle 65 */
.bg-blob--sage-gold {
  background: linear-gradient(to bottom, var(--blob-sage) 13.113%, var(--blob-gold-deep) 59.509%);
}
/* Rectangle 228 */
.bg-blob--sage-blue {
  background: linear-gradient(to bottom, var(--blob-sage) 29.808%, var(--blob-blue) 75.962%);
}
/* Rectangle 229 */
.bg-blob--wine-rust {
  background: linear-gradient(to bottom, var(--blob-wine) 22.276%, var(--blob-rust) 85.737%);
}
/* Rectangle 64 — same gradient as .cursor-blob-inner, dimmer + blurred harder */
.bg-blob--red-gold {
  background: linear-gradient(to bottom, var(--blob-red-orange) 13.113%, var(--blob-gold) 59.509%);
  opacity: 0.8;
}
/* 5th variant, not from Figma node 1325:33550 — a plain flat --white,
   no gradient. Sits behind every pull quote (.mind-pull-quote/
   .ch-pull-quote/.dash-pull-quote) as a soft lightening wash rather
   than an accent color, since a pull quote is a highlighted callout,
   not a section transition. */
.bg-blob--white {
  background: var(--white);
}

/* Section eyebrow — merges .cs-section-label and .ch-section-label,
   based on .cs-section-label with font-size bumped to 12px (the .cs-*
   page has since been rebuilt as .mind-*, which follows the same
   pattern). Per-page label classes keep only their margin-bottom and
   pair with this for type. */
.section-eyebrow {
  font-family: var(--font-sans);
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mid);
}

/* ── Scroll reveal (homepage) ───────────────────────────────────────
   Same class-toggle pattern as .mind-reveal in css/mind.css: opacity/
   transform start here in the stylesheet (not inline), and js/home.js
   just adds .is-visible on first scroll-into-view. Class-based rather
   than the inline-style approach the rest of home.js's reveal system
   uses, because .fragment-item also needs CSS to control its opacity
   later for the hover-dim group (.fragments-media below) — an inline
   style set by JS would permanently outrank that stylesheet rule. */
.home-reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}
.home-reveal.is-visible { opacity: 1; transform: translateY(0); }

/* ── Navigation ─────────────────────────────────────────────────── */
nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  padding: 0 var(--side-pad);
  z-index: 100;
  background-color: rgba(239,236,231,0.85);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.nav-inner {
  width: 100%;
  max-width: var(--max-w);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-serif);
  font-size: 22px;
  letter-spacing: -0.02em;
}

/* Home nav — transparent over the dark hero (no bright bar breaking
   the gradient), off-white text to match. Once scroll passes the
   hero it picks up .is-scrolled (toggled in home.js) and becomes the
   same opaque light bar every other page uses, since by then it's
   sitting over the light work section and needs the backdrop + dark
   text back for contrast. */
.nav-home {
  background-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
}
.nav-home .nav-brand,
.nav-home .nav-link {
  color: var(--off-white);
  transition: color 0.3s ease, opacity 0.45s ease-in-out;
}
.nav-home .nav-brand .divider {
  background: var(--off-white);
  transition: background-color 0.3s ease;
}
.nav-home.is-scrolled {
  background-color: rgba(239,236,231,0.85);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.nav-home.is-scrolled .nav-brand,
.nav-home.is-scrolled .nav-link {
  color: var(--black);
}
.nav-home.is-scrolled .nav-brand .divider { background: var(--black); }

/* Case-study nav — same transparent-until-scrolled idea as .nav-home,
   for a hero that tucks under the nav (see e.g. .mind-hero), but
   without the text-color swap: these heroes sit on a light gradient
   over --off-white, not a dark image, so the default dark nav text
   (inherited from body) already reads fine transparent or opaque.
   .is-scrolled here is toggled at a fixed 200px scroll distance
   (mind.js and friends), not the hero's own height like .nav-home's —
   a case-study hero isn't a fixed viewport-height section to key off
   the same way the homepage's is. */
.nav-overlay {
  background-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition: background-color 0.3s ease, backdrop-filter 0.3s ease;
}
.nav-overlay.is-scrolled {
  background-color: rgba(239,236,231,0.85);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.nav-brand .name { font-style: normal; }
.nav-brand .divider {
  width: 28px;
  height: 1px;
  background: var(--black);
  flex-shrink: 0;
}
.nav-brand .role { font-style: italic; }

.nav-photo {
  width: 32px;
  height: 32px;
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid rgba(0,0,0,0.15);
  flex-shrink: 0;
}
.nav-photo img { width: 100%; height: 100%; object-fit: cover; }

/* Text nav links (home only) — .nav-photo's alternative on the
   homepage, where the persistent header carries the page links
   instead of a portrait shortcut to About. */
.nav-links {
  display: flex;
  align-items: center;
  gap: 32px;
}
.nav-link {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 20px;
  color: var(--black);
  transition: opacity 0.3s ease-in-out;
}
.nav-link:hover { opacity: 0.6; }
@media (max-width: 600px) {
  .nav-links { gap: 16px; }
  .nav-link { font-size: 16px; }
}
/* Below ~480px even the shrunk single-row nav (brand + 3 links)
   doesn't fit — any nav bar carrying .nav-links (home's .nav-home, and
   about.html's plain nav, which reuses the same text-links pattern
   instead of home's nav-photo shortcut) switches to a wrapped,
   auto-height bar. --nav-h is redefined on body (not the nav itself —
   a sibling's var() lookup wouldn't see it there) so .page /
   .hero-atmosphere / .nav-pills, which all key off that variable for
   offsets, stay correct without touching the fixed 64px nav used on
   pages with the plain nav-photo shortcut instead. */
@media (max-width: 480px) {
  body:has(nav .nav-links) { --nav-h: 96px; }
  nav:has(.nav-links) {
    height: auto;
    padding: 14px var(--side-pad);
  }
  nav:has(.nav-links) .nav-inner { flex-wrap: wrap; row-gap: 10px; }
  .nav-brand .divider,
  .nav-brand .role { display: none; }
  .nav-links { width: 100%; justify-content: space-between; gap: 12px; }
  .nav-link { font-size: 15px; }
}

/* ── Nav Pills (home page only) ─────────────────────────────────── */
.nav-pills {
  display: flex;
  gap: 12px;
  position: absolute;
  bottom: 80px;
  left: 50%;
  transform: translateX(-50%);
  animation: heroPillReveal 0.6s ease both;
  animation-delay: 0.6s;
}

.nav-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 28px;
  border: 0.5px solid var(--off-white);
  border-radius: 40px;
  font-family: var(--font-serif);
  font-size: 28px;
  color: var(--off-white);
  transition: background 0.2s ease, color 0.2s ease;
  white-space: nowrap;
}
.nav-pill:hover {
  background: var(--off-white);
  color: var(--dark-brown);
}

/* ── Page wrapper ───────────────────────────────────────────────── */
/* position + z-index: 0 establishes ONE shared stacking context for the
   whole page — every case study's .bg-blob instances (nested many
   levels deep, inside whichever section/row/canvas they're visually
   anchored to) escape past their own position:relative container
   (which stays a containing block for their top/left%, but must NOT
   set its own z-index) and land here instead. That keeps every blob
   painting in the same single "negative z-index" layer, directly on
   top of the flat body background and below ALL of the page's real
   content uniformly — rather than each blob being trapped inside its
   own section's isolated stacking context, where a blob large/blurred
   enough to visually bleed past its own section's box would paint OVER
   an earlier sibling section's content instead of staying behind it
   (still ordered by DOM position between same-level stacking contexts,
   z-index:-1 or not). */
.page {
  position: relative;
  z-index: 0;
  min-height: 100vh;
  padding-top: var(--nav-h);
}

/* ── HOME ───────────────────────────────────────────────────────── */
/* Both live outside .page (direct <body> children) for two reasons:
   1. .page's own fadeUp animation leaves a non-'none' computed
      transform once it settles, which would make .page a containing
      block for any position:fixed descendant — trapping .cursor-blob
      as if absolutely positioned inside .page and breaking "stays
      fixed" while scrolling.
   2. Stacking order is resolved per sibling group, not by reaching
      inside a group — a z-index on an element nested in .page can
      never slot between two body-level siblings. Since .page's whole
      subtree (headline, work cards, everything real) must paint above
      both layers, and .hero-atmosphere must paint below .cursor-blob
      so the blob glows over it instead of being hidden behind it,
      both need to be body-level siblings ordered by z-index alone:
      atmosphere (-2) under blob (-1) under .page (unpositioned, so it
      paints after any negative-z-index sibling regardless of value). */

/* Hero atmosphere — Figma's own exported blob assets (node 951:34434's
   4 ellipses — really 3 irregular blurred *paths* plus one true
   ellipse, not 4 clean ellipses), used as literal images, each held
   at a fixed px size/position rather than scaled to the viewport.
   .hero-atmosphere's own height stays a FIXED px value per breakpoint
   (not tied to 100vh — see the earlier, still-relevant note that
   pairing a vh-based container with viewport-relative blobs is what
   broke on short/wide windows).
   The blobs themselves are sized and positioned in plain px, at each
   asset's native exported pixel size, centered on the page as one
   fixed composition: `top` is a constant px offset from the top of
   .hero-atmosphere, and `left` is a constant px offset from the page's
   horizontal MIDPOINT (`calc(50% - Npx)`), not the left edge. Widening
   the window doesn't stretch this composition at all — it just
   reveals more of the same fixed-size image on each side (and a
   narrower window reveals less), the way panning out on a large
   centered raster image would. This intentionally does NOT reproduce
   the old vw-based version's ultra-wide safety margin (blobs widened
   1.45x so they'd still reach the far corners at 2560px+): at some
   width beyond each blob's own native size the plain background will
   show at the edges, which is the expected trade-off of a fixed-size
   composition rather than a bug to guard against.
   Numbers below are each image's real Figma coordinates (at the
   design's native 1512px width, i.e. identical to that asset's own
   SVG viewBox size — no unit conversion needed since both are already
   real px). Paint order matches Figma's actual layer order: wine,
   gold, sage, then dark LAST — see .atm-blob--dark's own note for why.
   The flat linear-gradient this replaced is kept as its own variant —
   see index-linear-gradient.html / css/style-linear-gradient.css —
   not discarded, just reserved for a more uniform-sweep look than
   these organic, irregular-edged blobs give. */
.hero-atmosphere {
  position: absolute;
  /* Starts at the true page top (not var(--nav-h)) — .nav-home is
     transparent over the hero, so the dark backdrop has to reach
     behind it too, or that strip falls through to the plain light
     body background and reads as a bright bar under the nav text. */
  top: 0;
  left: 0;
  width: 100%;
  /* 2500px — confirmed by eye against the real scroll speed: 1500px
     let the (then-)clipped bottom edge show up as a visible hard seam
     before it cleared the viewport. At this height every blob's own
     extent — including .atm-blob--dark's negative top offset — fits
     inside [0, 2500] with room to spare, which is what makes dropping
     overflow: hidden below safe: nothing was relying on it to hide an
     edge that still overflows. */
  height: 2500px;
  z-index: -2;
  /* No overflow: hidden — it was clipping .atm-blob--dark's own
     negative top offset (it starts above y:0 by design), which is
     exactly the buffer that should be covering the top of this box.
     Scrolling up past the very top produces a momentary elastic
     overscroll (scrollY goes negative), which briefly pushes this
     layer's translateY *down* past its resting position — without
     that buffer, the gap it opened up above y:0 had nothing painted
     there and showed a flash of plain white above the nav before the
     bounce settled back to 0. */
  will-change: transform;
  pointer-events: none;
  background-color: var(--off-white);
}
@media (max-width: 900px) {
  .hero-atmosphere { height: 1850px; }
}
.atm-blob {
  position: absolute;
  background-repeat: no-repeat;
  background-size: 100% 100%;
}
.atm-blob--wine {
  background-image: url(../images/gradient-bg/ellipse-wine.svg);
  left: calc(50% - 1297.14px);
  top: 145.15px;
  width: 3172.18px;
  height: 1282.18px;
}
.atm-blob--gold {
  background-image: url(../images/gradient-bg/ellipse-gold.svg);
  mix-blend-mode: multiply;
  left: calc(50% - 1439.50px);
  top: 470.30px;
  width: 2452.46px;
  height: 1292.77px;
}
.atm-blob--sage {
  background-image: url(../images/gradient-bg/ellipse-sage.svg);
  mix-blend-mode: multiply;
  left: calc(50% - 1007.96px);
  top: 551.58px;
  width: 2617.11px;
  height: 1775.55px;
}
/* Ellipse2 in the real component is its LAST child — painted on top
   of wine/gold/sage, not underneath. It's solid at its own core and
   only fades via its baked-in blur, which is what makes the hero's
   top read as dark while letting the bands below it (outside its
   reach) show their full, undarkened color. */
.atm-blob--dark {
  background-image: url(../images/gradient-bg/ellipse-dark.svg);
  left: calc(50% - 1702.51px);
  top: -707.69px;
  width: 3368.74px;
  height: 1781.14px;
}

/* Page grain — fixed over the whole viewport (not just the hero), so
   it reads consistently over both the dark hero and the light work
   section, à la the pearl-sequeira.com reference. Same tiled
   feTurbulence technique as the hero used to have on its own
   (superseded by this one, so it isn't doubled up there); desaturated
   to pure luminance grain, overlay-blended so it reads as texture
   rather than a gray haze. position: fixed + a high z-index put it
   above literally everything, including content — pointer-events:
   none is what keeps it from blocking clicks/selection. */
.page-grain {
  position: fixed;
  inset: 0;
  z-index: 999;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 220px 220px;
  opacity: 0.05;
  mix-blend-mode: overlay;
}

/* Cursor blob — a fixed, mouse-following glow, based on Figma node
   1157:31583 (328x280, top-to-bottom linear gradient, blurred 54px)
   but centered on the cursor on both axes rather than Figma's own
   top-edge anchor, dimmed to 0.65 opacity (Figma's own 0.8 read as too
   bright/attention-grabbing), and given a slow organic wiggle.
   Split into two elements because those two jobs can't share one:
   .cursor-blob (outer) only ever gets its `transform` written by
   home.js every mousemove, in px, to track the cursor — a CSS
   `animation` targeting the same property on the same element would
   fight it (animations win the cascade over inline styles for
   whatever they target, so the position tracking would lose).
   .cursor-blob-inner carries the actual look (gradient, blur, the
   fixed size) and a CSS-only animation that slowly morphs its
   border-radius and adds a gentle scale/rotate — the "wiggle" — with
   no transform conflict since home.js never touches this element. */
.cursor-blob {
  position: fixed;
  top: 0;
  left: 0;
  width: 328px;
  height: 280px;
  margin: -140px 0 0 -164px;
  pointer-events: none;
  z-index: -1;
  transform: translate3d(50vw, 40vh, 0);
  transition: transform 0.35s cubic-bezier(0.22, 0.68, 0.32, 1);
  will-change: transform;
}
.cursor-blob-inner {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    rgba(235, 79, 40, 0.7) 13.113%,
    rgba(255, 178, 0, 0.7) 59.509%);
  opacity: 0.65;
  filter: blur(54px);
  animation: cursorBlobWiggle 12s ease-in-out infinite;
}
/* 4 organic border-radius shapes plus scale/rotate/drift. The swings
   here are deliberately large — this sits behind a 54px blur, which
   washes out fine shape detail almost completely, so a subtle wiggle
   (an earlier version of this) is invisible in practice. This one is
   sized to still read clearly once blurred. 12s, not fast. */
@keyframes cursorBlobWiggle {
  0%   { border-radius: 30% 70% 70% 30% / 60% 40% 60% 40%; transform: scale(1) rotate(0deg) translate(0, 0); }
  25%  { border-radius: 70% 30% 40% 60% / 30% 68% 32% 70%; transform: scale(1.18) rotate(16deg) translate(10px, -8px); }
  50%  { border-radius: 46% 54% 62% 38% / 58% 44% 66% 34%; transform: scale(0.86) rotate(-12deg) translate(-10px, 8px); }
  75%  { border-radius: 66% 34% 32% 68% / 38% 66% 34% 62%; transform: scale(1.1) rotate(9deg) translate(6px, 6px); }
  100% { border-radius: 30% 70% 70% 30% / 60% 40% 60% 40%; transform: scale(1) rotate(0deg) translate(0, 0); }
}
/* Devices without a hovering pointer never fire mousemove — hide it
   rather than leave it stuck in its default spot. */
@media (hover: none), (pointer: coarse) {
  .cursor-blob { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .cursor-blob-inner { animation: none; border-radius: 50%; }
  .hero-thread, .hero-statement, .hero-sub, .nav-pills { animation: none; opacity: 1; }
}

.hero {
  min-height: calc(100vh - var(--nav-h));
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: relative;
  max-width: var(--max-w-outer);
  margin: 0 auto;
  padding: 0 var(--side-pad);
  padding-bottom: 140px;
}
/* Extra breathing room between the hero's "Featured projects" pill and
   "Case studies" below it. margin-bottom, not more padding-bottom:
   .hero vertically centers its content inside a min-height box
   (justify-content: center), so padding just steals from that
   centering slack instead of growing the box — margin sits outside
   the box entirely, so it reliably pushes .work-section down
   regardless of how much slack centering leaves. Also lives BEFORE
   #work (.work-section), not inside it, so clicking the pill
   (href="#work") still scrolls straight to the title with just its
   normal small clearance, not to a few hundred px of blank space
   above it.
   Less on mobile than desktop, not a scaled-down copy of the same
   300px — a short mobile viewport makes that much dead space feel
   unbalanced against the content around it, not just "smaller". */
@media (max-width: 900px) {
  .hero { margin-bottom: 72px; }
}
@media (min-width: 901px) {
  .hero { margin-bottom: 220px; }
}

/* Thread illustration — sits centered above the greeting/headline.
   ~120px on desktop per spec, scaling down on mobile. It's an 8-frame
   stop-motion flipbook (real photographed yarn, not an illustration) —
   see js/thread.js for the interaction; this is purely layout/sizing.
   object-fit: contain because the frames aren't all the same aspect
   ratio (the more tangled ones sprawl wider with loose ends), so a
   fixed square box needs to letterbox rather than crop or stretch —
   otherwise a frame swap would visibly jump/resize the art. */
.hero-thread {
  width: 120px;
  height: 120px;
  margin: 0 auto 20px;
  cursor: pointer;
  animation: heroReveal 0.6s ease both;
  animation-delay: 0.15s;
}
.hero-thread-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  -webkit-user-drag: none;
  user-select: none;
}
@media (max-width: 900px) {
  .hero-thread { width: 96px; height: 96px; }
}
@media (max-width: 600px) {
  .hero-thread { width: 80px; height: 80px; margin-bottom: 16px; }
}

.hero-statement {
  max-width: 680px;
  margin: 0 auto;
  text-align: center;
  /* Figma has this hero title at 64px — the same size .h1-display
     already sets for every other page's h1 (including its own 48px
     mobile step), so no override is needed here at all. An earlier
     content revision had it at 100px and this class carried a bespoke
     clamp() to match; that's gone now that the real size is back to
     the shared default. */
  color: var(--gold);
  transition: color 0.35s ease-out;
  animation: heroReveal 0.6s ease both;
  animation-delay: 0.3s;
}

.hero-statement em {
  font-style: italic;
}

/* Layout only — pairs with .body-big for type, per the site's
   "per-page class carries layout, shared class carries type" rule.
   color is a legitimate override: .body-big defaults to --black,
   but this sits on the dark hero. */
.hero-sub {
  margin: 24px auto 0;
  max-width: 700px;
  text-align: center;
  color: var(--off-white);
  animation: heroReveal 0.6s ease both;
  animation-delay: 0.45s;
}

/* ── Featured Projects (homepage) ──────────────────────────────────
   Alternating visual/info rows. .work-card-image + .placeholder-label
   survive only as the style guide's generic "cover image" stand-in
   (see #work-card there) — the homepage itself now uses real project
   screenshots via .frame. */
.work-section {
  max-width: var(--max-w-outer);
  margin: 0 auto;
  padding: 0 var(--side-pad);
  /* #work is this section's own anchor (both the hero's CTA pill and
     the nav link point at it). Without this, jumping to the anchor
     puts "Case studies" flush at the very top of the viewport —
     partially hidden behind the fixed nav, which this page's own
     scroll-behavior: smooth (html, base rules) doesn't account for on
     its own. scroll-margin-top clears the nav plus a bit of comfort
     space, so the title lands fully visible with some room above it. */
  scroll-margin-top: calc(var(--nav-h) + 32px);
}

.work-card-image {
  width: 100%;
  aspect-ratio: 4/3;
  background: rgba(217,217,217,0.3);
  border: 1px solid rgba(0,0,0,0.1);
  border-radius: 16px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.work-card-image .placeholder-label {
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--mid);
  opacity: 0.5;
}

.project-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 80px;
  align-items: center;
  padding: 60px 0;
  min-height: 740px;
  border-bottom: 1px solid var(--neutral-border);
}
.project-row:last-child { border-bottom: none; padding-bottom: 100px; }

/* Reversed rows put the visual second in the markup (so it stays
   first in source/reading order on mobile) but first visually. */
.project-row--reverse .project-visual { order: 2; }
.project-row--reverse .project-info { order: 1; }

.project-visual {
  position: relative;
}
.project-visual img {
  width: 100%;
  border-radius: 8px;
}
/* The phone mockup (7Mind) is a tall, narrower asset — let it sit at
   its own width instead of stretching to the column. Its screenshot
   also reads as a phone-shaped CARD, not a flat screen edge — Figma's
   own frame (node 1157:30275) rounds it at ~32px, well past .frame's
   8px default (right for the wider desktop/landing screenshots the
   other two project rows use). --frame-radius is exactly the hook
   .frame exposes for this per-instance override.
   justify-self: center is the load-bearing part here, not just
   centering: as a CSS grid item (this is one column of .project-row's
   grid), .project-visual--phone stretches to fill its whole column
   width by default, same as its non-phone siblings — fine for them,
   since their <img> also fills the wrapper at width: 100%, so wrapper
   and image are the same size. This wrapper's own <img> is narrower
   (width: auto, capped by max-height), so without justify-self the
   wrapper stayed column-width regardless, and .frame's ::before —
   sized to the WRAPPER's box, not the <img>'s — rendered as one big
   square card spanning the full column instead of hugging the phone
   image. justify-self: center shrinks the grid item to its content
   width like an inline element would, so the wrapper (and therefore
   .frame) actually matches the image's real footprint. */
.project-visual--phone {
  display: flex;
  justify-content: center;
  justify-self: center;
}
.project-visual--phone.frame {
  --frame-radius: 32px;
}
.project-visual--phone img {
  width: auto;
  max-height: 520px;
}

.project-annotation {
  /* Hidden on the homepage project cards — kept in the DOM/CSS in case
     they come back, just not shown here. */
  display: none;
  position: absolute;
  z-index: 2; /* above .frame's own img (z-index: 1) and ::before glow */
  right: -24px;
  bottom: -24px;
  max-width: 280px;
}
/* .annotation-card--neutral's default (--white-20) reads fine over the
   plainer case-study backgrounds it was built for, but is too sheer
   over a busy product screenshot — bump toward the same white-45/
   white-70 pairing .frame already uses, just for this placement. */
.project-annotation.annotation-card--neutral {
  background: var(--white-45);
  border-color: var(--white-70);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
.project-annotation.annotation-card--neutral p { color: var(--black); }
@media (max-width: 900px) {
  .project-annotation { right: 0; bottom: -20px; left: 24px; max-width: none; }
}

.project-info { max-width: 460px; }
.project-row--reverse .project-info { justify-self: end; }

.project-title {
  margin: 12px 0 16px;
}

.project-desc {
  margin-bottom: 28px;
  color: var(--mid);
}

.project-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-serif);
  font-style: normal;
  font-size: 18px;
  color: var(--black);
  border: 0.5px solid var(--black);
  border-radius: 40px;
  padding: 10px 24px;
  transition: background 0.2s ease, color 0.2s ease;
}
.project-link:hover { background: var(--black); color: var(--white); }

/* Section header shared by "Case studies" (above the project rows)
   and "Design fragments" (see .fragments-header below) — centered
   title + sub-title, sub-title capped at Figma's own 532px text box
   width so it wraps the same way. Layout only; pairs with .h2-section
   (title) and .body-big (sub) for type. */
.work-section-header,
.fragments-header {
  text-align: center;
}
/* Tighter than .fragments-header's 80px — combined with the first
   .project-row's own padding-top (see .project-row:first-of-type
   below), an 80px header margin here left ~140px of dead space
   before the first project card, more than the section needed. */
.work-section-header { margin-bottom: 40px; }
.fragments-header { margin-bottom: 80px; }
/* :first-of-type, not :first-child — .work-section-header (a <div>)
   is .work-section's actual first child, so :first-child never
   matches any .project-row at all; :first-of-type matches the first
   <article class="project-row"> among its siblings regardless.
   Same rhythm as every other row's own 60px padding-top would leave
   ~140px combined with the header's own margin above — this row sits
   right under a title, not between two other rows, so it gets less. */
.project-row:first-of-type { padding-top: 24px; }
.work-section-sub,
.fragments-sub {
  max-width: 532px;
  margin: 24px auto 0;
  color: var(--mid);
}

/* ── Design Fragments (homepage) ────────────────────────────────────
   Two square screenshot cards + one wide autoplaying motion reel.
   Figma's own "Design fragments" frame (node 1342:52752) is a
   bespoke 1313px-wide block, narrower than the rest of the page's
   --max-w-outer container; rather than add a second one-off content
   width just for this section, it reuses --max-w-outer like every
   other homepage section, so the numbers below (gap, inset) are
   adapted proportionally rather than copied as literal px. */
.fragments-section {
  max-width: var(--max-w-outer);
  margin: 0 auto;
  padding: 100px var(--side-pad) 100px;
}
.fragments-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 53px; /* Figma: 683px card2-x − 630px card width = 53px */
}
.fragment-item {
  margin: 0; /* reset <figure>'s default margin */
}
/* Plain card, not .frame — Figma's own cards here (node 1311:36383/
   1311:36390) are a flat white box + drop shadow, not .frame's glass
   blur/gradient border treatment used on the project screenshots
   above. Numbers are Figma's own box-shadow, converted from its 3
   layers to plain px/rgba. */
.fragment-frame {
  aspect-ratio: 1/1;
  position: relative;
  overflow: hidden;
  border-radius: 8px;
  background: var(--white);
  box-shadow:
    0 0.35px 1.4px var(--black-18),
    0 1.4px 11px -2.8px var(--black-10),
    0 45px 38px var(--black-10);
}
.fragment-frame img,
.fragment-frame video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Placeholder-only override: .work-card-image's own aspect-ratio:4/3
   default would otherwise fight .fragment-frame's 1/1 (or 5/3 for the
   wide card) — height:100% + aspect-ratio:auto lets it just fill
   whatever shape the frame around it already establishes. Drop this
   rule once the real <img>/<video> replace these placeholders —
   .fragment-frame img/video above already sizes those correctly. */
.fragment-frame > .work-card-image {
  height: 100%;
  aspect-ratio: auto;
  border-radius: inherit;
}
/* The wide motion-reel card sits inset from the two square cards
   above it (Figma: 103px inset each side within its own narrower
   frame) — centered here at its own native 1070:642 (5:3) ratio
   instead, so it reads as the same relative "narrower, centered"
   accent regardless of the container width difference noted above. */
.fragment-item--wide {
  max-width: 1070px;
  margin: 80px auto 0; /* Figma: 900px video-y − 820px row bottom = 80px */
}
.fragment-item--wide .fragment-frame { aspect-ratio: 5/3; }
.fragment-caption {
  margin-top: 16px;
  text-align: center;
  color: var(--mid);
}
/* Hover-dim group — scoped to this section only. Hovering any one
   fragment (image or video, square or wide) brings IT to full
   opacity and fades the other two to 0.2, regardless of which row
   they're in. :has() keeps this CSS-only: no JS needed to know which
   sibling is hovered. Hover-capable pointers only — dimming siblings
   on tap would just leave two fragments stuck faded on touch, since
   touch has no hover to clear it. */
/* Opacity transition for the dim/undim above rides on .home-reveal's
   own `transition: opacity 0.5s ease, ...` (every .fragment-item also
   carries .home-reveal) — a second `transition` declaration here would
   just overwrite that shorthand rather than add to it, since they
   both set the same property. */
@media (hover: hover) and (pointer: fine) {
  .fragments-media:has(.fragment-item:hover) .fragment-item:not(:hover) {
    opacity: 0.2;
  }
}

/* ── ABOUT PAGE ─────────────────────────────────────────────────── */
.about-layout {
  max-width: var(--max-w-outer);
  margin: 0 auto;
  padding: 72px var(--side-pad) 120px;
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 80px;
  align-items: start;
}

.about-photo-col {
  position: sticky;
  top: calc(var(--nav-h) + 40px);
}

.about-photo {
  width: 100%;
  aspect-ratio: 4/5;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid var(--light);
}
.about-photo img { width: 100%; height: 100%; object-fit: cover; }

.about-content-col { }

.about-h1 {
  margin-bottom: 32px;
}

.about-p {
  font-family: var(--font-sans);
  font-size: 17px;
  line-height: 1.8;
  color: var(--gray-text);
  font-weight: 300;
  margin-bottom: 20px;
}

.about-section-title {
  font-family: var(--font-serif);
  font-size: 28px;
  letter-spacing: -0.015em;
  color: var(--black);
  margin: 56px 0 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.resume-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-serif);
  font-style: normal;
  font-size: 18px;
  font-weight: 400;
  color: var(--black);
  border: 0.5px solid var(--black);
  border-radius: 40px;
  padding: 10px 24px;
  transition: background 0.2s, color 0.2s;
  white-space: nowrap;
}
.resume-btn:hover { background: var(--black); color: var(--white); }

.exp-item {
  padding: 24px 0;
  border-bottom: 1px solid var(--light);
}
.exp-item:first-child { border-top: 1px solid var(--light); }

.exp-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 8px;
}
.exp-role {
  font-family: var(--font-serif);
  font-size: 20px;
  letter-spacing: -0.01em;
  color: var(--black);
}
.exp-company {
  font-family: var(--font-sans);
  font-size: 14px;
  color: var(--mid);
  margin-top: 2px;
}
.exp-date {
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--mid);
  white-space: nowrap;
  flex-shrink: 0;
  margin-left: 16px;
  margin-top: 4px;
}
.exp-bullets {
  list-style: none;
  margin-top: 12px;
}
.exp-bullets li {
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.6;
  color: var(--mid);
  font-weight: 300;
  padding: 3px 0 3px 16px;
  position: relative;
}
.exp-bullets li::before {
  content: '–';
  position: absolute;
  left: 0;
}

.skills-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 4px;
}
.skill-tag {
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--black);
  border: 1px solid rgba(0,0,0,0.2);
  border-radius: 40px;
  padding: 6px 16px;
  font-weight: 400;
  letter-spacing: 0.01em;
}

/* ── ABOUT PAGE (2026 redesign) ─────────────────────────────────────
   Replaces the sticky-photo/résumé-list layout above on about.html
   itself; that older block is left in place only because styleguide.html
   still documents .resume-btn/.skill-tag/.exp-bullets as components.
   Ink color: the Figma redesign uses a warm #301712 throughout this
   page's text instead of the site's usual near-black --black — close
   enough to the existing --dark-brown token (already reserved for the
   hero gradient's warm end) to reuse rather than add a near-duplicate
   variable. */
body.about-body { position: relative; color: var(--dark-brown); }
/* No opaque bar — the plain `nav` bg/blur reads as a bright strip over
   this page's much softer atmosphere; Figma's own nav is just the dark
   ink floating directly on the gradient. */
body.about-body nav { background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; }

/* About atmosphere — the homepage's hero gradient (node 951:34434's
   wine/gold/sage blob assets) re-anchored to the FOOTER instead of the
   page top. Figma's own About composition is the identical gradient
   rotated 180° and shifted, which works out to the same blob assets at
   the same sizes as home's .atm-blob values, just placed with their
   BOTTOM edge a fixed distance above the page bottom instead of their
   top edge a fixed distance below the page top — resolved by hand from
   Figma's node geometry (outer wrapper top:-3013px in a 5745.6px-tall
   rotated container; a child at local top Y/height H lands at final
   page-Y = 2732.6 − Y − H once the rotation is unwound). Anchoring to
   the bottom (rather than replicating Figma's fixed 2211px page height)
   keeps the wine blob sitting behind the footer regardless of how tall
   the real bio/skills content above it runs.
   No responsive resizing, same reasoning as .atm-blob: a fixed-size
   composition centered on the page, revealing less on a narrower
   viewport rather than scaling down. */
/* The hero-to-skills wash: Figma's own overlay is a transparent→sage
   linear-gradient inside the same rotated composition, which unwinds to
   "solid sage near the top, fading to transparent by ~y=2237" — approximated
   here as a plain top-anchored fade rather than re-deriving the exact
   rotated stop position, since the effect (pale sage at the hero, gone
   by the time the blobs take over) is the same either way. */
.about-atmosphere {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
  pointer-events: none;
}
.about-atm-tint {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 1500px;
  background: linear-gradient(to bottom, #d0d4c1 0%, rgba(208,212,193,0.9) 40%, rgba(208,212,193,0) 100%);
}
/* The wine blob's own asset fades out (baked-in blur) well inside its
   own box, so anchoring it near the footer isn't enough by itself —
   past a certain footer height the plain page background would show
   through as a band of white right at the very bottom. This floor is
   a flat-color safety net behind the blob: transparent until deep into
   the footer's own territory, then solid, so the footer always ends on
   dark ink regardless of exactly how tall its real content runs. */
.about-atm-floor {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 900px;
  background: linear-gradient(to bottom, rgba(58,17,32,0) 0%, rgba(58,17,32,0.9) 45%, #3a1120 100%);
}
.about-atm-blob {
  position: absolute;
  background-repeat: no-repeat;
  background-size: 100% 100%;
}
.about-atm-blob--wine {
  background-image: url(../images/gradient-bg/about-ellipse-wine.svg);
  left: calc(50% - 1874.92px);
  bottom: -74px;
  width: 3171.85px;
  height: 1282.32px;
}
.about-atm-blob--gold {
  background-image: url(../images/gradient-bg/about-ellipse-gold.svg);
  mix-blend-mode: multiply;
  left: calc(50% - 1013.05px);
  bottom: 251px;
  width: 2452.44px;
  height: 1292.77px;
}
.about-atm-blob--sage {
  background-image: url(../images/gradient-bg/about-ellipse-sage.svg);
  mix-blend-mode: multiply;
  left: calc(50% - 1609.25px);
  bottom: 332px;
  width: 2617.4px;
  height: 1775.66px;
}

/* Intro: portrait + bio, two columns — no fixed/sticky behavior at all,
   just a normal grid item that scrolls with the rest of the page.
   align-items: start lines the portrait's own top edge up with the
   "About me" heading's top edge, rather than centering it against the
   full (taller) bio text block below that heading. */
.about-hero {
  max-width: var(--max-w-outer);
  margin: 0 auto;
  padding: 96px var(--side-pad) 0;
  display: grid;
  grid-template-columns: 400px 1fr;
  gap: 75px;
  align-items: start;
}
/* Just the photo + Figma's own drop-shadow (node 1071:6489's
   drop-shadow-[...] value) — no .frame glass border/bleed, per request:
   the portrait should read as a plain shadowed photo, not a framed card. */
.about-portrait {
  width: 100%;
  aspect-ratio: 400 / 501;
  border-radius: 22px;
  box-shadow:
    0 0.5px 1px rgba(0,0,0,0.18),
    0 2px 8px rgba(0,0,0,0.1),
    0 64px 27px rgba(0,0,0,0.1);
  animation: heroReveal 0.6s ease both;
  animation-delay: 0.1s;
}
.about-portrait img { width: 100%; height: 100%; object-fit: cover; border-radius: 22px; }

.about-h1 {
  font-size: 54px;
  line-height: 60px;
  color: var(--dark-brown);
  margin-bottom: 24px;
}
.about-intro .about-h1 {
  animation: heroReveal 0.6s ease both;
  animation-delay: 0.3s;
}
.about-bio {
  animation: heroReveal 0.6s ease both;
  animation-delay: 0.45s;
}
.about-bio p {
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 24px;
  letter-spacing: -0.02em;
  margin-bottom: 18px;
}
.about-bio p:last-child { margin-bottom: 0; }
.about-email-link {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Skills — a page-level section again (not nested in the bio column),
   so it spans the full page width like .about-hero rather than just
   the bio's narrower text measure — three columns at the largest
   breakpoint instead of the bio column's two. */
/* White text — the section sits on the atmosphere's warm/dark half by
   the time it's on screen, so the page's usual dark-brown ink (fine at
   the pale top) loses contrast down here; white reads clearly across
   the section's own gradient range. */
.about-skills {
  max-width: var(--max-w-outer);
  margin: 80px auto 0;
  padding: 0 var(--side-pad);
  color: var(--off-white);
}
.about-divider {
  height: 0.5px;
  background: rgba(239,236,231,0.3);
  margin-bottom: 19px;
}
.about-skills-title { margin-bottom: 19px; color: var(--off-white); }
.about-skills-cols {
  display: flex;
  gap: 80px;
}
.about-skills-col { flex: 1 0 0; min-width: 0; }
.about-skills-col-title {
  font-family: var(--font-serif);
  font-size: 22px;
  font-weight: 400;
  color: var(--off-white);
  margin-bottom: 16px;
}
.about-skills-rule {
  height: 0.5px;
  background: rgba(239,236,231,0.3);
  margin-bottom: 16px;
}
.about-skills-list { list-style: none; }
.about-skills-list li {
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 24px;
  letter-spacing: -0.02em;
  padding: 5px 0;
  color: var(--off-white);
}

/* CTA pill above the footer */
.about-cta {
  display: flex;
  justify-content: center;
  padding: 100px var(--side-pad) 120px;
}
.about-cta .nav-pill { position: static; animation: none; }

/* Footer on the dark end of the atmosphere — same footer--home layout,
   off-white text instead of the page's default dark ink since this
   sits over the wine blob rather than the plain page background. */
footer.footer--on-atmosphere {
  border-top-color: rgba(255,255,255,0.12);
}
footer.footer--on-atmosphere .footer-home-heading,
footer.footer--on-atmosphere .footer-home-col .section-eyebrow,
footer.footer--on-atmosphere .footer-home-col a,
footer.footer--on-atmosphere .footer-home-col span {
  color: var(--off-white);
}
footer.footer--on-atmosphere .footer-home-sub,
footer.footer--on-atmosphere .footer-home-col a:hover { color: rgba(239,236,231,0.7); }
footer.footer--on-atmosphere .footer-home-bottom { border-top-color: rgba(255,255,255,0.12); }
footer.footer--on-atmosphere .footer-home-copy { color: rgba(239,236,231,0.4); }

@media (max-width: 900px) {
  .about-hero { grid-template-columns: 1fr; gap: 32px; padding-top: 48px; }
  .about-portrait { max-width: 280px; }
  .about-skills-cols { flex-direction: column; gap: 40px; }
  .about-h1 { font-size: 40px; line-height: 1.15; }
}

/* ── Annotation Card (reusable — case study callouts) ─────────────── */
/* Skin only: border/background/text per sentiment variant. Positioning,
   reveal animation and any label markup are case-study-specific and stay
   in that case study's own stylesheet. Usage:
   <div class="annotation-card annotation-card--negative">
     <p class="annotation-card-label">...optional label...
       <span class="annotation-card-number">1</span>
     </p>
     <p>Note text</p>
   </div>
*/
.annotation-card {
  border-radius: 8px;
  padding: 12px 16px;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}
.annotation-card p { margin: 0; font-family: var(--font-sans); font-size: 12px; line-height: 1.5; }
/* Structured notes (title / description / "Impact:" bullets) stack
   multiple <p> and a <ul> instead of one plain paragraph — this is
   what puts space between them without touching the single-paragraph
   case above. */
.annotation-card p + p,
.annotation-card p + ul,
.annotation-card ul + p { margin-top: 6px; }
.annotation-card b,
.annotation-card strong { font-weight: 700; }
.annotation-card ul {
  margin: 0;
  padding-left: 16px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.5;
}
/* A leading outcome checkmark (used by the homepage's project
   annotations) — always positive/green regardless of card variant. */
.annotation-card-check { color: var(--green); font-weight: 700; }
.annotation-card-label {
  font-family: var(--font-sans);
  font-size: 12px;
  margin-bottom: 4px;
  display: flex;
  align-items: center;
  gap: 3px;
  flex-wrap: wrap;
}
.annotation-card-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  font-size: 9px;
  font-weight: 700;
}

/* Negative — flags a problem/gap */
.annotation-card--negative {
  border: 1px solid var(--red);
  background: rgba(232, 216, 220, 0.5);
}
.annotation-card--negative p,
.annotation-card--negative ul,
.annotation-card--negative .annotation-card-label { color: var(--red); }
.annotation-card--negative .annotation-card-number {
  background: rgba(144, 32, 58, 0.2);
  color: var(--red);
}

/* Neutral — a plain design note, no positive/negative charge */
.annotation-card--neutral {
  border: 1px solid var(--neutral-border);
  background: var(--white-20);
}
.annotation-card--neutral p,
.annotation-card--neutral ul,
.annotation-card--neutral .annotation-card-label { color: var(--brown); }
.annotation-card--neutral .annotation-card-number {
  background: rgba(195, 186, 176, 0.5);
  color: var(--brown);
}

/* ── Card (reusable — statement cards with a numbered, sentiment-
   colored heading). Skin only: border/background are fixed, only the
   number's color changes per sentiment variant. Layout (grid, gap)
   stays on the page-specific wrapper (e.g. .ch-problem-cards). Pair
   the text with .body-regular for type. Usage:
   <div class="card card--negative">
     <h3 class="card-title h3-serif"><span class="card-number">1.</span> Title</h3>
     <p class="body-regular">Supporting text.</p>
   </div>
*/
.card {
  border: 1px solid var(--white-70);
  background: var(--white-45);
  border-radius: 16px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.card--negative .card-number { color: var(--red); }
/* Positive — used where the whole heading (not just a number) carries
   the accent, e.g. a numbered principle or a plain statement card with
   no number at all (span inherits the color when there's nothing to
   override it, so a bare .card-title with no .card-number works too). */
.card--positive .card-title { color: var(--green); }

/* Compact — a smaller, denser layout for the same card (e.g. sticky
   sidebar cards that all need to fit one viewport height at once): a
   circular numbered badge sits beside a stacked title+body column,
   rather than the number inline with the title above a full-width
   paragraph. Only .card--compact's own padding/gap change on .card
   itself; everything else here is new markup inside it. Usage:
   <div class="card card--negative card--compact">
     <div class="card-row">
       <div class="card-badge-slot"><span class="card-badge">1</span></div>
       <div class="card-body">
         <h3 class="card-title h3-serif-sm">Title</h3>
         <p class="body-regular">Supporting text.</p>
       </div>
     </div>
   </div>
*/
.card--compact {
  border-radius: 20px;
  padding: 14px 16px 16px;
  gap: 12px;
}
.card-row {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  width: 100%;
}
/* Sized to .h3-serif-sm's own line-height (24px * 1.17 ≈ 28px), not
   the badge's own 23px — centers the badge against the title's first
   line instead of pinning it to the row's top edge, which is what
   .card-row's align-items: flex-start alone would do. */
.card-badge-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 28px;
  flex-shrink: 0;
}
.card-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 23px;
  height: 23px;
  border-radius: 50%;
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 500;
  flex-shrink: 0;
}
.card--negative .card-badge { background: rgba(144, 32, 58, 0.2); color: var(--red); }
.card--positive .card-badge { background: rgba(121, 138, 39, 0.2); color: var(--green); }
.card-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}
/* 24px serif — the compact card's own title size, between .h3-serif
   (32px) and body text. Not one of the site's 3 shared heading styles
   since it's specific to this one component, not a page heading. */
.h3-serif-sm {
  font-family: var(--font-serif);
  font-size: 24px;
  font-weight: 400;
  line-height: 1.17;
  letter-spacing: -0.02em;
  color: var(--black);
}
.card--compact .card-body p { font-size: 12px; line-height: 1.5; }

/* ── Stats (reusable — merges .ch-stats and .ch-outcome-metrics) ───
   Columns are intrinsic now, not a per-page breakpoint: auto-fit +
   minmax packs in as many 200px-wide columns as actually fit the
   row's real width, wrapping on its own as that width shrinks — no
   @media query needed. 200px is fixed regardless of item count (3 or
   4) rather than tuned per row, so every stats instance wraps at a
   consistent, visibly-reachable width instead of a 4-up row needing a
   narrower per-cell minimum just to keep all 4 on one line longer.
   gap and .stat's own horizontal padding are still page-overridable
   via custom properties, for cases like .ch-outcome-metrics's
   gap: 0 — its border-left dividers are the separator, a gap would
   double it up. column-gap and row-gap are split (rather than one
   shared `gap`) so that override only zeroes the space BETWEEN
   same-row items — once auto-fit wraps down to one column on a small
   viewport, the divider no longer separates anything (it's a left
   border, not a horizontal rule) and stacked items need their own
   vertical breathing room regardless; row-gap keeps the default 18px
   for that instead of inheriting the same override. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  column-gap: var(--stat-gap, 18px);
  row-gap: var(--stat-row-gap, 18px);
}
.stat {
  border-left: 1px solid;
  padding: 0 var(--stat-pad-x, 18px);
}
/* Layout only now — type pairs with the shared heading/body scale
   (.h2-section / .body-regular), same "per-page class carries layout,
   shared class carries type" rule as .card-title etc. .stat-value is a
   real <h2> too, not just a styled <p> — it's the number a stat is
   built around, which is heading-level content, not body copy. */
.stat-value { margin-bottom: 16px; }

/* Neutral — an observational fact, no positive/negative charge */
.stats--neutral .stat { border-left-color: var(--gold); }
.stats--neutral .stat-value { color: var(--gold); }
.stats--neutral .stat-label { color: var(--black); }

/* Positive — a result/outcome */
.stats--positive .stat { border-left-color: var(--green); }
.stats--positive .stat-value { color: var(--green); }
.stats--positive .stat-label { color: var(--body-text); }

/* ── Frame (reusable — glass-card treatment for photos/screens) ────
   Applied to the element that wraps an <img>/<video>, not the media
   itself. The visual layer (gradient, shadows, blur) paints on an
   absolutely-positioned ::before that bleeds past the wrapper's own
   box via --frame-bleed. Because that layer is out of flow, it adds
   nothing to .frame's box — its rendered width/height/position stay
   identical to the media it wraps, with or without the class. Anything
   that measures the image (JS position math, siblings placed relative
   to image height/width) keeps working unmodified.
   Note: a wrapper with `overflow: hidden` (e.g. .work-card-image)
   will clip the bleed — drop that overflow rule wherever .frame is
   applied, since .frame's own border-radius + the media's own
   border-radius already do the clipping .work-card-image relied on
   overflow for. */
.frame {
  position: relative;
  /* Isolates the z-index: 0/1 used below into their own stacking
     context. Without this, .frame > img's z-index: 1 is compared
     against whatever ancestor context .frame sits in — e.g. inside
     .ch-shot-canvas it would out-rank the annotation connector-line
     SVG (z-index: auto), which relies on DOM order (painted last) to
     stay on top, jumping the image in front of lines that are meant
     to cross over it. */
  isolation: isolate;
  border-radius: 8px;
  --frame-radius: 8px;
  --frame-bleed: 8px;
}
.frame > img,
.frame > video {
  position: relative;
  z-index: 1;
  display: block;
  border-radius: var(--frame-radius);
}
.frame::before {
  content: '';
  position: absolute;
  inset: calc(-1 * var(--frame-bleed));
  z-index: 0;
  /* Outer radius = inner radius + bleed keeps both arcs' center point
     the same, so the frame reads as one continuous curve instead of a
     ring of uneven thickness at the corners. */
  border-radius: calc(var(--frame-radius) + var(--frame-bleed));
  background: linear-gradient(173deg, var(--white-45) 27.11%, var(--white-90) 121.88%);
  box-shadow:
    0 0.5px 2px 0 var(--black-18),
    0 2px 16px -4px var(--black-10),
    0 64px 54px 0 var(--black-10),
    inset 0 0.5px 1px 0 var(--white-15),
    inset 0 0 24px 0 var(--white-15);
  backdrop-filter: blur(40px);
  -webkit-backdrop-filter: blur(40px);
  pointer-events: none;
}
/* Variant for flat content (a diagram, a screenshot) with no shadow of
   its own: the glass border sits flush against its edge instead of
   the usual 8px halo past it, which reads as unwanted padding around
   something this size. 0px, not 0 — ::before's inset above resolves
   via calc(-1 * var(--frame-bleed)), and a unitless 0 makes that
   calc() produce a <number> instead of a <length>, which is invalid
   for inset and silently breaks the whole shorthand. */
.frame--flush { --frame-bleed: 0px; }

/* ── Footer ─────────────────────────────────────────────────────── */
footer {
  border-top: 1px solid var(--light);
  padding: 40px var(--side-pad);
  max-width: var(--max-w-outer);
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.footer-name {
  font-family: var(--font-serif);
  font-size: 16px;
  font-style: italic;
  color: var(--mid);
}
.footer-links {
  display: flex;
  gap: 24px;
}
.footer-links a {
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--mid);
  transition: color 0.2s;
}
.footer-links a:hover { color: var(--black); }

/* Home footer — a taller, two-part variant. Scoped to .footer--home
   so the plain `footer` rule above (about/case-study pages) is
   untouched. */
footer.footer--home {
  flex-direction: column;
  align-items: stretch;
  gap: 48px;
  padding: 100px var(--side-pad) 40px;
}

.footer-home-top {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 40px;
  flex-wrap: wrap;
}

.footer-home-lead { max-width: 480px; }
.footer-home-heading { margin-bottom: 16px; }
.footer-home-sub { color: var(--mid); }

.footer-home-cols {
  display: flex;
  gap: 64px;
}
.footer-home-col {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.footer-home-col .section-eyebrow { margin-bottom: 4px; }
.footer-home-col a,
.footer-home-col span {
  font-family: var(--font-sans);
  font-size: 15px;
  color: var(--black);
}
.footer-home-col a:hover { color: var(--mid); }

.footer-home-bottom {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 24px;
  flex-wrap: wrap;
  padding-top: 32px;
  border-top: 1px solid var(--light);
}
.footer-home-copy {
  font-family: var(--font-sans);
  font-size: 13px;
  color: var(--mid);
}
@media (max-width: 600px) {
  footer.footer--home { padding-top: 72px; }
  .footer-home-cols { gap: 40px; }
  .footer-home-bottom { flex-direction: column; }
}

/* ── Page transitions ───────────────────────────────────────────── */
.page { animation: fadeUp 0.5s ease both; }
@keyframes fadeUp {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Hero entrance — thread, headline, sub-headline and CTA pill reveal
   one at a time (staggered animation-delay on each element) rather
   than all at once with the rest of .page, so the hero reads as a
   small sequence on load. Same easing/motion as fadeUp, just scoped
   per element. .nav-pills needs its own variant since its resting
   transform already carries translateX(-50%) for centering — a plain
   translateY would clobber that instead of combining with it. */
@keyframes heroReveal {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes heroPillReveal {
  from { opacity: 0; transform: translate(-50%, 16px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* ── Responsive ─────────────────────────────────────────────────── */
@media (max-width: 900px) {
  :root { --side-pad: 32px; }
  .about-layout { grid-template-columns: 1fr; gap: 40px; }
  .about-photo-col { position: static; }
  .about-photo { max-width: 280px; }
  .nav-pills { flex-direction: column; align-items: center; bottom: 60px; }
  .nav-links { gap: 20px; }
  .nav-link { font-size: 17px; }
  .h1-display { font-size: 48px; }
  .h2-section { font-size: 40px; }
  .h3-sub { font-size: 20px; }
  .h3-serif { font-size: 28px; line-height: 36px; }
  .project-row,
  .project-row--reverse {
    grid-template-columns: 1fr;
    gap: 32px;
    padding: 40px 0;
    min-height: 0;
  }
  .project-row--reverse .project-visual,
  .project-row--reverse .project-info { order: initial; }
  .project-info,
  .project-row--reverse .project-info { max-width: none; justify-self: stretch; }
  .project-visual--phone img { max-height: 440px; }
  .fragments-row { grid-template-columns: 1fr; gap: 32px; }
  .fragment-item--wide { margin-top: 32px; }
}

@media (max-width: 600px) {
  :root { --side-pad: 20px; }
  .footer-home-top { flex-direction: column; }
  /* Line-height stays unitless (1.5 / 1.6), so it scales proportionally
     with the smaller size automatically — no need to redeclare it. */
  .body-big { font-size: 16px; }
  .body-regular { font-size: 14px; }
  /* Color blobs only — :not(.bg-blob--white) excludes the flat pull-quote
     wash, which stays full size since it isn't one of the Figma-sized
     gradient shapes. border-radius stays 50% (relative to the new size,
     no override needed); blur stays 181px, so it reads relatively
     heavier against the smaller shape at this width. */
  .bg-blob:not(.bg-blob--white) { width: 650px; height: 550px; }
}
