/* ─── Fonts ─────────────────────────────────────────────────────────────────
   Display: Clash Display   (Fontshare, 400 + 500 + 700)
   Body/UI: Geist           (Google Fonts, 400 + 700)

   ── Display ──
   Stands in for the reference's licensed Landour, chosen by measurement, not
   by eye. Setting "Crafting products at scale" at 100px:
       Landour 1237.9px  ·  Clash Display 1220.9px   (−1.4%)
   It also matches the letterforms that carry the reference's character:
   double-storey 'a', single-storey 'g', straight-diagonal 'y', circular '0'.

   ── Body ──
   Geist is a deliberate choice, not a metric substitution — it replaced
   Satoshi, which itself stood in for the reference's Delight. So the old
   chain of width matching no longer applies to this face; what matters is
   what the swap did to the layout. Measured, setting
   "Ola, I'm Shriya. A senior product designer" at 100px:
       Satoshi 1811.1px  ·  Geist 1867.4px   (+3.1%)
   Geist is the wider face, so anything with a hand-tuned width is the thing
   to watch. Checked before and after at the same viewport: the hero bio still
   breaks to 4 lines, the work-row body to 2, the work subtitle and contact
   rows to 1. Nothing reflowed, so no width was retuned to absorb this.

   Geist ships only the weights this site asks for: 400 for body copy, 700 for
   the two bold UI controls. Weight 500 belongs to Clash Display alone (the nav
   destinations at rest, testimonial quotes), so Geist does not load one.

   Two different CDNs, so index.html carries preconnects for both. Every rule
   below references the two variables, so swapping a family is a one-line
   change here plus the <link> in each page's head.
────────────────────────────────────────────────────────────────────────────*/

/* ─── Type scale (fluid vw, ref: 1440px → max @ 2160px) ─────────────────
   Role-named, not size-named — this site's text doesn't sit on a linear
   ramp, it clusters by where it's used. Every token below has at least
   one real consumer; see style.css usages before adding another. ───── */
/* Clash Display's variable file, self-hosted for the flip card's title, which
   needs weight 600 (not in the Fontshare request in <head>). A separate family
   name, so it can never replace the Fontshare faces the rest of the site uses.
   font-display: block: the title is invisible until the file arrives rather
   than flashing a fallback; it is preloaded in index.html to keep that short.
   Clash Display, Indian Type Foundry, ITF Free Font License. */
@font-face {
  font-family: 'Clash Display Variable';
  src: url('fonts/ClashDisplay-Variable.woff2') format('woff2');
  font-weight: 200 700;
  font-style: normal;
  font-display: block;
}

:root {
  --font-display: 'Clash Display', sans-serif;
  --font-ui:      'Geist', sans-serif;

  --text-label:            clamp(0.75rem,   0.83vw,  1rem);     /* 12px → 16px  — micro labels (career list) */
  --text-meta:             clamp(0.875rem,  1.11vw,  1.125rem); /* 14px → 18px  — small UI text (career rows, toast) */
  --text-ui:               clamp(0.875rem,  1.111vw, 1.5rem);   /* 14px → 24px  — UI copy (16px @ 1440) */
  --text-bio:              clamp(0.75rem,   1.111vw, 1.5rem);   /* 12px → 24px  — hero bio only (16px @1440) */
  --text-body:             clamp(1.25rem,   1.667vw, 2.25rem);  /* 20px → 36px  — work rows, career bio */
  --text-lead:             clamp(0.9375rem, 1.389vw, 1.875rem); /* 15px → 30px  — about-overlay bio paragraphs */
  --text-caption:          clamp(1rem,      1.528vw, 2.0625rem);/* 16px → 33px  — photo captions */
  --text-hero:             56px;                                /* fixed — hero heading, deliberately non-fluid */
  --text-headline:         clamp(2.25rem,   4.444vw, 6rem);     /* 36px → 96px  — about text, overlay title */
  --text-headline-accent:  clamp(1.5rem,    2.5vw,   2.25rem);  /* 24px → 36px  — overlay title serif accent */
  --text-case:             clamp(2rem,      4.44vw,  4.5rem);   /* 32px → 72px  — case study titles + metrics */
  /* The landing page's heading size and weight, in one place: the hero
     headline and the Work and Testimonials section titles all read these, so
     one change moves all three. Scoped by selector (.g-head, #work,
     #testimonials) to the landing page: case studies and the overlays keep
     their own type. ?dial=header tunes them. */
  --landing-h-size:        56px;   /* dialled 2026-10-06 (was --text-case, 64px @ 1440) */
  --landing-h-weight:      500;
  --text-eyebrow:          32px;                                /* hero eyebrow, dialled 2026-10-06 */
  --text-case-h2:          clamp(1.5rem,    3.33vw,  3.5rem);  /* 24px → 56px  — case-study section heads (.cs-h2) */
  --text-section:          clamp(2.5rem,    5vw,     6.75rem);  /* 40px → 108px — work section title */
  --text-display:          clamp(2rem,      5.56vw,  7.5rem);   /* 32px → 120px — contact rows */

  --color-green:  #49a862;
  --color-cream:  #f5f4f0;
  --color-dark:   #1e1e20;
  --color-white:  #ffffff;
  /* A near-black, warmer and very slightly lighter than --color-dark
     (#1e1e20) rather than the same value — deliberately its own token, not a
     reuse of --color-dark, so the three components below can be retuned
     again without also moving every text/ink colour on the site that reads
     --color-dark. Was #136d62 (a teal) for one turn; changed to this on
     instruction before it shipped. Case-study.css's own header says it
     invents no colour of its own, so this lives here even though jupiter.html
     is its only consumer today. */
  --color-charcoal: #2b2d33;

  /* The brand accent, and the same value case-study.css already holds as
     --hook-highlight. CLAUDE.md names this exact hex as spacefire and warns
     against nearby corals or darkened variants, so it is written once here
     rather than approximated a second time; --hook-highlight should read from
     this token when that file is next touched.

     Used on the landing page only by .pro-eyebrow so far. It is a light
     colour and does not pass AA on cream (2.35:1) — on this site's dark and
     charcoal grounds it measures 6.44:1 and 5.33:1, which is why it is safe
     there and would not be in the hero. */
  --color-spacefire: #FC7A69;

  /* Grid: 120px at 1440px → scales with vw */
  --page-margin: 8.333vw;

  /* ─── Corner radius ──────────────────────────
     One value for every rounded thing on the site: cards, CTAs, images,
     overlays, toasts, nav items. No per-breakpoint variants — if something
     needs a corner, it uses this. */
  --radius: 8px;
  /* The UI archive's pictures only (on instruction: 16px corners). */
  --radius-lg: 16px;

  /* The one exception: standalone circular icon buttons that float over the
     content rather than sitting in a layout — the two overlay closes and the
     back-to-top. Their boxes are square, so an over-large radius clamps to a
     true circle and keeps doing so if the padding ever changes. */
  --radius-round: 999px;

  /* ─── Card shadow ────────────────────────────
     The lift shared by every card surface on the landing page — the project
     cards and the testimonial cards. One pair of tokens rather than two
     copied declarations, because "make the testimonials consistent with the
     project cards" is only true for as long as nobody edits one of them.

     Two layers, because one cannot do both jobs: the tight 1-2px layer is the
     contact edge that keeps a card from dissolving into the ground, and the
     wide, offset, negatively-spread layer is the lift. A single mid-blur
     shadow reads as a grey halo instead.

     REACH (needed for CLAUDE.md gotcha 11 — a shadow inside a scroller gets
     clipped, and .tm-card lives in one). Downward reach is
     offset-y + blur/2 + spread:
        rest   8 + 12 -  8 = 12px down,  8 - 12 +  8 =  4px up
        hover 16 + 20 - 12 = 24px down, 16 - 20 + 12 = -8px up (none)
     Sideways is blur/2 + spread: 4px at rest, 8px on hover.
     .tm-viewport pads 16px top / 32px bottom, which clears both. If these
     numbers grow, that padding has to grow with them. */
  --card-shadow:
    0 1px 2px  color-mix(in srgb, var(--color-dark)  6%, transparent),
    0 8px 24px -8px color-mix(in srgb, var(--color-dark) 14%, transparent);
  --card-shadow-hover:
    0 2px 4px  color-mix(in srgb, var(--color-dark)  8%, transparent),
    0 16px 40px -12px color-mix(in srgb, var(--color-dark) 20%, transparent);

  /* Nav shadow defaults; body[data-bg="hero"] pulls both back for cream. */
  --nav-shadow-depth: 58%;
  --nav-shadow-alpha: 56%;

  /* ─── Dot grid ───────────────────────────────
     One definition for the field that runs behind the about fold and the
     testimonials fold. Both reference these, so the two can never drift apart
     in tile size, dot weight or density — and the shared tile is what keeps
     their dot columns on the same rhythm. */
  --dot-tile:    16px;
  --dot-dot:     1px;
  --dot-opacity: 0.32;

  /* ─── Noise ──────────────────────────────────
     --noise-opacity   → overall intensity (0 = off, 0.08 = strong)
     --noise-frequency → grain (0.5 = coarse, 1.0 = fine)  [inert: the SVG
                         data-URI below hardcodes baseFrequency 0.75]
     --noise-size      → repeated tile size
  ──────────────────────────────────────────── */
  --noise-opacity:   0.36;
  --noise-frequency: 0.8;
  --noise-size:      180px;
}

/* ─── Noise overlay ────────────────────────────────────────────
   Fixed film-grain layer below the cursor and overlays, above the
   backgrounds. pointer-events: none = does not block clicks.
   To disable: set --noise-opacity: 0;
──────────────────────────────────────────────────────────────── */
body[data-bg="hero"]::after {
  opacity: 0; /* dots already fill the hero — grain would double up */
}

body::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 10;
  opacity: var(--noise-opacity);
  transition: opacity 0.5s ease;
  mix-blend-mode: overlay; /* spreads grain evenly on light and dark grounds */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' 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-repeat: repeat;
  background-size: var(--noise-size);
}

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

/* ─── Base ─────────────────────────────────── */
html {
  overflow-x: hidden;
}

/* Lenis smooth scroll utilities */
.lenis.lenis-smooth { scroll-behavior: auto !important; }

body {
  min-height: 100%;
  font-family: var(--font-ui);
  font-weight: 400;
  background-color: var(--color-cream);
  color: var(--color-dark);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  transition: background-color 0.6s ease, color 0.6s ease;
}

/* ─── Background states ────────────────────── */
/* --nav-fg / --nav-bg travel with the background state so the bar restyles
   itself as it passes over each section.

   --nav-shadow-depth  how far the shadow is mixed toward black (higher = lighter)
   --nav-shadow-alpha  how strongly it is laid down
   Cream needs both pulled back: on a near-white ground the same mix that reads
   as a whisper on green or dark reads as a grey smear. */
body[data-bg="hero"] {
  background-color: var(--color-cream);
  color: var(--color-dark);
  --nav-fg: var(--color-dark);
  --nav-bg: var(--color-cream);
  --nav-shadow-depth: 78%;
  --nav-shadow-alpha: 39%;
}

/* Cream type here, matching the landing screen's own treatment on green. */
body[data-bg="green"] {
  background-color: var(--color-green);
  color: var(--color-cream);
  --nav-fg: var(--color-cream);
  --nav-bg: var(--color-green);
}

body[data-bg="dark"] {
  background-color: var(--color-dark);
  color: var(--color-white);
  --nav-fg: var(--color-cream);
  --nav-bg: var(--color-dark);
}

/* ─── Nav ───────────────────────────────────────────────────────────────────
   Name (home) and the two social marks on the left, the three destinations on
   the right. The bar starts off-screen and above: script.js adds .nav-visible
   once the hero is behind you, and it stays for the rest of the page.

   Solid rather than blurred — a hard edge on a flat field is the same
   print-tech vocabulary the rest of the site uses, and it costs nothing to
   composite. The bottom hairline is the only thing separating it from the
   section beneath.
──────────────────────────────────────────────────────────────────────────── */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 12px var(--page-margin);
  color: var(--nav-fg);
  background-color: var(--nav-bg);
  border-bottom: 1px solid color-mix(in srgb, var(--nav-fg) 20%, transparent);

  /* The shadow is the bar's own background deepened toward black, so it is
     always the same hue as whatever section the bar is sitting on, only
     darker — and it restyles itself along with the theme. Depth and alpha are
     per-theme knobs. Negative spread keeps it tight under the hairline
     instead of washing down the page. */
  --nav-shadow: color-mix(in srgb, var(--nav-bg) var(--nav-shadow-depth), #000);
  box-shadow: 0 4px 12px -4px
              color-mix(in srgb, var(--nav-shadow) var(--nav-shadow-alpha), transparent);

  /* Hidden state. Also inert to the pointer, so it cannot catch a click on
     the hero while it is off-screen. */
  opacity: 0;
  transform: translateY(-100%);
  pointer-events: none;

  transition: transform 0.42s cubic-bezier(0.4, 0, 0.2, 1),
              opacity 0.28s ease,
              background-color 0.6s ease,
              box-shadow 0.6s ease,
              color 0.6s ease;
}

.nav.nav-visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ── Text destinations, and the name ──────────────────────────────────────
   The name is a destination too (home), so it takes the same hover highlight
   rather than a treatment of its own. The negative margins pull the padded
   hit areas back so the text itself still sits on the page grid. */
/* inline-grid rather than flex: the label and its bold ghost share one cell,
   so the cell is always as wide as the bold text. Going bold on hover then
   costs no width and the neighbouring destinations never shift.
   min-height keeps the hit area at 44px, which padding alone does not at the
   bottom of the UI type scale. */
.nav-logo,
.nav-link {
  display: inline-grid;
  grid-template-areas: "label";
  align-items: center;
  justify-items: center;
  min-height: 44px;
  font-size: var(--text-ui);
  line-height: 1;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  background: none;
  border: none;
  border-radius: var(--radius);
  padding: 12px 16px;
}

.nav-logo > span,
.nav-link > span { grid-area: label; }

/* Name and destinations share the display face and the UI size — the name is
   set apart by weight and by its own hover, not by a different typeface. */
.nav-logo,
.nav-link { font-family: var(--font-display); }

/* The name is the hero of the bar: bold at rest. */
.nav-logo {
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-left: -16px;
}

/* The destinations read as tertiary buttons — medium at rest, bold on hover. */
.nav-link { font-weight: 500; }

.nav-links .nav-link:last-child { margin-right: -16px; }

/* Invisible bold copy of the label, stacked in the same grid cell purely to
   hold the width open. Only the destinations change weight, so only they
   need it. */
.nav-link::after {
  content: attr(data-label);
  grid-area: label;
  font-weight: 700;
  visibility: hidden;
  pointer-events: none;
}

/* Destinations thicken on hover. Nothing moves and nothing fills — the weight
   is the whole signal, and the ghost above keeps the width from changing. */
.nav-link:hover > span,
.nav-link:focus-visible > span {
  font-weight: 700;
}

/* The name gets its own treatment instead: a band of lighter type travels
   across the letters once, left to right. The gradient is painted through the
   glyphs via background-clip, and both ends of it are --nav-fg, so at rest the
   name is simply solid and nothing appears to be going on.
   Guarded by @supports so the name can never end up invisible on a browser
   that cannot clip a background to text. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .nav-logo > span {
    --nav-shimmer: color-mix(in srgb, var(--nav-fg) 50%, var(--nav-bg));
    background-image: linear-gradient(100deg,
      var(--nav-fg) 42%,
      var(--nav-shimmer) 50%,
      var(--nav-fg) 58%);
    background-size: 300% 100%;
    background-repeat: no-repeat;
    background-position: 100% 0;   /* the band parked off the left edge */
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
  }

  .nav-logo:hover > span,
  .nav-logo:focus-visible > span {
    animation: nav-shimmer 900ms ease-out;
  }
}

@keyframes nav-shimmer {
  from { background-position: 100% 0; }
  to   { background-position: 0% 0; }
}

/* Focus ring sits outside the padded box, on the bar's own background. */
.nav-logo:focus-visible,
.nav-link:focus-visible {
  outline: 2px solid var(--nav-fg);
  outline-offset: 4px;
}

/* Reduced motion: the bar still arrives and the destinations still thicken —
   both carry meaning — but nothing is animated into place, and the shimmer,
   which is decoration rather than affordance, does not run at all. */
@media (prefers-reduced-motion: reduce) {
  .nav      { transition: opacity 0.2s ease; transform: none; }
  .nav.nav-visible { transform: none; }
  .nav-logo:hover > span,
  .nav-logo:focus-visible > span { animation: none; }
}


/* ─── Sections ─────────────────────────────── */
.section {
  height: 100svh;
  padding: 6vw var(--page-margin);
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  z-index: 1;
}

#work.section,
#contact.section {
  height: auto;
  padding-top: max(5rem, 10vw);
  min-height: 100svh;
}

/* The hero grows past one screen when it has to. With the 408px flip card the
   stacked tablet layout is taller than the viewport, and a fixed 100svh let
   the headline spill into #about. When it fits, min-height keeps it exactly
   one screen, as before. */
#introduction.section {
  height: auto;
  min-height: 100svh;
}

/* Extra breathing room at the end of the cases — stops the next section
   pulling in while you read the last row */
#work.section {
  padding-bottom: max(6rem, 14vw);
}

.section-inner {
  width: 100%;
}

.section-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--text-section);
  line-height: 1.111;
  letter-spacing: -0.02em;
  color: var(--color-cream);
  max-width: 49%;
  margin-bottom: clamp(1rem, 2vw, 2rem);
}

/* ─── Button ──────────────────────────────────
   THE pill button for this site. One definition, every consumer — change it
   here and every button on index.html moves with it. That is the whole point
   of it existing: the About CTA and the project cards' "Read Full Story" were
   two separate rules that had drifted apart in size, weight, gap and shape,
   and were pulled onto this one component on instruction.

   Consumers today:
     .about-cta          #about's "more about me"
     .pro-cta            each project card's arrow (adds .btn--icon too)

   Add the class alongside the consumer's own, e.g. class="btn about-cta".
   The consumer keeps only what is genuinely its own — where it sits in its
   parent's layout, and what it does on hover. Those two differ on purpose:
   .about-cta fades out because the custom cursor takes over that section,
   while a card's nudges its arrow. Neither belongs to every button, so
   neither is in here.

   The 999px radius is the documented "explicit instruction" exception to
   CLAUDE.md's two-radius rule — the same one .cs-did li and the filter pills
   carry, and the reason --radius-round is not reused (its own rule reserves
   it for three floating icon buttons).

   Not shared with case-study.css's "Next project" button: that stylesheet
   serves different pages and is not loaded here. If the two should ever be
   one component, this is the definition to move, not to copy. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 16px;
  padding: 16px 24px;
  border-radius: 999px;
  background: var(--color-dark);
  color: var(--color-cream);
  font-family: var(--font-ui);
  font-weight: 400;
  font-size: var(--text-ui);
  line-height: 1;
  white-space: nowrap;
  border: none;
  text-decoration: none;
  cursor: pointer;
}

/* An SVG is a flex item like any other and defaults to flex-shrink: 1, so its
   width/height attributes are only a starting size. Measured: the card CTA's
   arrow came out 19.4px against the about button's 20px, off by six tenths of
   a pixel — invisible, and exactly the kind of drift a shared component is
   supposed to make impossible. The icon is a fixed object; it does not
   negotiate. (The old .pro-cta-icon badge carried this and it was lost when
   the badge went.) */
.btn svg { flex: 0 0 auto; }

/* ── Icon-only modifier ──
   The project cards' CTA. The text is dropped and only the arrow kept, on
   instruction — the full pill was too tall for the heading and a whole card
   to clear one fold.

   This deliberately breaks with #about's button on two properties, and that
   IS the instruction: the earlier brief was "make the card CTAs consistent
   with section 2", and this one supersedes it for shape and padding only.
   Ground, ink, icon size, font and every transition still come from .btn, so
   the two remain one family rather than two rules drifting apart again.

   The radius is var(--radius) because the brief says match the card, and the
   card is var(--radius). Note the side effect: this is the one button on the
   page that does NOT need the 999px pill exception, because it holds no
   literal at all.

   Padding is measured off the reference, not guessed. There the button is
   116x76 around a 32px icon box — 22px above and below, 42px either side, so
   0.69 and 1.31 times the icon. Against this site's 20px arrow that is 13.75
   and 26.25, neither on the 4px grid. Rounding both DOWN to 12 and 24 keeps
   the proportion: 68x44 is an aspect of 1.55 against the reference's 1.53,
   inside 2%, where 16/28 would have squashed it to 1.46. Rounding down is
   also what keeps the height at 44px, which is exactly the tap-target floor
   CLAUDE.md sets — one step smaller and this stops being tappable. */
.btn--icon {
  padding: 12px 24px;
  border-radius: var(--radius);
}

/* ─── Work: project cards ─────────────────────
   A centred row of filter pills over a 2x2 grid of cards, following
   sohayainderkaur.com: square media panel on top, then eyebrow, title,
   summary and a CTA. (The CTA is icon-only now — see .btn--icon.)

   This is the one light block in the middle of the page — the section is
   data-bg="hero" (cream), so unlike every other card on this site these sit
   on a light ground: white cards on cream, dark ink, and the CTA is a dark
   pill the way the reference has it.

   Centred as a block, but the card's own contents stay left-aligned, which is
   how the reference does it too — centring the copy inside a card would leave
   every line's left edge ragged against the thumbnail above it.

   ── On the 999px radii below ──
   CLAUDE.md allows two radius tokens and calls a literal a bug. These three
   pills are the documented "explicit instruction" exception: the brief asked
   for the reference's filter pills and "the exact CTA" by name, and a pill at
   var(--radius) is a rectangle. Same literal and same reasoning as
   .cs-did li in case-study.css. --radius-round is deliberately not reused —
   its own rule reserves it for three floating icon buttons. Worth promoting
   all four to a --radius-pill token if a fifth ever appears.

   ── On the eyebrow's colour ──
   The reference's eyebrow is coral. This site's coral is spacefire (#FC7A69),
   and it cannot be used here: on a white card it measures 2.58:1, and the
   eyebrow is 16px bold, which WCAG counts as normal text needing 4.5:1.
   CLAUDE.md both forbids adding a third low-contrast spacefire pairing
   without raising it, and records that darkening spacefire to buy contrast
   has been tried and rejected twice. So the eyebrow is a muted ink instead —
   see .pro-eyebrow. */
.pro-section {
  /* Two 448px cards plus the 40px gap. Centred rather than left-aligned,
     which is this section only — every other block on the page still starts
     at the page margin.

     936, not 1140, on instruction: the cards were scaled to 80% so that the
     heading and a whole card clear one fold. The card width is not set
     anywhere — the grid divides this figure — so this number IS the 80%, and
     the gap has to be subtracted before scaling, not after: (1140 - 20) x 0.8
     = 896 of card, + 40 of gap = 936. Scaling 1140 whole would have shrunk
     the gap along with the cards, which is the opposite of the instruction. */
  width: 100%;
  max-width: 936px;
  margin-inline: auto;
}

/* The section heading is centred and uncapped for this section alone. The
   base .section-title caps itself at max-width: 49%, which is what was
   wrapping "Work I'm proud of" onto a second line. */
#work .section-title {
  font-size: var(--landing-h-size);
  font-weight: var(--landing-h-weight);
  max-width: none;
  text-align: center;
  color: var(--color-dark);
  margin-inline: auto;
}

/* ── Filter pills ── */
.pro-filters {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin-bottom: clamp(24px, 3vw, 40px);
}

.pro-filter {
  font-family: var(--font-ui);
  font-size: var(--text-meta);
  font-weight: 400;
  line-height: 1;
  color: color-mix(in srgb, var(--color-dark) 72%, var(--color-cream));
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--color-dark) 18%, transparent);
  border-radius: 999px;   /* see the note above */
  padding: 12px 20px;
  cursor: pointer;
}

/* Selected inverts to the dark ink, the same move the CTA makes, so "active"
   reads the same way in both places. */
.pro-filter.is-active {
  background: var(--color-dark);
  border-color: var(--color-dark);
  color: var(--color-cream);
}

@media (hover: hover) and (pointer: fine) {
  .pro-filter {
    transition: color 0.25s ease, border-color 0.25s ease, background-color 0.25s ease;
  }
  .pro-filter:hover:not(.is-active) {
    color: var(--color-dark);
    border-color: color-mix(in srgb, var(--color-dark) 45%, transparent);
  }
}

.pro-filter:focus-visible { outline: 2px solid var(--color-dark); outline-offset: 3px; }

/* ── The grid ── */
.pro-grid {
  display: grid;
  /* Exactly two columns, not auto-fit: at wide viewports auto-fit was finding
     room for a third track and breaking the 2x2. minmax(0, 1fr) rather than a
     bare 1fr so a long unbroken word in a card cannot push its column wider
     than its share. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 40px;
  /* stretch (on instruction, 2026-10-13): the cards in a row share one
     height, so their CTAs, pinned to the foot by margin-top: auto, line up. */
  align-items: stretch;
}

.pro-card {
  display: flex;
  flex-direction: column;
  padding: 24px;
  background: var(--color-white);
  border-radius: var(--radius);
  /* Shadow, not a border, on instruction. Shared with .tm-card via the token
     — see --card-shadow in :root for the two-layer reasoning and the reach
     arithmetic that CLAUDE.md gotcha 11 needs. Nothing between a .pro-card
     and the page scrolls, so nothing here can clip it; the testimonial row
     is the case that does. */
  box-shadow: var(--card-shadow);
  color: inherit;
  text-decoration: none;
}

/* What makes the filter pills actually filter. projects.js marks a card that
   is not in the selected category with the hidden attribute, but the browser's
   own [hidden] rule is only a display: none in the user-agent sheet, and the
   display: flex above beats it, so a "hidden" card stayed on the page and kept
   its grid cell. This puts the rule back, at the same specificity as the
   display it overrides but later in the file.

   Taking the card out of the layout (not just out of sight) is the point: the
   grid is exactly two fixed columns, so the cards that remain close up in
   order from the top left at their normal width. One project sits in the first
   column at the same size it has in the full set, with the second column left
   empty, rather than stretching across or sitting where its hidden neighbours
   were. */
.pro-card[hidden] {
  display: none;
}

@media (hover: hover) and (pointer: fine) {
  a.pro-card {
    transition: box-shadow 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
                transform  0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  }
  /* The lift now has to come from the shadow as well as the translate — with
     the border gone there is nothing else left to change. Deepest layer is
     0 16px 40px -12px, so the downward reach is 16 + 20 - 12 = 24px, less the
     4px the card rises = 20px below and 28px above. Verified by measurement
     against every ancestor scroller: #work and .pro-section are both
     overflow: visible, so there is no scrollport between the card and the
     page, and the page's own overflow-x: hidden on <html> only clips
     horizontally at the viewport edge, 200px+ clear of a 936px centred grid. */
  a.pro-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--card-shadow-hover);
  }
  a.pro-card:hover .pro-cta svg { transform: translateX(4px); }
  .pro-cta svg { transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
}

@media (prefers-reduced-motion: reduce) {
  a.pro-card, .pro-cta svg, .pro-filter { transition: none; }
  a.pro-card:hover { transform: none; }
  a.pro-card:hover .pro-cta svg { transform: none; }
}

a.pro-card:focus-visible { outline: 2px solid var(--color-dark); outline-offset: 4px; }

/* ── Media ──
   A true square. Takes a still, a GIF or a video; projects.js picks the
   element by file extension and both are styled the same here. Cream panel so
   a screenshot with a white background does not dissolve into the white card,
   and overflow: hidden because a radius on a container does nothing to the
   child without it. */
.pro-media {
  aspect-ratio: 1 / 1;
  background: var(--color-cream);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* An animated thumbnail comes wrapped in <picture> (projects.js swaps in a
   still for reduced motion). <picture> is inline by default and, as a flex
   item here, shrank to 0 x 0, taking the image with it, and a 0-size lazy
   image never even loads. It has to fill the panel the way the image does. */
.pro-media picture {
  display: block;
  width: 100%;
  height: 100%;
}

.pro-media img,
.pro-media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The screens are tall phone captures; their top carries the title and the
     first card, which is the part that makes a project recognisable here. */
  object-position: top center;
  display: block;
}

/* ── The "Fixing bugs" sign (projects.js, SIGN) ──────────────────────────
   A small hazard board in the place of a card's arrow CTA (the project is
   not open yet), a bug crawling along its top edge. It swings from its top
   edge (hover), and a press squashes the bug: it flattens, the count
   updates, and a new one crawls back on. */
.pro-sign {
  position: relative;
  margin-top: auto;              /* where .pro-cta sits: the foot of the card */
  align-self: flex-start;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  transform-origin: 50% 0;
  -webkit-tap-highlight-color: transparent;
}
.pro-sign:focus-visible { outline: 2px solid var(--color-dark); outline-offset: 4px; border-radius: 6px; }
/* Two strings from the top edge of the panel to the board's corners. */
.pro-sign-strings {
  display: none;                 /* the strings were for hanging it over a cover; in the CTA's place it stands */
  width: 72px;
  height: 20px;
  background:
    linear-gradient(var(--color-dark), var(--color-dark)) 12px 0 / 1.5px 100% no-repeat,
    linear-gradient(var(--color-dark), var(--color-dark)) calc(100% - 12px) 0 / 1.5px 100% no-repeat;
  opacity: .7;
}
.pro-sign-board {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 44px;              /* the arrow CTA's own height, so the card's foot matches its neighbours' */
  padding: 4px 12px;
  border-radius: 6px;
  background: #ffcf3f;
  /* The hazard border: black and yellow diagonal stripes, inside a clean
     yellow face. */
  box-shadow:
    inset 0 0 0 4px #ffcf3f,
    0 1px 2px rgba(0, 0, 0, .2),
    0 8px 16px -8px rgba(0, 0, 0, .4);
  border: 4px solid transparent;
  background-image:
    linear-gradient(#ffcf3f, #ffcf3f),
    repeating-linear-gradient(-45deg, #1e1e20 0 6px, #ffcf3f 6px 12px);
  background-origin: border-box;
  background-clip: padding-box, border-box;
}
.pro-sign-text {
  font-family: var(--font-ui);
  font-size: var(--text-label);
  font-weight: 700;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--color-dark);
  white-space: nowrap;
}
.pro-sign-count {
  font-family: var(--font-ui);
  font-size: 10px;
  font-weight: 500;
  color: color-mix(in srgb, var(--color-dark) 70%, transparent);
  white-space: nowrap;
}
.pro-sign-count:empty { display: none; }
/* The bug walks back and forth along the board's top edge, turning at
   each end. */
.pro-sign-bug {
  position: absolute;
  top: -8px;                     /* on the board's top edge */
  left: 0;
  display: block;
  width: 16px; height: 16px;
  animation: pro-bug-walk 4.8s linear infinite;
}
.pro-sign-bug svg { display: block; transition: transform .25s ease, opacity .25s ease; }
@keyframes pro-bug-walk {
  0%    { transform: translate(4px, 0) rotate(90deg); }
  45%   { transform: translate(calc(var(--sign-w, 120px) - 20px), 0) rotate(90deg); }
  50%   { transform: translate(calc(var(--sign-w, 120px) - 20px), 0) rotate(-90deg); }
  95%   { transform: translate(4px, 0) rotate(-90deg); }
  100%  { transform: translate(4px, 0) rotate(90deg); }
}
.pro-sign:hover .pro-sign-bug { animation-duration: 2.4s; }   /* it scurries when you come near */
/* Squashed: the bug flattens and fades; a new one walks on afterwards. */
.pro-sign.is-squashed .pro-sign-bug { animation-play-state: paused; }
.pro-sign.is-squashed .pro-sign-bug svg { transform: scale(1.4, .25); opacity: 0; transition-duration: .18s, .9s; }
@media (prefers-reduced-motion: reduce) {
  .pro-sign-bug { animation: none; transform: translate(8px, 0) rotate(90deg); }
}

.pro-media-empty {
  font-family: var(--font-ui);
  font-size: var(--text-label);
  color: color-mix(in srgb, var(--color-dark) 40%, transparent);
}

/* ── Text ── */
.pro-eyebrow {
  font-family: var(--font-ui);
  /* Matches .pro-summary's size too, on instruction — was --text-meta
     (15.98px), a step below the summary's 16px and enough to read as
     "smaller" once the two also shared a colour. Same token as .pro-summary
     now, not a separate one that happens to resolve close at one viewport:
     --text-ui is a clamp, so a literal 16px here would have agreed with it
     only at the width it was checked at. */
  font-size: var(--pro-eyebrow-size, var(--text-ui));
  font-weight: var(--pro-eyebrow-weight, 500);   /* a step lighter than 700, on instruction */
  line-height: 1.3;
  /* Matches .pro-summary's colour exactly, on instruction — the eyebrow used
     to be a darker 75%-mix (7.3:1) so it would out-rank the summary as the
     card's second-loudest line after the title. Levelled to the summary's
     65%-mix (5.2:1, still AA at this weight and size) instead: the two are
     now one colour, not two independently-tuned ones that happen to look
     close. If .pro-summary's colour ever moves, move this with it. */
  color: color-mix(in srgb, var(--color-dark) 65%, var(--color-white));
  /* The eyebrow/title/summary rhythm was 8 / 12 — uneven. Levelled at the
     LARGER of the two so nothing tightened, then +8 as instructed: 20px in
     both gaps. */
  margin: 0 0 var(--pro-gap-eyebrow, 20px);
}

.pro-eyebrow span { opacity: 0.5; margin: 0 4px; }

/* Clamped to 2 lines. The four cards' headers now run from 4 to 6 words at
   this column width, which at 1 line would leave three cards a whole line
   shorter than the fourth — and with the CTA pinned to margin-top: auto (see
   .pro-cta), that shortfall moves the arrow up the card, not down the page.
   2 lines is the ceiling every current header needs at most; a longer one
   truncates with an ellipsis rather than pushing the card taller than its
   neighbours. */
.pro-title {
  font-family: var(--font-display);
  font-weight: var(--pro-title-weight, 500);
  font-size: var(--pro-title-size, var(--text-body));
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--color-dark);
  margin: 0 0 var(--pro-gap-title, 8px);    /* on instruction: title and summary read as one block */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* Clamped to 2 lines, for the same reason as .pro-title just above: four
   cards, four summary lengths, one shared ceiling so the cards keep matching
   heights instead of each settling at whatever its own copy happens to run
   to. This is the harder-working half of "consistency in sizing" — the
   media panel is already a fixed square and the eyebrow is one line, so
   title and summary were the only two boxes left that could still vary. */
.pro-summary {
  font-family: var(--font-ui);
  font-size: var(--pro-summary-size, var(--text-ui));
  font-weight: var(--pro-summary-weight, 400);
  line-height: 1.5;
  /* 5.2:1 — AA for normal text. Also .pro-eyebrow's colour now — see the
     note there. */
  color: color-mix(in srgb, var(--color-dark) 65%, var(--color-white));
  margin: 0 0 var(--pro-gap-summary, 26px);   /* dialled (?dial=cards), 2026-10-13 */
  max-width: 46ch;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* ── CTA ──
   An icon-only .btn: the arrow alone, a corner radius matching the card's,
   and padding taken off the supplied reference. All of that lives on
   .btn--icon near .section-title; see the note there for the arithmetic and
   for why this one no longer matches #about's button.

   All four cards carry it, including the two with no href. Their card is a
   <div>, not an <a>, so the arrow is decoration on something that does not
   navigate — it replaced an "In progress" label that said so in words. That
   was asked for explicitly and is flagged rather than quietly softened.

   What stays here is only what is this button's own: margin-top: auto pins it
   to its own card's bottom edge, and align-self stops the flex column
   stretching it to the card's full width. Its hover (the arrow nudge) lives in
   the hover block above, next to the card lift it belongs to.

   Note that this does NOT make the CTAs level across a row — .pro-grid sets
   align-items: start, so each card is only as tall as its own content and a
   two-line summary sits its button ~24px above a three-line one's. Pinning to
   the bottom of a box that is not the same height as its neighbour cannot do
   more than that. Switching the grid to the default `stretch` would level them
   by making both cards the height of the taller; it is a visual change beyond
   what was asked for, so it has been left alone and flagged instead. */
.pro-cta {
  margin-top: auto;
  align-self: flex-start;
}

/* One column once two no longer fit. 860px is where a 2-col grid of these
   cards stops having room for the media panel and a readable measure beside
   it, not a device width. */
@media (max-width: 860px) {
  .pro-grid { grid-template-columns: minmax(0, 1fr); gap: 24px; }
}

@media (max-width: 640px) {
  .pro-card { padding: 16px; }
  .pro-media { margin-bottom: 16px; }
  .pro-summary { margin-bottom: 20px; }
}

/* ─── Hero ─────────────────────────────────── */
.section[data-bg="hero"] {
  justify-content: flex-start;
  align-items: flex-start;
}

/* ─── Hero: ScrollCue ───────────────────────────
   A mouse outline whose dot drops and rises on a loop, after the ScrollCue
   Framer component (supplied recording). Rebuilt here rather than embedded:
   this site has no Framer runtime. Every option the component exposes is a
   variable below; speed and pause are read by script.js, which plays the loop
   with the Web Animations API because a pause between loops cannot be written
   as a CSS keyframe percentage that follows a variable.

   Placement: bottom centre of the hero, 48px off its foot, in the fold's
   bottom padding (see #introduction.g-hero).

   Mouse only, desktop only: a mouse is the wrong picture on a touch screen,
   and below 1025px the hero is taller than the screen, so a cue at its foot
   would be out of view anyway. */
.scroll-cue {
  --cue-w: 24px;
  --cue-h: 40px;
  --cue-dot: 4px;
  --cue-travel: 12px;             /* how far the dot drops */
  --cue-icon: var(--color-dark);   /* the dot */
  --cue-bg: var(--color-cream);    /* masks the dot field behind it */
  --cue-border: color-mix(in srgb, var(--color-dark) 56%, var(--color-cream));
  --cue-border-w: 2px;
  --cue-radius: 12px;              /* half the width: a full pill */
  --cue-speed: 1.4s;               /* one drop and return (0.5s - 3s) */
  --cue-pause: 0.6s;               /* still time between loops (0s - 5s) */

  position: absolute;
  bottom: 48px;
  left: calc(50% - var(--cue-w) / 2);
  z-index: 2;
  display: none;
  width: var(--cue-w);
  height: var(--cue-h);
  padding: 0;
  border: var(--cue-border-w) solid var(--cue-border);
  border-radius: var(--cue-radius);
  background: var(--cue-bg);
  cursor: pointer;
  transition: opacity 0.4s ease, border-color 0.2s ease;
}
.scroll-cue-dot {
  position: absolute;
  top: 8px;
  left: calc(50% - var(--cue-dot) / 2);
  width: var(--cue-dot);
  height: var(--cue-dot);
  border-radius: 50%;
  background: var(--cue-icon);
}
.scroll-cue:hover { --cue-border: var(--color-dark); }
.scroll-cue:focus-visible {
  outline: 2px solid var(--color-dark);
  outline-offset: 4px;
}
/* Gone once the reader has started scrolling; script.js sets it. */
.scroll-cue.is-gone {
  opacity: 0;
  pointer-events: none;
}
@media (min-width: 1025px) and (hover: hover) {
  .scroll-cue { display: block; }
}

/* ─── Hero (2026-10-06) ──────────────────────────────────────────────────────
   Ported from the prototype hero-option-g2.html: an eyebrow and three-line
   headline over a curved carousel of three pills, with one flip card that
   turns to a new face for each. Markup and behaviour: index.html and hero.js.
   Scoped to #introduction.g-hero where a rule could collide. */
/* ── Option G2 ───────────────────────────────────────────────────────────
   Three pills on a curved carousel, one flip card that turns to a new face
   for each. Prototype only. */
#introduction.g-hero {
  min-height: 100svh;
  /* 72px under the fixed nav bar, which is visible from the top here. */
  padding: 96px 0 48px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  overflow: hidden;
}
.g-eyebrow {
  margin: 0 0 var(--g-gap-eyebrow, 16px);
  /* Instrument Serif italic, dialled (loaded in index.html). Instrument has one
     weight, so 500 renders as its regular. */
  font-family: var(--g-eyebrow-font, 'Instrument Serif'), 'Times New Roman', serif;
  font-style: var(--g-eyebrow-style, italic);
  font-weight: var(--g-eyebrow-weight, 500);
  font-size: var(--text-eyebrow);
  line-height: 1.1;
  color: color-mix(in srgb, var(--color-dark) 72%, var(--color-white));
}
.g-head-wrap { width: min(100% - 12vw, 960px); container-type: inline-size; }
.g-head {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--landing-h-weight);
  /* --landing-h-size defaults to --text-case, the page-title token. The cap
     keeps the longest line inside its container; ?dial=header lifts it. */
  font-size: min(var(--landing-h-size), var(--g-head-cap, 100cqi / 13.2));
  line-height: 1.08;
  letter-spacing: -0.02em;
  color: var(--color-dark);
}
.g-line { display: inline; white-space: nowrap; }
/* Break after the second line only (dialled): the first two share a row. */
.g-line:nth-of-type(2)::after { content: "\A"; white-space: pre; }
.g-word { display: inline-block; white-space: nowrap; }

/* Headline letters that turn into objects under the cursor (after wabi.ai). */
/* Only the letter (.g-glyph) hides during a swap; the object beside it in
   .g-char stays visible. Hiding the whole .g-char took the object with it,
   which left a blank gap in the word. The letter returns in 120ms, so it is
   back almost at once when the object leaves. */
.g-char { display: inline-block; position: relative; }
.g-glyph { display: inline-block; transition: opacity .12s ease, transform .12s ease; }
.g-char.is-swapped .g-glyph { opacity: 0; transform: scale(.6); }
.g-head.is-entering .g-glyph { transition: none; }
.g-obj {
  position: absolute; left: 50%; top: 50%;
  font-size: .78em; line-height: 1;
  pointer-events: none;
  translate: -50% -46%;
}

.g-bio {
  max-width: 520px;
  margin: 20px auto 0;
  padding: 0 6vw;
  box-sizing: content-box;
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.5;
  color: color-mix(in srgb, var(--color-dark) 65%, var(--color-white));
}

/* ── Curved pill carousel ──
   The pills sit on the outside of a cylinder whose axis is vertical and
   whose front faces the reader. The script places each one by its arc
   distance from the centre: it swings round to the side, recedes and turns
   edge-on as it nears either end, and is hidden past ~80 degrees. Type
   matches wabi's pills: 500 weight, -0.04em, 21.76px at 1440. */
.g-strip {
  position: relative;
  /* A window on the loop, not the full width: the selected pill and its two
     neighbours only, the neighbours cut off and faded at the window's edges
     (after wabi.ai). */
  width: min(640px, 100%);
  /* 120px tall with 16px given back below: the active pill's shadow reaches
     ~24px under it, and this window clips, so it needs real room. */
  height: 120px;
  margin: var(--g-gap-pills, 32px) 0 -16px;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 22%, #000 78%, transparent);
  /* Far enough that the arc's ends still reach the screen edges once depth
     has shrunk them (900px pulled them in to ~150px from each side). */
  perspective: 1800px;
  perspective-origin: 50% 50%;
  transform-style: preserve-3d;
  touch-action: pan-y;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}
.g-strip.is-dragging { cursor: grabbing; }
/* The strip is focusable for the arrow keys. Its own outline drew a black
   box round the whole window (the stray lines); the ring goes on the
   selected pill instead, and only for keyboard focus. */
.g-strip:focus { outline: none; }
.g-strip:focus-visible .g-pill.is-active { outline: 2px solid var(--color-dark); outline-offset: 2px; }
.g-pill {
  position: absolute;
  left: 50%; top: 50%;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  height: 64px;
  margin-top: -32px;
  padding: 0 24px 0 12px;
  border: none;
  border-radius: 999px;            /* full-pill exception, as .btn */
  background: transparent;
  color: color-mix(in srgb, var(--color-dark) 56%, transparent);
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: clamp(18px, 1.511vw, 24px);
  letter-spacing: -0.04em;
  white-space: nowrap;
  cursor: inherit;
  backface-visibility: hidden;
  transition: background-color .3s ease, color .3s ease, box-shadow .3s ease;
}
.g-pill img, .g-pill .g-thumb {
  flex: 0 0 auto;
  width: 32px; height: 32px;
  border-radius: 999px;
  object-fit: cover;
  -webkit-user-drag: none;
}
.g-emoji { flex: 0 0 auto; width: 32px; text-align: center; font-size: 26px; line-height: 1; letter-spacing: 0; }
/* The timer (after wabi.ai): the selected pill fills from left to right
   over 3s, then the next pill and card come in. The fill is a layer behind
   the label, clipped to the capsule. It pauses while the pointer is on the
   pills or the card. */
.g-pill { overflow: hidden; isolation: isolate; }
.g-pill::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: color-mix(in srgb, var(--color-dark) 7%, var(--color-white));
  transform: scaleX(0);
  transform-origin: 0 50%;
}
.g-pill.is-timing::before { animation: g-fill 3s linear forwards; }
.g-hero.is-paused .g-pill.is-timing::before { animation-play-state: paused; }
@keyframes g-fill { to { transform: scaleX(1); } }
.g-pill.is-active {
  background: var(--color-white);
  color: var(--color-dark);
  box-shadow: 0 2px 4px color-mix(in srgb, var(--color-dark) 6%, transparent),
              0 16px 40px -12px color-mix(in srgb, var(--color-dark) 22%, transparent);
}

/* ── Card ──
   Built the way the live flip card is: each face is a 3D group, not a flat
   picture. The photo layer is the only thing that clips; the content layer
   floats 24px in front of it and the title 64px in front, so as the card
   turns (and as it tilts toward the cursor) the type parallaxes off the
   image. Nothing that is preserve-3d has overflow: hidden, which is what
   makes Safari flatten a 3D group. */
.g-stage {
  margin-top: var(--g-gap-cards, 16px);
  perspective: 1000px;
  /* A polished floor: the card is mirrored below its own foot, the mirror
     fading out within ~90px. Chrome and Safari draw it; Firefox has no
     box-reflect and shows the card with its contact shadow only. The
     reflection takes no layout space. */
  -webkit-box-reflect: below 6px linear-gradient(transparent 78%, rgba(0, 0, 0, .22));
}
.g-card {
  --w: var(--g-card-w, 236px);
  position: relative;
  width: var(--w);
  aspect-ratio: 280 / 408;
  transform-style: preserve-3d;
  cursor: pointer;
}
.g-card-shadow {
  position: absolute; inset: 6% 4% -2%;
  border-radius: 14px;
  background: rgba(0,0,0,.28);
  filter: blur(18px);
  opacity: .42;
  transform: translateZ(-1px);
}
.g-face { position: absolute; inset: 0; transform-style: preserve-3d; }
.g-face--b { transform: rotateY(180deg); }
.g-media, .g-layer {
  position: absolute; inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.g-media {
  border-radius: 14px;
  overflow: hidden;
  background: var(--color-charcoal);
}
.g-media > img { width: 100%; height: 100%; object-fit: cover; display: block; -webkit-user-drag: none; }
.g-field {
  position: absolute; inset: 0;
  background-image: radial-gradient(circle, color-mix(in srgb, var(--color-cream) 22%, transparent) 1px, transparent 1px);
  background-size: 16px 16px;
  background-position: 8px 8px;
}
.g-shade { position: absolute; inset: 0; background: #000; opacity: 0; pointer-events: none; }
.g-layer { pointer-events: none; transform-style: preserve-3d; }
.g-layer--mid   { transform: translateZ(24px); }
.g-layer--title { transform: translateZ(64px); }
.g-title {
  position: absolute; left: 0; right: 0;
  top: 88%;
  transform: translateY(-50%);
  font-family: 'Clash Display Variable', var(--font-display);
  font-weight: 600;
  font-size: 22px;
  line-height: 1;
  letter-spacing: -0.01em;
  word-spacing: .14em;
  white-space: nowrap;
  color: var(--color-cream);
  text-shadow: 0 8px 24px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.2);
}

/* 5+ Years: bricks. */
/* Card pills: the 5+ Years and Industries stacks (.g-brick), one component. Each hugs its content, is fully round, and
   sits at its own depth (translateZ, set in hero.js) so it parallaxes as
   the card tilts. Dark on the dark card, the fill a lighter charcoal at 50%
   so the card's dots show through; a faint cream top edge lifts it. */
.g-brick {
  position: absolute;
  left: 0; top: 0;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 44px;
  padding: 0 16px 0 6px;
  border-radius: 999px;            /* full-pill exception, as .btn */
  background: color-mix(in srgb, color-mix(in srgb, var(--color-charcoal) 82%, var(--color-cream)) 50%, transparent);
  color: var(--color-cream);
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: 16px;
  letter-spacing: -0.02em;
  white-space: nowrap;
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--color-cream) 10%, transparent),
              0 1px 1px rgba(0,0,0,.3), 0 14px 28px -10px rgba(0,0,0,.6);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.g-brick { transform-origin: 50% 100%; opacity: 0; }
.g-brick--seg { padding: 0 16px; font-weight: 700; }
.g-brick img, .g-brick .g-logo { width: 32px; height: 32px; border-radius: 999px; object-fit: cover; flex: 0 0 auto; }
.g-logo { display: grid; place-items: center; background: var(--color-dark); color: var(--color-cream); font-weight: 700; font-size: 13px; }

@media (max-width: 640px) {
  /* The nav wraps to two rows here (105px), so the fold starts lower. */
  #introduction.g-hero { padding-top: 128px; }
  .g-line { white-space: normal; }
}
@media (prefers-reduced-motion: reduce) { .g-char { transition: none; } }
.g-hero .scroll-cue { bottom: 20px; }

/* ── Dots layer — absolute in page flow, scrolls naturally ─
   Starts at the end of the hero (≈720/840 of its height) and runs down
   through the about section. No fixed, no clip-path: more of it appears
   as the scroll advances. */
#snap {
  position: relative;
}

/* The field itself. Shared by the page-level layer below, the testimonials
   section, and the case study's prologue visual, so the pattern lives in
   exactly one place and the three fields cannot drift in tile size or dot
   weight. .cs-pro-dots takes only the pattern from here — its box, mask and
   opacity are the case study's own, in case-study.css. */
/* Part one: the pattern. .more-overlay joins the list here and NOT in part two
   below, because it is the only field that is not a dedicated layer -- it is
   the About me overlay's own scroll container, so an opacity on it would fade
   the overlay's contents along with the dots. It carries its lightness in
   --dot-ink instead; see .more-overlay.

   The gradient is written here, in the rule that matches the consumers, rather
   than held as a value in a :root token. A var() inside a custom property is
   substituted where that property is COMPUTED, so a --dot-image declared on
   :root would resolve var(--dot-ink) against :root -- always the fallback --
   and every field would inherit full-strength ink no matter what it set. That
   was tried and measured on 2026-09-29: the overlay's 16% ink came back as
   rgb(30, 30, 32). Declared here, --dot-ink resolves per element. */
.dots-layer,
#testimonials::before,
#work::before,
#visual::before,
.more-overlay,
.cs-pro-dots,
.cs-jn-dots,
.cs-sps-dots,
.cs-ground-dots,
.cs-impact-dots,
.cs-screen-dots {
  /* --dot-ink so a field on a dark ground can invert, or a field over running
     body copy can lighten, without forking the pattern -- the tile size and
     dot weight, the two things the comment above exists to keep in one place,
     stay shared. Every field that does not set it gets --color-dark, which is
     what all of them were. */
  background-image: radial-gradient(circle, var(--dot-ink, var(--color-dark)) var(--dot-dot), transparent var(--dot-dot));
  background-size: var(--dot-tile) var(--dot-tile);
  /* Half-tile offset so the first dot sits inside the box rather than being
     sliced in half at the edge. */
  background-position: calc(var(--dot-tile) / 2) calc(var(--dot-tile) / 2);
}

/* Part two: the dedicated layers only. Both declarations would be wrong on
   .more-overlay -- the opacity would fade its content, and pointer-events:
   none would make the whole overlay unclickable. */
.dots-layer,
#testimonials::before,
#work::before,
#visual::before,
.cs-pro-dots,
.cs-jn-dots,
.cs-sps-dots,
.cs-ground-dots,
.cs-impact-dots,
.cs-screen-dots {
  opacity: var(--dot-opacity);
  pointer-events: none;
}

.dots-layer {
  position: absolute;
  /* --dots-top / --dots-h are set by script.js only when the hero is taller
     than the viewport (the flip card does that on phones and tablets). The svh
     figures below assume a hero exactly one screen tall; past that they put
     the field behind the headline. When the hero fits, neither is set and
     these fallbacks are what renders, unchanged. */
  top: var(--dots-top, calc(85.7svh - 8px));   /* half-tile retreat — kills a transparent seam at the top edge */
  /* --dots-left / --dots-right are set by script.js on desktop so the field
     starts at the card's left edge and ends at the headline's right edge,
     rather than at the page margins, which neither column sits on. */
  left: var(--dots-left, var(--page-margin));
  right: var(--dots-right, var(--page-margin));
  height: var(--dots-h, calc(114.3svh + 8px)); /* covers to 200svh: (200 - 85.7)svh + the 8px retreat */
  z-index: 0;
}

/* The same field behind the testimonials fold. Scoped to the section rather
   than added to the page-level layer above: that one is positioned in svh from
   the top of <main>, and #testimonials sits below #work, which is taller than
   the viewport and free to change height — any svh figure for it would be a
   guess that silently drifts. Anchoring to the section costs no arithmetic and
   cannot go stale.
   z-index -1 keeps it above the section's own (transparent) background but
   below every in-flow child, so the title and the cards both sit on top of it.
   .section sets position: relative and z-index: 1, which makes #testimonials a
   stacking context — that is what stops the negative z-index escaping behind
   the page background. */
#testimonials::before {
  content: '';
  position: absolute;
  inset: 0 var(--page-margin);
  z-index: -1;
}

/* The same field behind the project cards. Scoped to the section rather than
   positioned in svh from the top of the page, for the reason #testimonials is:
   #work's height depends on how many cards are showing, and the filter pills
   change that at run time, so any fixed figure would go stale the moment
   someone clicks B2B.

   Inset by --page-margin on the sides like the other two, which is what puts
   all three fields' dot columns on the same x positions. z-index: -1 sits it
   above the section's own background but below every in-flow child — this
   works only because .section sets position: relative and z-index: 1, which
   makes #work a stacking context and stops the negative z-index escaping
   behind the page ground.

   .pro-card is opaque (--color-white) so the cards read as sitting ON the
   field rather than the dots running through the copy. If this section's
   data-bg ever goes back to dark, that fill and --dot-ink both have to
   change with it. */
#work::before {
  content: '';
  position: absolute;
  inset: 0 var(--page-margin);
  z-index: -1;
}

/* The folder section between them carries the same field, so the dots run
   unbroken from the project cards to the testimonials. The three are
   separate boxes, so each one's rows are nudged by --dot-shift (set by
   script.js from the section's offset) onto one page-wide grid: without it
   the rows would jump at each seam unless every section height happened to
   be a multiple of the tile. */
#visual::before {
  content: '';
  position: absolute;
  inset: 0 var(--page-margin);
  z-index: -1;
}
#work::before,
#visual::before,
#testimonials::before {
  background-position: calc(var(--dot-tile) / 2) calc(var(--dot-tile) / 2 + var(--dot-shift, 0px));
}

/* Inert in the reference — no matching elements. Kept for parity. */
.accent {
  color: var(--color-green);
  transition: color 0.6s ease;
}

.wave-hover {
  color: inherit;
  text-decoration: none;
  cursor: none;
  transition: color 0.2s ease;
}

.wave-hover:hover {
  color: var(--color-green);
}

/* ─── About ────────────────────────────────── */

/* Absolute centring — does not depend on section height */
#about .section-inner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Widened from 62.083vw (894px @ 1440). .about-text now carries three
     manual <br> breaks (see the markup comment above it) chosen to read as
     four specific lines, and the third of those — "It understands how they
     already" — does not fit inside the old 894px measure: it needs ~929px at
     this same 64px type size, real DOM layout confirmed the wrap holds below
     927px and breaks above 930px, and 65.5vw (943px @ 1440) clears that with
     ~13px to spare for font-hinting differences across browsers. Because the
     four lines are now pinned by <br> rather than left to find their own
     wrap, widening the box no longer pulls words up from one pinned line into
     another the way it would with plain wrap — each line only gains
     breathing room against wrapping into a second row of its own.

     The max(…, 540px) floor covers a gap that 65.5vw alone leaves open.
     --text-headline (the type driving all of this) is clamp(2.25rem, 4.444vw,
     6rem) — its own floor takes over below an 810px viewport, so from there
     down to the 640px mobile breakpoint the type sits fixed at 36px while
     this box, on 65.5vw alone, keeps shrinking with the viewport. Line 3
     needs ~523px at that fixed size (scaled down from the ~929px it needs at
     64px), and 65.5vw crosses back under that around 798px — confirmed by
     testing real layout down that range: holds at 810px, breaks at 790px.
     540px clears the requirement with the same margin as the 1440px case
     above, and the min() ceiling still protects the narrow end: even at
     641px, the bottom of this rule's range, "100% minus both page margins"
     comes to ~534px, which the max() floor cannot force past — the box is
     capped there instead of hitting 540, and 534px still clears line 3's
     ~523px requirement at that same fixed type size. */
  width: min(max(65.5vw, 540px), calc(100% - 2 * var(--page-margin)));  /* 943px @ 1440 */
  display: flex;
  flex-direction: column;
  gap: 48px;
}

.about-text {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--text-headline);
  line-height: 1.094;
  letter-spacing: -0.02em;
  /* Centred on request. The box itself (#about .section-inner) is unchanged —
     same width, same absolute centring on the page — so this only moves the
     four fixed-length lines within a box that was already sitting in the same
     place. Because the lines are pinned to different lengths on purpose (see
     the markup comment above), centring makes each one sit flush with the
     box's own centre rather than its left edge, which is the point: a ragged
     left edge reads as a mistake once the paragraph is no longer left-set. */
  text-align: center;
  /* Not text-wrap: balance, for the same reason as before text-wrap even
     entered the picture — balance picks whatever measure evens out line
     lengths, and the four lines this paragraph is pinned to on purpose (see
     the markup comment above .about-text) do not have even lengths by
     design, so balance would still be fighting the <br> tags to reflow them.
     Plain wrap is just the fallback for whichever of those four lines is
     still too long for a narrower viewport — each wraps a second row of its
     own rather than balance re-distributing words across all four. */
  max-width: 100%;
}

/* ─── About: word-split reveal ──────────────── */
.word-wrap {
  display: inline-block;
  vertical-align: bottom;
}
.word {
  display: inline-block;
}

.about-bold {
  font-weight: 700;
  /* Plain (non-balanced) wrap on .about-text can otherwise break this short
     emphasised phrase across two lines wherever it lands — keep it intact
     as one visual unit. It is a few words, never long enough to overflow
     the container on its own. */
  white-space: nowrap;
}

/* ─── About: custom cursor ─────────────────── */
#about {
  cursor: none;
}

/* ── Custom cursor ─────────────────────────────────────────────────────────
 * --cursor-bg  → background colour (set from JS per row/section)
 * --cursor-fg  → text colour (set from JS)
 * Appears with a fade + slight scale. Colour transitions smoothly between rows.
 * ----------------------------------------------------------------------- */
.cursor-label {
  position: fixed;
  top: 0;
  left: 0;
  display: inline-flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.5rem;
  /* 999px, on instruction: a full pill, consistent with .about-cta and the
     case study's "Next project" button. */
  border-radius: 999px;
  font-family: var(--font-ui);
  font-size: var(--text-ui);
  font-weight: 400;
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
  z-index: 9999;

  background: var(--cursor-bg, var(--color-dark));
  color:      var(--cursor-fg, var(--color-cream));

  opacity: 0;
  transform: translate(-50%, -50%) scale(0.85);
  transition:
    opacity          0.1s  ease,
    transform        0.1s  ease,
    background-color 0.15s  ease;
}

.cursor-label.visible {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

/* ── Icons ── */
.cursor-label .icon-copy         { display: none; }
.cursor-label .icon-arrow-up-right { display: none; }

/* "contact-email" state — copies the address */
.cursor-label.contact-email .icon-arrow { display: none; }
.cursor-label.contact-email .icon-copy  { display: block; }

/* "contact-link" state — opens an external link */
.cursor-label.contact-link .icon-arrow            { display: none; }
.cursor-label.contact-link .icon-arrow-up-right   { display: block; }

/* "contact-copied" state — feedback after copying (no icon) */
.cursor-label.contact-copied .icon-arrow          { display: none; }

/* "work-soon" state — case study pending (no icon) */
.cursor-label.work-soon .icon-arrow               { display: none; }


/* ─── More overlay ──────────────────────────────────────────────────────────
 *
 * 8-column grid, ref: 1440px, margin: 120px (8.33vw), gutter: 24px
 * Text is always 4 columns wide (588px = 40.83vw).
 * Vertical spacing between blocks: 320px ref = 22.22vw with a clamp.
 *
 * ────────────────────────────────────────────────────────────────────────── */

.more-overlay {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  --mo-margin:   8.33vw;         /* 120/1440 — side margin */
  --mo-gap:      clamp(5rem, 22.22vw, 20rem); /* 320px ref */
  /* The vertical rhythm BETWEEN the overlay's three sections — the headline,
     the reel, and the photo-and-bio unit — on instruction on 2026-09-29:
     "fix the padding between the 3 key sections so that it's consistent. Do
     leave plenty of breathing space".

     Before this there was no rhythm, only three numbers that had each been
     dialled in isolation and never compared. Measured at 1440: 186px above
     the headline, 40px between the headline and the reel, 298px between the
     reel and the unit, 332px below it. The 40 was the outlier — it was the
     reel's old separation from whatever fold sat above it, kept when the
     statement spread became this section's headline — and the 298/332 were
     largely accidental, the sum of a dialled margin and whatever slack the
     fold's min-height happened to leave.

     calc(6vw + 112px), 198px at 1440. Same shape as the two calc(6vw + Npx)
     figures it replaces: 6vw is .section's own top/bottom padding, the
     landing screen's one documented figure for how much room a piece of
     content gets from its neighbour, and the flat term holds a real minimum
     at narrow widths where 6vw alone collapses (54px at 900 wide). Pitched
     at the generous end of what was already there rather than at the middle
     — "plenty of breathing space" — so it reads as more air above the reel,
     not as the page tightening up. 166px at 900, 227px at 1920.

     Used in four places, which is what makes the rhythm even: the padding
     above the headline, the headline's bottom margin, the profile fold's top
     margin, and the same fold's bottom padding. */
  --mo-section-gap: calc(6vw + 112px);
  --mo-col4:     40.83vw;        /* 588/1440 — width of 4 columns */
  --mo-col1:     var(--mo-margin);             /* where col 1 starts */
  --mo-col3:     29.58vw;        /* 426/1440 — where col 3 starts */

  position: fixed;
  inset: 0;
  /* Light mode, on instruction — this whole overlay used to be a dark room
     (var(--color-dark) ground, cream type) deliberately set against the cream
     landing page. It is now the same cream as the page's own hero and about
     grounds, so opening About me reads as going deeper into one site rather
     than stepping into a second one. Every colour below is the mirror of what
     it was; each carries its own note. Nothing here is a fresh palette — the
     ground/ink pair is the site's existing --color-cream / --color-dark, and
     the surfaces on top of it are --color-white with --card-shadow, exactly
     the relationship the project and testimonial cards already have.

     Note there is no grain here, before and after: body::after is fixed at
     z-index 10 and this overlay is 200, so it sits above the noise. That was
     invisible on a dark ground and is slightly more noticeable on cream,
     where the landing page's cream IS grained — flagged, not fixed, because
     fixing it means moving the grain above z-index 200 and that would then
     also sit over the case-study overlays. */
  background-color: var(--color-cream);
  /* The dot field, on instruction on 2026-09-29: "add a dot grid for this
     entire about me page". It replaces the single field that used to sit
     behind the profile fold only (.more-fold--profile::before, now retired),
     so the pattern now runs unbroken from the headline to the end of the
     scroll instead of appearing for one fold.

     Painted on this element's own background rather than on a ::before,
     because this element IS the scroll container. An absolutely positioned
     pseudo-element with inset: 0 fills the container's PADDING BOX — one
     viewport — not its scroll height, so the lifted technique would have
     produced a field that stayed put while the content moved through it.
     background-attachment: local is the counterpart that does what is wanted:
     the background is positioned against the content and scrolls with it, and
     it tiles the whole scrollable canvas.

     background-COLOR above rather than the `background` shorthand it replaced:
     the shorthand resets background-image, so writing it after these lines --
     or reinstating it later -- silently erases the field.

     Lightness is carried by --dot-ink, not by opacity, and this is the one
     field on the site that has to do it that way: opacity here would fade the
     overlay's own contents, and there is no separate layer to put it on. 16%
     of --color-dark mixed into nothing is the same result the retired field
     reached with --dot-opacity: 0.16 on full-strength ink, so the instruction
     behind that value -- "make the pixel grid a lot lighter here so it
     doesn't clash with the text" -- still holds, and now holds over the reel
     and the headline too. --dot-image is the shared pattern from :root, so
     this field still cannot drift from the page's others in tile size or dot
     weight. The pattern itself comes from the shared rule near the top of the
     file, which this element's selector joins, so the overlay's field cannot
     drift from the page's others in tile size or dot weight. Only the ink and
     the attachment are this element's own. */
  --dot-ink: color-mix(in srgb, var(--color-dark) 16%, transparent);
  background-attachment: local;
  z-index: 200;
  overflow-y: auto;
  scroll-snap-type: y proximity;   /* inert — no child sets scroll-snap-align */
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease;
}

.more-overlay.visible {
  opacity: 1;
  pointer-events: all;
}

/* ── Close button — pill with fill and stroke ── */
.more-close {
  position: fixed;
  top: 5.56vw;
  left: var(--mo-margin);
  /* Inverted with the overlay. Was a near-black pill (#1e1e20 ground, #2c2c2c
     hairline, cream arrow) — the same dark-on-dark treatment the ground made
     necessary. On cream it becomes the site's standard raised surface: white,
     a dark hairline at the cards' own 12%, and --card-shadow so it reads as
     sitting ON the page rather than as a white hole cut into it. A flat white
     pill on #f5f4f0 measures 1.10:1 and is effectively invisible; the shadow,
     not the fill, is what makes it a button.
     Arrow ink is --color-dark, which measures 10.4:1 against the white pill
     once the 0.85 opacity below is applied to it (16.6:1 at full). */
  background: var(--color-white);
  border: 1px solid color-mix(in srgb, var(--color-dark) 12%, transparent);
  box-shadow: var(--card-shadow);
  border-radius: var(--radius-round);
  padding: 16px;
  cursor: pointer;
  color: var(--color-dark);
  /* Unchanged, and load-bearing: openOverlay() in script.js animates this
     element's opacity TO 0.85 on entrance, so this is the value the hover
     below lifts from. Do not swap it for a colour change. */
  opacity: 0.85;
  transition: opacity 0.25s ease;
  align-items: center;
  justify-content: center;
  z-index: 201;
  display: none;
}

.more-overlay.visible .more-close {
  display: flex;
}

.more-close:hover {
  opacity: 1;
}

/* ── Intro: full-screen dark — the overlay's "hero" ────────────────── */
/* NOT converted to light mode, and deliberately so. .more-intro is not in
   index.html — the statement fold replaced it — and unlike the flat text
   colours elsewhere in this overlay, its two visual layers (the cream dot
   grid at 4% below, and .more-intro-image::after's fade to --color-dark) are
   compositions that only work over a dark ground. Recolouring them is a
   redesign, not a find-and-replace, so they are left as they are and flagged
   here: if this hero is ever revived on the light overlay, both need
   rebuilding, not inverting. */
.more-intro {
  position: relative;
  height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 0 var(--mo-margin) clamp(4rem, 12vw, 10rem);
  overflow: hidden;
}

/* White dots on the dark ground — very subtle (4% opacity) */
.more-intro::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, var(--color-cream) 1px, transparent 1px);
  background-size: 16px 16px;
  background-position: top left;
  opacity: 0.04;
  pointer-events: none;
}

/* Inert in the reference — styled, but no .more-intro-image element exists. */
.more-intro-image {
  position: absolute;
  left: 45.83vw;   /* 660/1440 — starts at the top of col 6 */
  top: 50%;
  transform: translateY(-50%);
  width: 45.83vw;
  height: clamp(220px, 30.49vw, 460px);
  border-radius: var(--radius);
  overflow: hidden;
}

.more-intro-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.5;
  display: block;
}

/* Gradient: dark at the left, transparent at the right */
.more-intro-image::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to right,
    var(--color-dark) 23%,
    transparent 85%
  );
  border-radius: var(--radius);
}

/* Intro text: 7 columns (1047px ref @ 1440px = 72.71vw) */
.more-intro-text {
  position: relative;
  z-index: 1;
  width: 72.71vw; /* 1047/1440 — col 1 through the end of col 7 */
}

/* ── Content blocks ── */
.more-block {
  margin-top: var(--mo-gap);
}

/* First block after the intro: padding-top instead of margin-top */
.more-intro + .more-block {
  margin-top: 0;
  padding-top: var(--mo-gap);
}

/* Last block: generous padding-bottom */
.more-block:last-of-type {
  padding-bottom: var(--mo-gap);
}

/* Inert in the reference — no matching element. */
.more-block--final {
  margin-left: 25vw;
  width: 50vw;
}

.more-title {
  font-family: var(--font-display);
  font-size: var(--text-headline);
  font-weight: 400;
  line-height: 1.125;
  letter-spacing: -0.02em;
  /* Was --color-cream, for the dark ground. Full --color-dark rather than a
     softened mix: this is the overlay's opening statement and the largest
     type in it, so it takes the same full-strength ink the landing page's own
     headlines take. 15.1:1 on cream. */
  color: var(--color-dark);
  text-align: left;
}

.more-block--final .more-title {
  font-size: var(--text-headline-accent);
  line-height: 1.333;
  letter-spacing: -0.02em;
  text-align: center;
  text-indent: 0;
}

.more-title-serif {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.02em;
}


/* ── Bio text (col 1 or col 3, always 4 columns wide) ── */
.more-block--col1 {
  margin-left: var(--mo-col1);
  width: var(--mo-col4);
}

/* Inert in the reference — no matching element. */
.more-block--col3 {
  margin-left: var(--mo-col3);
  width: var(--mo-col4);
}

.more-bio {
  font-family: var(--font-ui);
  /* --text-ui, not --text-lead, and 1.5, not 1.8: this is now the same
     body-copy treatment as every other run of prose on the site. Measured at
     1440 before the change, this bio was 18px/1.8 while .pro-summary and every
     case-study paragraph were 16px/1.5 — 12% larger and much airier, which is
     what made it read as a different voice from the rest of the page. Changed
     on instruction ("match the site"). --text-lead is now unused by this rule
     but is still the token to come back to if the overlay ever wants its own
     larger reading size again. */
  font-size: var(--text-ui);
  font-weight: 400;
  line-height: 1.5;
  /* Was --color-cream. Full --color-dark, not the 65% mix the project cards'
     summaries use: those are small supporting lines inside a card that has a
     louder title above them, while this is the overlay's only body copy and
     has nothing to rank below. Mirrors the dark version, which likewise ran
     the bio at full cream rather than a dimmed one. 15.1:1. */
  color: var(--color-dark);
  font-feature-settings: 'liga' 0;
}

/* Was: font-family: var(--font-display), sized and spaced to suit that face
   (--text-caption, 1.636 line-height, -0.02em tracking). Removed — body font
   requested for this copy. Falls back to the base .more-bio rule above:
   var(--font-ui), --text-lead, line-height 1.8 — the plain body-copy
   treatment used everywhere else .more-bio appears without its own override. */

/* No size override here any more. There used to be one — a dial-kit result,
   clamp(0.9375rem, 1.25vw, 1.875rem), landing on 20px at 1600 and 18px at
   1440 — which was tuned against the older three-paragraph copy. It was a
   magic value: that clamp appeared nowhere in the :root scale, so the one
   passage of long-form prose on the site was the only text not sized from a
   token. Removed on instruction; this block now inherits --text-ui / 1.5 from
   .more-bio above, like the rest of the site's body copy. If a future brief
   wants this bio larger again, add a named token to :root for it rather than
   restoring a literal clamp here. */

/* Gap between consecutive paragraphs in the same block */
.more-bio + .more-bio {
  margin-top: 1.5rem;
}

/* ── Photo rails (full-width with margins) ── */
.more-block--rail {
  margin-left: var(--mo-margin);
  margin-right: var(--mo-margin);
  width: calc(100% - 2 * var(--mo-margin));
}

.more-rail {
  display: flex;
  align-items: flex-start;
}

/* ── Each photo + caption ── */
.more-photo-item {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.more-photo-item img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
}

.more-caption {
  font-family: var(--font-ui);
  font-size: var(--text-label);
  line-height: 1.5;
  /* Inverted with the rest of the overlay even though .more-rail is no longer
     in index.html, so a revived rail is not invisible on cream. A mix rather
     than full ink: a caption hanging under a photograph is subordinate to it.

     65%, NOT the 55% the dark version used. The two are not mirror images:
     cream at 55% over --color-dark measures 5.47:1, but dark at 55% over
     cream is only 3.67:1 — below AA, and this is --text-label, small type.
     65% measures 4.99:1, the first step on the 5% scale that clears the
     floor, and it is the same 65% .pro-summary and .pro-eyebrow already use
     for subordinate copy on a light ground. */
  color: color-mix(in srgb, var(--color-dark) 65%, var(--color-cream));
  text-align: right;
  opacity: 0;
  transition: opacity 0.5s ease;
}

.more-photo-item:hover .more-caption {
  opacity: 1;
}

/* ── Rail 1: loose layout with individual vertical offsets ── */
.rail-1 { gap: 0; }
.rail-1 .more-photo-item:nth-child(1) { width: 19.72vw; margin-top: 7.22vw; }
.rail-1 .more-photo-item:nth-child(2) { width: 12.5vw;  margin-top: 0;      margin-left: 6.67vw; }
.rail-1 .more-photo-item:nth-child(3) { width: 26.39vw; margin-top: 4.1vw;  margin-left: 2.36vw; }
.rail-1 .more-photo-item:nth-child(4) { width: 9.79vw;  margin-top: 2.01vw; margin-left: 6.67vw; }

/* ── Rail 2: loose layout with individual vertical offsets ── */
.rail-2 { gap: 0; }
.rail-2 .more-photo-item:nth-child(1) { width: 16.67vw; margin-top: 0; }
.rail-2 .more-photo-item:nth-child(2) { width: 9.17vw;  margin-left: 9.51vw; margin-top: 10.49vw; }
.rail-2 .more-photo-item:nth-child(3) { width: 12.92vw; margin-left: 2.78vw; margin-top: 2.85vw;  }
.rail-2 .more-photo-item:nth-child(4) { width: 10.49vw; margin-left: 2.78vw; margin-top: 1.94vw;  }
.rail-2 .more-photo-item:nth-child(5) { width: 12.5vw;  margin-left: 6.53vw; margin-top: 4.31vw;  }

/* ── Profile section: big photo left + text right ──
   Revived for the About Me overlay's fold 2 (Don Norman + the business-side
   copy), which now sits inside a .more-fold. That fold already insets both
   sides by var(--mo-margin) (8.33vw) — this block used to be an unmargined
   direct child of the overlay, so its own 18.82vw left offset (desktop) and
   6.4vw offset + calc'd width (mobile, see the block below) were doing that
   inset themselves. Nested inside .more-fold now, both would double up and
   shove the pair off-centre; removed here in favour of centring the pair
   within whatever width the fold leaves. */
/* Three columns — paragraph, photograph, paragraph — on instruction on
   2026-09-29: "centre align the image first, then the first paragraph to the
   left of the image towards the top half, the second to the right towards the
   bottom half… the layout should almost feel like a waterfall where the user's
   eyes are well guided." The guided path is a diagonal: top-left text, down
   through the photograph, bottom-right text.

   1fr auto 1fr is what centres the PHOTOGRAPH, which is the thing the
   instruction asks to be centred — not the unit as a whole, which is what the
   old flex + justify-content: center did. Equal side tracks put the middle
   track on the fold's centreline, and .more-fold gives the fold equal left and
   right margins, so the fold's centreline IS the screen's. The photograph
   stays centred no matter how unequal the two paragraphs are, which a flex
   row could not promise.

   This supersedes two earlier instructions, both kept here because they are
   being given up rather than having gone stale: the pair used to sit at the
   fold's left edge so its margin matched the statement fold's, and then it was
   centred as a UNIT ("the whole unit of image + text should be centre
   aligned"). Neither alignment survives — the photograph alone is now the
   centred thing. */
.more-block--profile {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  /* Half the dialled 6.875vw (110px @1600) that used to separate the image
     from the single text column. Halved rather than reused whole because
     there are two gutters now where there was one: the total distance between
     the photograph and the prose is unchanged, it is just spent on both sides
     instead of one. */
  column-gap: calc(6.875vw / 2);
}

.more-profile-img {
  grid-column: 2;
  /* align-self, so the photograph sets the row height and the two paragraphs
     hang off it rather than stretching to match each other. */
  align-self: start;
  width: 22.25vw;   /* dial-kit final: 356px @1600 */
  overflow: hidden;
  border-radius: var(--radius);
}

.more-profile-img img {
  display: block;
  width: 100%;
  aspect-ratio: 437 / 611;
  object-fit: cover;
}

/* The two paragraphs are grid items in their own right — the
   .more-profile-text wrapper that used to hold both is gone from index.html.
   That also retires .more-bio + .more-bio for this block: the paragraphs are
   no longer adjacent siblings, so the 1.5rem stack gap simply does not apply
   and did not need cancelling.

   align-self is what makes the diagonal: the lead sits on the photograph's top
   edge (its top half), the tail on its bottom edge (its bottom half).
   justify-self pulls each toward the photograph rather than out to the
   viewport edges — at widths past ~1600 the 1fr tracks grow faster than the
   prose measure does, and outward alignment would leave the photograph
   stranded alone in the middle instead of reading as one cascade.

   The max-width is the old .more-profile-text width, unchanged: this is the
   same dialled measure, now applied to one paragraph at a time. It only binds
   above ~1600 — below that the 1fr track is the narrower of the two.

   width: calc(100% - 40px) takes 40px off each paragraph on instruction on
   2026-09-29 ("decrease the width of both the left and right text boxes by
   40px while keeping the padding between the text boxes and the image
   constant… this will make the overall layout a bit tighter"). 100% here is
   the 1fr track, so the reduction tracks the viewport rather than being a
   fixed share of it.

   The constant gutter is why this is a width and not a change to column-gap:
   the 40px comes off the OUTER edge of each paragraph, because justify-self
   pins each one to the edge facing the photograph. The lead's right edge and
   the tail's left edge do not move, so the image-to-prose distance is
   untouched and the whole unit closes in from the outside. */
.more-bio--lead,
.more-bio--tail {
  width: calc(100% - 40px);
  max-width: 29.75vw;   /* dial-kit final: 476px @1600 */
}

.more-bio--lead {
  grid-column: 1;
  align-self: start;
  justify-self: end;
}

.more-bio--tail {
  grid-column: 3;
  align-self: end;
  justify-self: start;
}


/* ── Career section ── */
/* Inert: no .more-career element exists in index.html. Recoloured for the
   light overlay anyway (cream ink -> --color-dark, white hairlines -> dark
   mixes) so it is not invisible if it is ever revived. The separators drop
   from 7% to 8% rather than mirroring exactly: a light hairline on a dark
   ground reads at a lower alpha than a dark one on cream does. */
/* ── Career: desktop 2-col grid ──
 *   left=588/1200=49%, gap=132/1200=11%, right=480/1200=40%
 *   col1=60% (content 49% + visual gap 11%), col2=40%
 *   column-gap: 0 + padding-right: 11% on col1 → border spans the full width
 * ─────────────────────────────────────────────────────────────────────── */
.more-career {
  display: grid;
  grid-template-columns: 60% 40%;
  column-gap: 0;
  row-gap: 0;
  align-items: start;
  width: 100%;
}

.more-career-header {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  align-items: center;
  padding-right: 11%;
  padding-bottom: 14px;
  border-bottom: 1px solid color-mix(in srgb, var(--color-dark) 12%, transparent);
}

.more-career-label {
  font-family: var(--font-ui);
  font-size: var(--text-label);
  font-weight: 400;
  color: var(--color-dark);
  font-feature-settings: 'liga' 0;
}

/* Where / Main role column heads — with an underline */
.more-career-cols-header {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  padding-bottom: 14px;
  border-bottom: 1px solid color-mix(in srgb, var(--color-dark) 12%, transparent);
  margin-top: 0;
}

.more-career-col-head {
  font-family: var(--font-ui);
  font-size: var(--text-label);
  font-weight: 400;
  color: var(--color-dark);
  font-feature-settings: 'liga' 0;
  width: 50%;
}

.more-career-bio {
  grid-column: 1;
  grid-row: 2;
  font-family: var(--font-display);
  font-size: var(--text-body);
  font-weight: 400;
  line-height: 1.5; /* 36/24 */
  letter-spacing: -0.02em;
  color: var(--color-dark);
  font-feature-settings: 'liga' 0;
  padding-right: 11%;
  padding-top: clamp(1.5rem, 2.5vw, 2.5rem);
  padding-bottom: clamp(1.5rem, 2.5vw, 2.5rem);
}

.more-career-list {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  padding-top: clamp(1.5rem, 2.5vw, 2.5rem);
}

.more-career-row {
  display: flex;
  align-items: center;
  border-top: 1px solid color-mix(in srgb, var(--color-dark) 8%, transparent);
  padding: 12px 0;
}

.more-career-row:last-child {
  border-bottom: 1px solid color-mix(in srgb, var(--color-dark) 8%, transparent);
}

.more-career-company,
.more-career-role {
  width: 50%;
  font-family: var(--font-ui);
  font-size: var(--text-meta);
  font-weight: 400;
  line-height: 1.714; /* 24/14 */
  color: var(--color-dark);
  font-feature-settings: 'liga' 0;
}

.more-career-row:first-child {
  border-top: none;
}


/* ─── Copy toast ───────────────────────────────
   Same visual as the desktop cursor-label. Appears centred at the
   bottom of the screen on every device when the email is copied. */
.copy-toast {
  position: fixed;
  bottom: calc(32px + env(safe-area-inset-bottom, 0px));
  left: 50%;
  transform: translateX(-50%) translateY(12px);
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 1.1rem;
  border-radius: var(--radius);
  background: var(--color-dark);
  color: var(--color-cream);
  font-family: var(--font-ui);
  font-size: var(--text-meta);
  font-weight: 400;
  letter-spacing: 0;
  white-space: nowrap;
  pointer-events: none;
  z-index: 9999;
  opacity: 0;
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.copy-toast.visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* ─── About: CTA button ─────────────────────────
   Always visible. On desktop it fades out while the section is
   hovered, because the cursor-label takes over.   */
/* Everything this used to declare about how a button LOOKS now comes from
   .btn (see the component near .section-title) — the shape, ground, type and
   padding all moved there when the project cards were pulled onto the same
   button. What is left is the two things that are this button's alone: where
   it sits in #about's flex column, and the fade that hands the section over
   to the custom cursor on desktop. */
.about-cta {
  align-self: flex-start;
  opacity: 1;
  transition: opacity 0.35s ease;
}

.about-cta:active {
  opacity: 0.7;
}

/* Desktop: the cursor already gives copy feedback — toast unnecessary */
@media (hover: hover) {
  .copy-toast { display: none; }
}

/* Desktop: button disappears when the cursor-label takes over */
@media (hover: hover) {
  #about:hover .about-cta {
    opacity: 0;
    pointer-events: none;
  }
}

/* ─── Contact: table layout ─────────────────── */

.contact-table {
  width: 100%;
}

.contact-row {
  display: flex;
  align-items: center;
  border-top: 1px solid rgba(255, 255, 255, 0.10);
  padding-top: 24px;
  padding-bottom: 24px;
  cursor: none;
}

/* No border-bottom — like the cases, only a top stroke per row */

/* Inline icon — hidden on desktop, visible on mobile */
.contact-row-icon {
  display: none;
  flex-shrink: 0;
  color: var(--color-cream);
  opacity: 0.55;
}

.contact-row-text {
  font-family: var(--font-display);
  font-size: var(--text-display);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--color-cream);
  display: inline-block;
  transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.contact-row-text strong {
  font-weight: 700;
}

.contact-row:hover .contact-row-text {
  transform: translateX(1.5vw);
}

/* ─── Site footer ───────────────────────────────────────────────────────────
   Ends every case study and the About me overlay; never the landing page,
   which has #contact instead. Markup and links live in site-footer.js.

   A sign-off: a small line, the address large (the hero), a copy button,
   four icon links and a signature. Type is this site's own: Geist for the
   small lines, Clash Display for the address.

   A slab rather than a band: it stops 16px short of the viewport on three
   sides so it reads as an object sitting on the page, and its content is
   padded back out so the text still lines up with the host page's own left
   edge (--sf-margin).

   Every colour below comes from the --sf-* variables, so the ground is one
   block. White and green grounds were built and compared; charcoal won. */
/* Two boxes. The footer is a frame that clips; the slab inside it is the
   visible surface. The clip is what lets the slab ride up from the bottom on
   entrance without its start position adding to the page's scroll height
   (a transformed box still counts as scrollable overflow), and it is
   overflow: clip rather than hidden so the frame never becomes a scroll
   container. */
.site-footer {
  --sf-margin: var(--page-margin);
  --sf-gutter: 16px;
  padding: 0 var(--sf-gutter) var(--sf-gutter);
  overflow: hidden;
  overflow: clip;
}

.sf-slab {
  position: relative;
  overflow: hidden;
  padding: 96px max(24px, calc(var(--sf-margin) - var(--sf-gutter))) 40px;
  border-radius: var(--radius);
  background-color: var(--sf-bg);
  color: var(--sf-ink);
}

/* Charcoal, the case studies' own dark band (--color-charcoal), on
   instruction: not black. Muted line measures 6.2:1. */
.site-footer {
  --sf-bg:    var(--color-charcoal);
  --sf-ink:   var(--color-cream);
  --sf-muted: color-mix(in srgb, var(--color-cream) 65%, var(--color-charcoal));
  --sf-faint: color-mix(in srgb, var(--color-cream) 12%, var(--color-charcoal));
  --sf-ok:    var(--color-green);
}

/* On a case study, .cs ends on its own --cs-sec-gap of padding after the
   close section's equal one. The footer takes back the outer half, so the
   close section stays centred in its band. */
.cs-page .site-footer {
  --sf-margin: var(--cs-margin);
  margin-top: calc(-1 * var(--cs-sec-gap));
}
.more-overlay .site-footer { --sf-margin: var(--mo-margin); }

/* A sign-off, centred: one line as the hero, four icon tiles, and a
   signature on a hairline at the foot. Plain charcoal ground. */
.sf-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  container-type: inline-size;
}

/* The hero line, in the treatment the address had: Clash Display 400 at
   --text-case (64px at 1440). One line on desktop: the cap shrinks it only
   if the slab is too narrow to hold it whole. The steak stands in for
   "meaty", sitting in the line like a word (after the reference's rock). */
.sf-head {
  margin: 0;
  font-family: var(--font-display);
  font-size: min(var(--text-case), 100cqi / 15.6);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--sf-ink);
  white-space: nowrap;
}
.sf-steak {
  display: inline-block;
  height: 1.12em;
  width: auto;
  margin: 0 0.06em;
  vertical-align: -0.3em;
  transform: rotate(-6deg);
  filter: drop-shadow(0 0.08em 0.12em rgba(0, 0, 0, .45));
  transition: transform .6s cubic-bezier(0.34, 1.56, 0.64, 1);
  -webkit-user-drag: none;
}
/* The steak carries the same small label as the tiles (.sf-tip, below). */
.sf-steak-wrap { position: relative; display: inline-block; }
/* Hovering the line, the steak turns level with a small spring. */
.sf-head:hover .sf-steak { transform: rotate(4deg) scale(1.06); }

/* The tiles (after the second reference): a cream square, a dark disc on
   it, and the disc sitting a little off an accent disc behind it. */
.sf-icons {
  display: flex;
  justify-content: center;
  gap: 16px;
  margin-top: 48px;
}
.sf-icon {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 64px; height: 64px;
  padding: 0;
  border: none;
  border-radius: 16px;
  background: var(--color-cream);
  color: var(--color-cream);
  cursor: pointer;
  box-shadow: 0 1px 0 rgba(255, 255, 255, .5) inset, 0 8px 20px -8px rgba(0, 0, 0, .6);
  transition: transform .3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow .3s ease;
}
.sf-disc {
  position: relative;
  display: grid;
  place-items: center;
  width: 40px; height: 40px;
  border-radius: 999px;
  background: var(--color-dark);
  box-shadow: 2px 2px 0 var(--color-spacefire);
  transition: box-shadow .3s ease;
}
.sf-glyph { grid-area: 1 / 1; display: inline-flex; transition: opacity .2s ease, transform .2s ease; }
.sf-glyph--done { color: var(--color-green); opacity: 0; transform: scale(.6); }
.sf-icon.is-copied .sf-glyph:not(.sf-glyph--done) { opacity: 0; transform: scale(.6); }
.sf-icon.is-copied .sf-glyph--done { opacity: 1; transform: none; }

.sf-icon:hover,
.sf-icon:focus-visible {
  transform: translateY(-4px);
  box-shadow: 0 1px 0 rgba(255, 255, 255, .5) inset, 0 16px 28px -10px rgba(0, 0, 0, .7);
}
.sf-icon:hover .sf-disc,
.sf-icon:focus-visible .sf-disc { box-shadow: 3px 3px 0 var(--color-spacefire); }
.sf-icon:active { transform: translateY(-1px) scale(.97); }

/* The label: above the tile on hover, so the icons stay wordless at rest. */
.sf-tip {
  position: absolute;
  bottom: calc(100% + 12px);
  left: 50%;
  translate: -50% 4px;
  padding: 4px 8px;
  border-radius: 6px;
  background: color-mix(in srgb, var(--color-cream) 12%, var(--color-charcoal));
  color: var(--sf-ink);
  font-family: var(--font-ui);
  font-size: var(--text-label);
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: normal;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease, translate .2s ease;
}
.sf-icon:hover .sf-tip,
.sf-icon:focus-visible .sf-tip,
.sf-steak-wrap:hover .sf-tip,
.sf-icon.is-copied .sf-tip { opacity: 1; translate: -50% 0; }
.sf-icon.is-copied .sf-tip { color: var(--color-green); }

/* The signature: a small line on a hairline, closing the page. */
.sf-sign {
  align-self: stretch;
  margin: 72px 0 0;
  padding-top: 24px;
  border-top: 1px solid var(--sf-faint);
  font-family: var(--font-ui);
  font-size: var(--text-meta);
  line-height: 1.5;
  color: var(--sf-muted);
}

.sf-icon:focus-visible {
  outline: 2px solid var(--sf-ink);
  outline-offset: 4px;
}

.sf-live {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Entrance ──────────────────────────────────────────────────────────────
   Plays once, when a quarter of the slab is on screen:
     0ms     the slab rides up from below the frame on a spring and
             settles with a slight overshoot
     ~420ms  the line slides up from behind its own baseline (a mask, not
             a fade)
     ~620ms  the tiles one by one, then the signature
   Every delay is a --sf-d set per element in site-footer.js.

   Nothing here hides anything on its own: these states only apply under
   .sf-armed, which site-footer.js adds after checking that reduced motion
   is off, and removes again once the entrance has finished. Without the
   script, or with reduced motion, the footer renders finished. */
.site-footer { --sf-ease: cubic-bezier(0.22, 1, 0.36, 1); }

/* The mask: a line box that clips its own content. The padding and equal
   negative margin give descenders (g, j, @) room without moving the line. */
.sf-line {
  display: inline-block;
  vertical-align: top;
  padding-bottom: 0.16em;
  margin-bottom: -0.16em;
}
.sf-line > span { display: inline-block; }

/* The slab starts wholly below the frame's clip edge and rides up on a
   spring with a small overshoot. --sf-rise-ease is a CSS linear() curve
   sampled from that spring by site-footer.js (with its own duration); the
   cubic-bezier is the fallback for a browser without linear(). */
.sf-armed .sf-slab { transform: translateY(calc(100% + var(--sf-gutter))); }
.sf-armed .sf-line { overflow: hidden; }   /* the mask, only while it is needed: at rest the steak's label must reach above the line */
.sf-armed .sf-line > span { transform: translateY(110%); }
.sf-armed .sf-icon,
.sf-armed .sf-sign {
  opacity: 0;
  transform: translateY(12px);
}

.sf-armed.sf-in .sf-slab {
  transform: none;
  transition: transform var(--sf-rise-dur, 1.1s)
              var(--sf-rise-ease, cubic-bezier(0.34, 1.3, 0.64, 1));
}
.sf-armed.sf-in .sf-line > span {
  transform: none;
  transition: transform 1s var(--sf-ease) var(--sf-d, 0ms);
}
.sf-armed.sf-in .sf-icon,
.sf-armed.sf-in .sf-sign {
  opacity: 1;
  transform: none;
  transition: opacity 0.6s ease var(--sf-d, 0ms),
              transform 0.9s var(--sf-ease) var(--sf-d, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  .sf-steak, .sf-icon, .sf-disc, .sf-glyph, .sf-tip { transition: none; }
  .sf-head:hover .sf-steak { transform: rotate(-6deg); }
  .sf-icon:hover, .sf-icon:focus-visible { transform: none; }
}

@media (max-width: 1024px) {
  .sf-slab { padding-top: 80px; }
}

/* Tablets and phones: the line is free to wrap between words, at a size
   that stays the largest thing in the footer. */
@media (max-width: 900px) {
  .sf-head { white-space: normal; font-size: var(--text-case); text-wrap: balance; }
}
@media (max-width: 640px) {
  .site-footer { --sf-gutter: 8px; }
  .sf-slab  { padding: 56px 24px 32px; }
  .sf-head  { font-size: 40px; }
  .sf-icons { gap: 12px; margin-top: 32px; }
  .sf-sign  { margin-top: 56px; padding-top: 20px; }
}

/* ─── Peek ──────────────────────────────────────────────────────────────────
   The footer's Resume icon opens here instead of leaving the site: a window
   floats over the page on a tinted, blurred scrim, after Arc's Peek (the
   supplied recording). Round buttons sit just outside its top-right corner:
   close, and open in a new tab.

   Only the resume opens here. LinkedIn, X and Medium send
   X-Frame-Options: SAMEORIGIN, which stops any other site framing them
   (checked 2026-10-03), so those pills open a new tab.

   A <dialog> opened with showModal(), for the focus trap, Esc and an inert
   page behind. Its own ::backdrop is unused: the scrim is a real element so
   it can be clicked and animated alongside the window. Motion lives in
   site-footer.js. */
.peek {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: none;
  background: transparent;
  overflow: hidden;
}
.peek[open] { display: grid; place-items: center; }
.peek::backdrop { background: transparent; }

.peek-scrim {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--color-charcoal) 56%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/* The window and its buttons move as one, so they share a frame. */
.peek-frame {
  position: relative;
  transform-origin: 50% 50%;
}

.peek-window {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--color-white);
  color: var(--color-dark);
  box-shadow:
    0 2px 4px color-mix(in srgb, var(--color-dark) 10%, transparent),
    0 32px 80px -16px color-mix(in srgb, var(--color-dark) 48%, transparent);
}

/* The resume: nearly the whole viewport, as Arc's window is. 60px each side
   leaves room for the button column. */
.peek--doc .peek-window {
  width: min(1024px, 100vw - 160px);
  height: calc(100dvh - 48px);
}
.peek-doc {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Arc's round buttons, outside the window's top-right corner. 999px is the
   full-pill exception again, on instruction (the recording's buttons are
   circles); 44px for the tap target. */
.peek-actions {
  position: absolute;
  top: 0;
  left: calc(100% + 12px);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.peek-btn {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: var(--color-charcoal);
  color: var(--color-cream);
  box-shadow: 0 8px 24px -8px color-mix(in srgb, var(--color-dark) 48%, transparent);
  cursor: pointer;
  transition: background-color 0.2s ease, transform 0.2s ease;
}
.peek-btn:hover { background: color-mix(in srgb, var(--color-cream) 14%, var(--color-charcoal)); }
.peek-btn:active { transform: scale(0.92); }
.peek-btn:focus-visible {
  outline: 2px solid var(--color-cream);
  outline-offset: 2px;
}

/* Phones: no room beside the window, so the buttons move above it and the
   window takes the width. */
@media (max-width: 640px) {
  .peek--doc .peek-window { width: calc(100vw - 16px); height: calc(100dvh - 76px); }
  .peek-frame { margin-top: 52px; }
  .peek-actions {
    top: auto;
    bottom: calc(100% + 8px);
    left: auto;
    right: 0;
    flex-direction: row;
  }
}

/* ─── Elevator button ───────────────────────
   Only visible on the contact section (JS adds .visible).
   Positioned relative to the right and bottom edges. */
.elevator-btn {
  position: fixed;
  bottom: 5.56vw;   /* 80px @ 1440px — same as the overlay's × */
  right: 5.56vw;
  background: var(--color-green);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: var(--radius-round);
  padding: 16px;
  cursor: pointer;
  color: var(--color-cream);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  opacity: 0;
  pointer-events: none;
  transform: translateY(12px);
  transition: opacity 0.35s ease, transform 0.35s ease;
}

.elevator-btn.visible {
  opacity: 1;
  pointer-events: auto;
  transform: translateY(0);
}

.elevator-btn:hover { opacity: 0.8; }


/* ─── Case overlay ──────────────────────────────────────────────────────────
   Light ground (#f5f4f0), reuses the "more about me" overlay pattern.
   Each case has its own id (case-wave-overlay, case-mosaico-overlay…),
   all share the .case-overlay class and the content classes.             */

.case-overlay {
  --case-margin:  8.33vw;   /* 120/1440 — side margin */
  --case-full:   83.33vw;   /* 1200/1440 — full image width */
  --case-text:   52.78vw;   /* 760/1440 — text block width */
  --case-half:   40.83vw;   /* 588/1440 — half width */

  position: fixed;
  inset: 0;
  background: var(--color-cream);
  color: var(--color-dark);
  z-index: 200;
  overflow-y: auto;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.5s ease;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.case-overlay.visible {
  opacity: 1;
  pointer-events: all;
}

/* Close button — light ground, subtle border */
.case-close {
  position: fixed;
  top: 5vw;              /* ~72px @ 1440 */
  right: var(--case-margin);
  background: var(--color-cream);
  border: 1px solid #e7e7e7;
  border-radius: var(--radius-round);
  padding: 16px;
  cursor: pointer;
  color: var(--color-dark);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 201;
  transition: opacity 0.25s ease;
}

.case-overlay.visible .case-close {
  display: flex;
}

.case-close:hover { opacity: 0.6; }

/* ── Hero ── */
.case-hero {
  padding: var(--case-margin) var(--case-margin) 0;
}

.case-hero img {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 1;
  object-fit: cover;
  border-radius: var(--radius);
}

/* ── Main: title + text + metrics ── */
.case-main {
  padding: var(--case-margin) var(--case-margin) var(--case-margin);
}

.case-text-block {
  width: var(--case-half); /* 588px @ 1440 — half the usable area */
  display: flex;
  flex-direction: column;
  gap: 40px;
}

.case-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-case);
  line-height: 1.125;                      /* 72/64 */
  letter-spacing: -0.02em;
  color: var(--color-dark);
  margin: 0;
}

.case-body {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.case-body p {
  font-family: var(--font-ui);
  font-size: var(--text-ui);
  line-height: 1.5;     /* 24/16 */
  color: var(--color-dark);
  margin: 0;
  font-feature-settings: 'liga' 0;
}

/* Metrics — 760px (3×200 + 2×80), border only above the content */
.case-metrics {
  width: var(--case-text); /* 760px @ 1440 — exact for the 3 cards */
  border-top: 1px solid var(--color-dark);
  padding: 16px 0 0;
  display: flex;
  gap: 80px;
  margin-top: var(--case-margin); /* 120px @ 1440 */
}

.case-metric {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 200px;
}

.case-metric-number {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-case);
  line-height: 1.125;
  letter-spacing: -0.02em;
  color: var(--color-dark);
}

.case-metric-label {
  font-family: var(--font-ui);
  font-size: var(--text-ui);
  line-height: 1.5;
  color: var(--color-dark);
}

/* ── Video ── */
.case-video-section {
  padding: var(--case-margin) var(--case-margin) 0;
}

.case-video-wrapper {
  position: relative;
  width: 50%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  overflow: hidden;
}

/* Inert in the reference — no matching element. */
.case-video-wrapper--placeholder {
  background: #e2e2e2;
}

.case-video-wrapper iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* ── Gallery ── */
.case-gallery-section {
  padding: var(--case-margin) var(--case-margin) 0;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.case-img-full,
.case-img-half {
  overflow: hidden;
  border-radius: var(--radius);
}

.case-img-full img {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 1;
  object-fit: cover;
}

.case-img-row {
  display: flex;
  gap: 24px;
}

.case-img-half {
  flex: 1;
}

.case-img-half img {
  display: block;
  width: 100%;
  aspect-ratio: 588 / 600;
  object-fit: cover;
}

/* Phones composition — contain so the screens are not cropped */
.case-img-half--phones {
  background: #e2e2e2;
}

.case-img-half--phones img {
  object-fit: contain;
}

/* ── Team ── */
.case-team-section {
  padding: var(--case-margin) var(--case-margin);
}

.case-team-inner {
  width: var(--case-half); /* 588px @ 1440 */
  display: flex;
  flex-direction: column;
  gap: 40px;
}

.case-team-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-case);
  line-height: 1.125;
  letter-spacing: -0.02em;
  color: var(--color-dark);
  margin: 0;
}

.case-team-groups {
  display: flex;
  flex-direction: column;
  gap: 33px;
}

.case-team-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.case-team-label {
  font-family: var(--font-ui);
  font-size: var(--text-ui);
  line-height: 1.5;
  color: #a3a3a3;
}

.case-team-names {
  font-family: var(--font-ui);
  font-size: var(--text-ui);
  line-height: 1.5;
  color: var(--color-dark);
}

/* Clickable work rows — pointer cursor. data-case opens an overlay in place,
   data-href leaves for a long-form case study page. */


/* ── Touch / no-hover: remove all mouse-only interactions ──────────────────
   @media (hover: none) catches touch screens and styluses without hover.
   Everything that depends on a mouse (custom cursors, visual hover states)
   is neutralised here. JS still runs but .cursor-label stays invisible. */
@media (hover: none) {

  /* Restore the default cursor (#about had cursor: none) */
  #about { cursor: auto; }

  /* Hide the custom cursor entirely */
  .cursor-label { display: none !important; }


  /* Contact: restore the cursor, inline icons always visible,
     text and icon at opposite ends of the row */
  .contact-row {
    cursor: auto;
    justify-content: space-between;
  }
  .contact-row:hover .contact-row-text { transform: none; }
  .contact-row-icon { display: block; }

  /* Buttons shrink on mobile. This used to be a .about-cta rule, which is
     exactly the drift the .btn component exists to stop — once the project
     cards' CTAs joined the component, the About button was the only one still
     shrinking here and the three stopped matching (measured 44px against 52
     at 375px wide). Moved onto .btn so one edit still moves every button.

     The font-size it used to carry (a hardcoded 1rem) is gone rather than
     moved: --text-ui is a clamp that already resolves to 14px at this width,
     so the override was pushing the type back UP to 16px, and CLAUDE.md
     forbids an inline font-size regardless. */
  .btn {
    padding: 12px 20px;
  }
  /* Re-asserted, because this .btn rule sits later in the file than
     .btn--icon and would otherwise win on source order and pull the icon
     button in to 60px wide. Its 44px height is the tap-target floor and its
     width is proportioned off the reference; neither shrinks on mobile.
     Only the buttons with text in them do. */
  .btn--icon {
    padding: 12px 24px;
  }

  /* Overlay captions: always visible (no hover to reveal them) */
  .more-caption { opacity: 1; }
}

/* ── Mobile (< 640px) ──────────────────────────────────────────────────────
   Single-column layout. Work rows stacked. Overlay intro with the text
   at full width. */
@media (max-width: 640px) {

  /* Base sections */
  .section {
    height: auto;
    padding: 6vw;
    min-height: 100svh;
  }

  #work.section,
  #contact.section {
    padding-top: max(5rem, 14vw);
  }


  /* Stacked: the card sits at the top of the column, with nothing beside it
     to align to. At 280 x 408 it is fully above the fold on a 390px phone. */

  /* Dots: pushed below the hero content on mobile */
  .dots-layer {
    top: var(--dots-top, max(calc(90svh - 8px), 680px));
  }

  /* About: mobile typography */
  #about .section-inner {
    gap: 32px;
    /* 6vw each side — matches .section's mobile padding exactly, so About's
       centred text box lands flush with every other section's grid edge. */
    width: calc(100% - 12vw);
  }

  .about-text {
    font-size: 2rem;     /* 32px */
    line-height: 1.3125; /* 42/32 */
    max-width: 100%;
  }

  /* Turns off the three desktop line-break pins from 641px up (see the
     markup comment above .about-text in index.html). At this width each of
     the four fixed-length segments they create sub-wraps independently, and
     two of them stranded a single word alone on its own row when tested live
     — display: none here hands the whole paragraph back to plain, un-pinned
     wrap, exactly as it read before those breaks existed. */
  .about-text .about-break {
    display: none;
  }

  /* Section title */
  .section-title {
    max-width: 100%;
    hyphens: auto;
    margin-top: clamp(2rem, 10vw, 4rem);
    margin-bottom: 8px;
  }

  /* ── Work section ── */
  #work.section { padding-top: 120px; }












  /* Arrow: completely hidden on mobile */

  #work.section { padding-bottom: max(3rem, 8vw); }

  /* ── Nav: two rows ──
     The name plus three destinations still overrun a 375px line at a legible
     size, so the bar wraps: name on top, the three links spread beneath.
     Everything stays reachable — nothing hides behind a menu button. */
  .nav {
    flex-wrap: wrap;
    gap: 0;
    padding: 8px 6vw;
  }
  .nav-links { width: 100%; justify-content: space-between; gap: 0; }

  .nav-logo,
  .nav-link { padding: 8px 12px; }
  .nav-logo { margin-left: -12px; }
  .nav-links .nav-link:last-child { margin-right: -12px; }

  /* ── Hero: allowed to grow beyond 100svh if needed ── */
  .section[data-bg="hero"] {
    height: auto;
    min-height: 100svh;
    /* 80px from the bottom edge + the iPhone home-bar safe area */
    padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
  }

  /* ── Work: products column hidden ── */

  /* ── Contact: smaller text on mobile ── */
  .contact-row-text { font-size: 1.5rem; } /* 24px — mobile only */

  /* ── Overlay: close button nearer the corner ── */
  .more-close {
    top: 20px;
    padding: 12px;
  }

  /* ── Overlay intro: 100svh, text anchored to the base ── */
  .more-intro {
    min-height: 100svh;
    height: auto;
    padding: 0 6.4vw clamp(3rem, 8vw, 5rem);
    justify-content: flex-end;
    overflow: visible; /* avoids clipping on smaller screens */
  }

  .more-intro-image { display: none; }

  .more-intro-text { width: 100%; }

  /* Intro title smaller on mobile: 32px / 36px */
  .more-title {
    font-size: 2rem; /* 32px fixed — does not scale with vw on mobile */
    line-height: 1.125;
  }

  /* ── Overlay blocks: simple side margins ── */
  .more-block--col1,
  .more-block--col3 {
    margin-left: 6.4vw;
    width: calc(100% - 12.8vw);
  }

  /* Running bio (section 2): 16px / 28px */
  .more-block--col1 .more-bio {
    font-size: 1rem;       /* 16px */
    line-height: 1.75;     /* 28/16 */
  }

  /* ── Overlay: larger gap between blocks ── */
  .more-overlay { --mo-gap: 6rem; }

  /* ── Overlay: captions hidden on mobile ── */
  .more-caption { display: none !important; }

  .more-block--rail {
    margin-left: 6.4vw;
    margin-right: 6.4vw;
    width: calc(100% - 12.8vw);
  }

  /* Photo rails: loose horizontal strip
     !important needed to override the higher-specificity nth-child rules */
  .more-rail {
    flex-wrap: nowrap;
    overflow-x: visible;
    gap: 3px;
    align-items: flex-end; /* common baseline, margin-bottom creates the stagger */
  }

  /* Photos at natural proportion on mobile — no fixed height */
  .more-photo-item img {
    height: auto;
  }

  /* Reset the desktop offsets */
  .rail-1 .more-photo-item,
  .rail-2 .more-photo-item {
    flex-shrink: 0;
    margin-left: 0 !important;
    margin-top:  0 !important;
  }

  /* Rail 1: widths and vertical stagger at 375px */
  .rail-1 .more-photo-item:nth-child(1) { width: 24.5vw !important; margin-bottom: 6px; }
  .rail-1 .more-photo-item:nth-child(2) { width: 15.5vw !important; margin-bottom: 19px; }
  .rail-1 .more-photo-item:nth-child(3) { width: 32.5vw !important; margin-bottom: 0; }
  .rail-1 .more-photo-item:nth-child(4) { width: 12vw  !important; margin-bottom: 27px; }

  /* Rail 2: shows all 5 photos, loose strip */
  .rail-2 .more-photo-item:nth-child(1) { display: flex !important; width: 22.7vw !important; margin-bottom: 22px; }
  .rail-2 .more-photo-item:nth-child(2) { width: 12.5vw !important; margin-bottom: 9px; }
  .rail-2 .more-photo-item:nth-child(3) { width: 17.6vw !important; margin-bottom: 27px; }
  .rail-2 .more-photo-item:nth-child(4) { width: 14.1vw !important; margin-bottom: 26px; }
  .rail-2 .more-photo-item:nth-child(5) { width: 17.1vw !important; margin-bottom: 0; }

  /* ── Profile section on mobile: the waterfall collapses to one column ──
     A three-track diagonal cannot survive 375px, so the grid goes to a single
     stack and the source order carries it: lead paragraph, photograph, tail
     paragraph. The desktop version reads top-left / centre / bottom-right;
     stacked, that same order reads straight down, which is the same guided
     path with the horizontal offsets taken out.

     Note this puts the photograph in the MIDDLE of the copy rather than above
     it, which is where it used to sit on a phone. That follows from the source
     order the desktop layout needs, and it is the better read anyway — the
     image breaks the two paragraphs instead of front-loading the fold.

     No margin-left/width override here either, for the same reason as the
     desktop rule above: .more-fold already insets both sides. */
  .more-block--profile {
    display: block;
  }

  /* profile-big: ~45.6% of the screen (171/375), left-aligned */
  .more-profile-img {
    display: block;
    width: 45.6vw;
    margin-left: 0;
  }

  /* The desktop max-width is a vw figure tuned for a three-column row; at
     phone width it would crush the measure to ~110px. */
  .more-bio--lead,
  .more-bio--tail {
    max-width: none;
  }

  /* Size is a deliberate mobile floor: --text-ui bottoms out at 14px, which is
     too small for a full paragraph on a phone, so this holds 16px. The leading
     is NOT an exception — it tracks the desktop rule's 1.5 so the bio keeps the
     same rhythm as the rest of the site's prose at every width. Was 1.625. */
  .more-block--profile .more-bio {
    font-size: 1rem;  /* 16px */
    line-height: 1.5;
  }

  /* Space between the stacked items: 16px on mobile (was 24px between the two
     paragraphs, which are no longer adjacent siblings). */
  .more-block--profile > * + * {
    margin-top: 1rem; /* 16px */
  }

  /* ── Case overlay: mobile ── */
  .case-overlay {
    --case-margin-v-hero:  84px;
    --case-margin-v-main:  80px;
    --case-margin-v-short: 40px;
  }

  .case-close {
    top: 24px;
    right: 24px;
    padding: 12px;
  }

  .case-hero {
    padding: var(--case-margin-v-hero) 6.4vw 0;
  }

  .case-main {
    padding: var(--case-margin-v-main) 6.4vw 32px;
  }

  .case-text-block {
    width: 100%;
    gap: 32px;
  }

  .case-title,
  .case-team-title {
    font-size: 2rem;    /* 32px */
    line-height: 1.125; /* 36/32 */
  }

  .case-body p {
    font-size: 0.875rem; /* 14px */
    line-height: 1.571;  /* 22/14 */
  }

  /* Metrics: stacked on mobile */
  .case-metrics {
    width: 100%;
    flex-direction: column;
    gap: 32px;
    margin-top: 32px;
  }

  .case-metric { width: 100%; }

  .case-metric-number {
    font-size: 4rem; /* 64px — keeps its impact on mobile */
    line-height: 1.125;
  }

  .case-video-section {
    padding: var(--case-margin-v-short) 6.4vw 0;
  }

  .case-video-wrapper {
    width: 100%; /* full-width on mobile */
  }

  .case-gallery-section {
    padding: var(--case-margin-v-short) 6.4vw 0;
    gap: 24px;
  }

  /* The image row becomes a column. Radius does not change here — it is one
     value at every breakpoint. */
  .case-img-row { flex-direction: column; }

  .case-img-full img { aspect-ratio: 327 / 164; }

  .case-img-half { flex: none; width: 100%; }

  .case-img-half img { aspect-ratio: 327 / 334; }

  .case-team-section {
    padding: var(--case-margin-v-main) 6.4vw;
  }

  .case-team-inner { width: 100%; }

  .case-team-label,
  .case-team-names {
    font-size: 0.875rem; /* 14px */
    line-height: 1.571;  /* 22/14 */
  }

  /* Career: back to a block layout on mobile */
  .more-career {
    display: block;
  }
  .more-career-header {
    padding-right: 0;
  }
  .more-career-cols-header {
    margin-top: clamp(1.5rem, 2.5vw, 2.5rem);
  }
  .more-career-bio {
    padding-right: 0;
  }
  .more-career-list {
    padding-top: 0;
  }
}

/* ── Tablet (641px – 1024px) ────────────────────────────────────────────── */
@media (min-width: 641px) and (max-width: 1024px) {

  /* Stacked: the headline is no longer beside the frame, so there are no
     ascenders to align to and no stretched column to bottom-anchor against.
     Both offsets go back to zero and the pair just sits at the top. */

  /* Close button nearer — 112px is too much for tablet */
  .more-close {
    top: 40px;
  }

  .more-intro-image {
    left: 50vw;
    width: 44vw;
  }

  .more-intro-text { width: 85vw; }

  /* Overlay cols: less offset, more width — col1 and col3 share a width
     so they do not look wider than the career-bio that spans the full rail */
  .more-block--col1,
  .more-block--col3 {
    margin-left: var(--mo-margin);
    width: 65vw;
  }
}

/* ── Large screens (> 1600px) ───────────────────────────────────────────── */
@media (min-width: 1600px) {

  /* Stops the intro image stretching too far on 4K */
  .more-intro-image {
    max-width: 740px;
  }

  /* Close button: never more than ~8vw from the corner */
  .more-close {
    top: max(80px, 5.5vw);
  }

  /* ── With vw-based fonts the proportions already hold.
     Only the max-width of fixed-container elements needs reducing. ── */
  .section-title { max-width: 42%; }   /* was 49% */

  /* Overlay: col1 capped in px so it does not run away */
  .more-block--col1 {
    width: min(40.83vw, 56rem); /* 896px cap */
  }
}


/* ─── Testimonials ──────────────────────────────
   Interaction borrowed from bikiron.in/'s Recommendations row (a continuously
   translating, seam-free card list); every visual value below is this site's
   own. Notably NOT imported from the reference: its uppercase roles — this
   site uses no text-transform anywhere — and its square 0-radius avatars.
   Behaviour lives in testimonials.js. */
#testimonials.section {
  height: auto;
  min-height: 100svh;
  justify-content: center;
}

/* .section-title hardcodes cream for the dark work section; on the cream
   ground it has to flip. Type, size and the shared scroll reveal all still
   come from the base component. */
#testimonials .section-title {
  font-size: var(--landing-h-size);
  font-weight: var(--landing-h-weight);
  color: var(--color-dark);
  max-width: 72%;
  /* +24px over the base 48px. The section centres its content vertically
     (.section's justify-content: center), so widening this gap doesn't push
     the cards down — it grows the whole title+cards block symmetrically
     around the section's centre, which reads as the title moving up. */
  margin-bottom: 72px;
  /* Centred on request, same treatment as #work's heading: margin-inline:auto
     centres the 72%-wide box on the page, text-align centres the two lines
     within it. .tm-viewport below is unaffected — it is full-bleed by its own
     negative margins (see its comment), not positioned relative to this
     heading, so the marquee still runs edge to edge under a centred title. */
  margin-inline: auto;
  text-align: center;
}

.tm-lede {
  font-family: var(--font-ui);
  font-size: var(--text-body);
  line-height: 1.5;
  color: rgba(30, 30, 32, 0.56);   /* the #a3a3a3 label idiom, re-weighted for a light ground */
  margin-bottom: clamp(2.5rem, 5vw, 4.5rem);
}

/* Full-bleed: the row should read as continuing past both edges, so it escapes
   the section's page margins while the heading above stays on the grid. */
.tm-viewport {
  position: relative;
  width: calc(100% + 2 * var(--page-margin));
  margin-left: calc(-1 * var(--page-margin));

  --tm-card-w: clamp(272px, 26vw, 420px);

  /* Room for the hover shadow to render without being clipped. `overflow-x:
     hidden` alone does not buy this: CSS computes overflow-y as `auto`
     whenever the two axes differ (regardless of which was set explicitly),
     and `auto` clips painted effects — box-shadow included — exactly like
     `hidden` does, it only differs in whether scrollbars can appear. There is
     no way to clip one axis and leave the other truly open on a single box,
     so this pads real room into the box instead and cancels the layout shift
     with a matching negative margin. Bottom needs more than top because the
     shadow's own offset points down.
     --tm-vpad-top is also the clone's `top` below — that is the one property
     this whole thing depends on. An absolutely positioned child's `top: 0`
     sits at the padding edge, not the content edge, so if this padding and
     the clone's offset ever came from two different values they would drift
     out of sync and the two card sets would misalign, which is exactly what
     happened here before this was tied to one variable. */
  --tm-vpad-top: 16px;
  --tm-vpad-bottom: 32px;
  padding: var(--tm-vpad-top) 0 var(--tm-vpad-bottom);
  margin-block: calc(-1 * var(--tm-vpad-top)) calc(-1 * var(--tm-vpad-bottom));

  overflow: hidden;
  /* Softens both ends so cards dissolve rather than being guillotined. */
  mask-image: linear-gradient(to right, transparent 0, #000 8%, #000 92%, transparent 100%);
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 8%, #000 92%, transparent 100%);
}

.tm-track {
  display: flex;
  align-items: stretch;
  gap: 24px;
  width: max-content;
  will-change: transform;
}

/* Only the swipe strip is inset to the page grid; the marquee runs edge to edge
   (padding there would open a hole in the loop, since the repeating unit is
   measured from the track's own width). */
.is-swipe .tm-track { padding: 0 var(--page-margin); }

/* The duplicate set rides one full set-width to the right of the original. */
.tm-track--clone {
  position: absolute;
  /* Matches --tm-vpad-top on .tm-viewport, not hardcoded — see the comment
     there. This is where the in-flow track actually starts once the padding
     is applied; `top: 0` would sit at the padding edge instead and the two
     card sets would misalign. */
  top: var(--tm-vpad-top);
  left: 0;
}

.is-marquee .tm-track { cursor: default; }

/* Touch: no marquee, just a scroll-snapping strip under the thumb. */
.is-swipe {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.is-swipe::-webkit-scrollbar { display: none; }
.is-swipe .tm-card { scroll-snap-align: center; }

/* ── Card ── */
.tm-card {
  flex: 0 0 auto;
  width: var(--tm-card-w);
  display: flex;
  flex-direction: column;
  gap: 32px;
  padding: 32px;
  margin: 0;
  /* No border — the project cards dropped theirs for a shadow and these were
     asked to match. Same tokens, so the two surfaces cannot drift apart. */
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  /* White, matching the project cards, on instruction — this used to be
     var(--color-cream), the same value as the section's own ground
     (#testimonials is data-bg="hero"), specifically so the card would blend
     into the dot field rather than sit on it. That reasoning is gone now:
     the two card types share a background token as well as a shadow one, so
     neither can drift from the other, and the card reads as a surface ON the
     field instead of a patch of the same field — the same relationship the
     white project cards already have with the dark #work ground. Still
     opaque, so it still isn't see-through to the dots or to body::after's
     grain (fixed above everything at z-index 10) sitting behind it. */
  background: var(--color-white);
  transition: box-shadow 0.28s ease;
  position: relative;
  z-index: 0;   /* see the hover note — the card needs a z-index at rest too,
                   or its resting shadow paints under the next card along */
}

/* ── Card hover ───────────────────────────────────────────────────────────
   Shadow only — no size change. The row keeps translating under the cursor
   while hovered, so a card that also grew would keep shifting its own edges
   under a stationary pointer: the boundary the pointer is inside moves out
   from under it, hover drops, the card shrinks back, the boundary moves back
   in, hover fires again — a feedback loop that reads as flicker. Shadow alone
   doesn't move the card's box, so the pointer's relationship to it never
   changes while the row scrolls past underneath. */
.tm-card:hover {
  /* "Slightly darker and bigger": same token pair the project cards use, so
     hovering a testimonial and hovering a project do the same thing. */
  box-shadow: var(--card-shadow-hover);
  z-index: 1;                     /* above its neighbours, so the shadow shows */
}

.tm-quote {
  font-family: var(--font-display);
  font-weight: 500;   /* medium — Clash Display's real 500, not synthesized */
  font-size: var(--text-lead);
  line-height: 1.32;
  letter-spacing: -0.02em;
  color: var(--color-dark);
  margin: 0;
}


.tm-attrib {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-top: auto;   /* pins attribution to the base so cards align */
}

.tm-avatar {
  width: 64px;
  height: 64px;
  flex-shrink: 0;
  border-radius: var(--radius);
  object-fit: cover;
  display: block;
}

/* Stand-in until real portraits land — a monogram, never a stock face. */
.tm-avatar--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(30, 30, 32, 0.08);
  border: 1px solid rgba(30, 30, 32, 0.12);
  font-family: var(--font-display);
  font-size: var(--text-ui);
  color: rgba(30, 30, 32, 0.4);
  user-select: none;
}

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

.tm-name {
  font-family: var(--font-ui);
  font-size: var(--text-ui);
  line-height: 1.3;
  color: var(--color-dark);
}

.tm-role {
  font-family: var(--font-ui);
  font-size: var(--text-meta);
  line-height: 1.35;
  color: rgba(30, 30, 32, 0.56);
}

@media (max-width: 640px) {
  #testimonials .section-title { max-width: 100%; }
  .tm-card { padding: 24px; gap: 24px; }
  .tm-avatar { width: 56px; height: 56px; }
  /* The mask fights a thumb-scrolled strip — the first card should sit flush. */
  .tm-viewport {
    mask-image: none;
    -webkit-mask-image: none;
  }
}

/* ─── Ring-bound photo album ─────────────────
   Sits in the About me overlay in place of the two photo rails. Reads as a
   physical object on the overlay's dark ground: warm paper, a metal ring
   binding across the middle, and a top page that turns over the rings.

   The binding is HORIZONTAL, so a page turn is rotateX about the album's
   centre line — a ring-bound flip pad, not a book. */
.album-wrap {
  display: flex;
  justify-content: center;
  padding: clamp(24px, 5vw, 72px) 0;
}

.alb {
  --alb-paper:   #f4f1ea;   /* warm off-white, from the references */
  --alb-paper-2: #eae6dd;   /* the tone the lower page sits at */
  --alb-ink:     #26262a;
  --alb-lift:    0;         /* 0 at rest, 1 when the page stands upright */

  position: relative;
  width: min(680px, 90vw);
  /* Perspective on the container, not the leaf: every child shares one vanishing
     point, so the turning page and the rings agree about where the viewer is. */
  perspective: 1700px;
  perspective-origin: 50% 50%;
  cursor: grab;
  outline: none;
  user-select: none;
  -webkit-user-select: none;
  touch-action: pan-x;      /* we own vertical drag; let horizontal scroll pass */
}

.alb.is-dragging { cursor: grabbing; }
.alb:focus-visible { box-shadow: 0 0 0 2px var(--color-cream); border-radius: var(--radius); }

/* ── Paper ── */
.alb-deck,
.alb-lower {
  position: relative;
  background: var(--alb-paper);
  border-radius: 2px;   /* sharp, not a rounded card — per the brief */
  box-shadow:
    0 1px 0 rgba(255,255,255,.5) inset,
    0 18px 40px rgba(0,0,0,.42),
    0 2px 6px rgba(0,0,0,.3);
}

/* Very quiet paper tooth. Two offset gradients rather than a noise image, so
   there is no extra request and the texture cannot tile visibly. */
.alb-deck::after,
.alb-lower::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  opacity: .5;
  background:
    repeating-linear-gradient(94deg,  rgba(0,0,0,.012) 0 2px, transparent 2px 5px),
    repeating-linear-gradient(3deg,   rgba(0,0,0,.010) 0 3px, transparent 3px 7px);
}

.alb-deck {
  height: clamp(240px, 34vw, 300px);
  transform-style: preserve-3d;
}

.alb-lower {
  margin-top: 26px;         /* the gap the binding sits across */
  background: var(--alb-paper-2);
  overflow: hidden;
}

/* ── Leaves ── */
.alb-leaf {
  position: absolute;
  inset: 0;
  background: var(--alb-paper);
  border-radius: 2px;
  backface-visibility: hidden;
  /* Hinge on the BOTTOM edge — the binding line — so the page pivots there
     exactly as paper pivots on a ring. */
  transform-origin: 50% 100%;
  transform-style: preserve-3d;
  will-change: transform;
}

.alb-leaf--next    { z-index: 1; }
.alb-leaf--current {
  z-index: 2;
  transition: transform .42s cubic-bezier(.22,1,.36,1);
}

/* The shadow the lifting page throws onto the page beneath it. Driven by --lift
   so it deepens as the page stands up and clears as it falls away. */
.alb-leaf--current::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(0,0,0,.30) 0%, rgba(0,0,0,0) 62%);
  opacity: calc(var(--lift, 0) * .85);
}

/* Page thickness: a hairline on the hinge edge, visible only while turning. */
.alb-leaf--current::before {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 2px;
  background: linear-gradient(180deg, #d9d4c8, #b9b3a5);
  opacity: calc(var(--lift, 0) * 1);
  transform: translateZ(-1px);
}

/* ── Type ── */
.alb-page {
  position: absolute;
  inset: 0;
  padding: clamp(18px, 3.4vw, 30px);
  display: flex;
  flex-direction: column;
  color: var(--alb-ink);
}

.alb-meta {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10px;
  letter-spacing: .16em;
  color: rgba(38,38,42,.62);
}

.alb-page-body { margin-top: auto; }

.alb-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(19px, 2.6vw, 26px);
  line-height: 1.22;
  letter-spacing: -0.01em;
  max-width: 20ch;
  margin: 0 0 10px;
}

/* The handwritten accent the references use for occasional titles. */
.alb-script {
  font-family: 'Snell Roundhand', 'Apple Chancery', 'Segoe Script', cursive;
  font-size: clamp(24px, 3.4vw, 34px);
  font-weight: 400;
  margin: 0 0 12px;
  color: var(--alb-ink);
}

.alb-body {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
  line-height: 1.75;
  letter-spacing: .01em;
  color: rgba(38,38,42,.74);
  max-width: 46ch;
  margin: 0;
}

.alb-date {
  position: absolute;
  right: clamp(18px, 3.4vw, 30px);
  bottom: clamp(14px, 2.4vw, 22px);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 9.5px;
  letter-spacing: .2em;
  color: rgba(38,38,42,.45);
}

/* ── Photographs ── */
.alb-photos {
  display: grid;
  gap: 8px;
  padding: 10px;
  height: clamp(250px, 38vw, 340px);
}
.alb-photos--single { grid-template-columns: 1fr; }
.alb-photos--pair   { grid-template-columns: 1fr 1fr; }
.alb-photos--trio   { grid-template-columns: repeat(3, 1fr); }
.alb-photos--inset  { grid-template-columns: 1fr; position: relative; }

.alb-fig {
  position: relative;
  margin: 0;
  overflow: hidden;
  background: #ddd8cd;
  min-height: 0;   /* lets the image shrink inside the grid row */
}

.alb-fig img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* the grain is baked into the files; this is only a whisper of print contrast */
  filter: contrast(1.02);
}

.alb-fig figcaption {
  position: absolute;
  left: 6px;
  bottom: 5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 8.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: rgba(255,255,255,.92);
  text-shadow: 0 1px 3px rgba(0,0,0,.7);
}

/* The smaller photograph laid over a corner of the larger one. */
.alb-fig--inset {
  position: absolute;
  right: 20px;
  bottom: 20px;
  width: 34%;
  height: 42%;
  border: 3px solid var(--alb-paper);
  box-shadow: 0 6px 16px rgba(0,0,0,.34);
}

/* Tiny, deliberate misalignments so the album is not mathematically perfect. */
.alb-photos--trio  .alb-fig:nth-child(1) { transform: translateY(-2px) rotate(-.25deg); }
.alb-photos--trio  .alb-fig:nth-child(3) { transform: translateY(2px)  rotate(.2deg); }
.alb-photos--pair  .alb-fig:nth-child(2) { transform: translateY(-2px) rotate(.2deg); }

/* ── Binding ── */
.alb-binding {
  position: absolute;
  left: 0; right: 0;
  /* centred on the seam between the two halves */
  top: clamp(240px, 34vw, 300px);
  height: 26px;
  margin-top: -1px;
  z-index: 5;             /* above BOTH halves, so a turning page passes behind */
  pointer-events: none;
}

.alb-rail {
  position: absolute;
  inset: 0;
  /* The reference's spine is a dark metal channel, not a pale paper strip.
     Three stops: a bright top lip catching the light, a deep shadowed trough,
     and a second highlight where the lower lip turns back toward the viewer. */
  background: linear-gradient(180deg,
    #b9bcc0 0%, #7c8085 9%, #35383c 26%,
    #17191b 50%,
    #303337 74%, #6f7378 91%, #a9adb1 100%);
  box-shadow:
    0 1px 3px rgba(0,0,0,.5),
    0 -1px 0 rgba(255,255,255,.22) inset;
}

/* A hairline of the channel's inner slot, running the length of the rail. */
.alb-rail::after {
  content: '';
  position: absolute;
  left: 3%; right: 3%;
  top: 50%; height: 2px;
  margin-top: -1px;
  background: linear-gradient(180deg, rgba(0,0,0,.85), rgba(120,124,128,.5));
  border-radius: 1px;
}

/* Wire rings. The SVG carries the metal; this only places it and lets it sit
   proud of the rail so the loop reads as passing over the paper's edge. */
.alb-ring {
  position: absolute;
  top: 50%;
  width: 30px;
  height: 56px;
  margin: -28px 0 0 -15px;
  overflow: visible;
}

/* ── Progress ── */
.alb-progress {
  display: flex;
  gap: 5px;
  justify-content: center;
  margin-top: 16px;
}
.alb-tick {
  width: 14px; height: 1px;
  background: rgba(245,244,240,.26);
  transition: background .3s ease;
}
.alb-tick.is-on { background: rgba(245,244,240,.85); }

/* Screen-reader announcement of the current page; never shown. */
.alb-live {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Contact. For the fraction of a second the page lands, the shadow it casts
   on the sheet below deepens and then clears — the visual half of the weight
   cue that Sound.land() carries in the ear. */
.alb.is-landing .alb-lower::before {
  opacity: 1;
  transition: opacity .06s ease-out;
}
.alb-lower::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity .5s ease-in;
  background: linear-gradient(180deg, rgba(0,0,0,.34) 0%, rgba(0,0,0,0) 46%);
}

/* Reduced motion: no 3D turn at all, just a quiet settle. */
.alb.is-fading .alb-deck,
.alb.is-fading .alb-lower { opacity: .35; transition: opacity .18s ease; }

@media (prefers-reduced-motion: reduce) {
  .alb { cursor: pointer; }
  .alb-leaf--current { transition: none; }
}

@media (max-width: 640px) {
  .alb { width: 92vw; }
  .alb-deck   { height: clamp(210px, 46vw, 260px); }
  .alb-binding{ top: clamp(210px, 46vw, 260px); }
  .alb-photos { height: clamp(200px, 52vw, 280px); }
  /* three photographs at phone width would each be a stamp — go to two-up */
  .alb-photos--trio { grid-template-columns: 1fr 1fr; }
  .alb-photos--trio .alb-fig:nth-child(3) { display: none; }
}

/* ─── About me overlay: four folds ───────────
   Restructured from the old intro / bio / profile / career stack. Each fold is
   one idea with room around it, rather than blocks butted together. */
.more-fold {
  /* A shade under a full viewport. At exactly 100svh a centred fold reads as
     floating in a void; slightly under, with the next fold's top edge just
     showing, it reads as a considered spread. */
  min-height: 88svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* The overlay does not inset its children — the blocks this replaced each
     carried their own margin (.more-block--col1 used --mo-col1, the rails used
     --mo-margin). Without this the folds sat flush against the viewport edge. */
  margin-left: var(--mo-margin);
  margin-right: var(--mo-margin);
  /* Cancels the .more-block gap these inherit. A fold already owns its vertical
     rhythm through min-height plus centring; stacking a 320px block margin on
     top of a full-viewport box is what pushed the whole composition down the
     screen and left a void above it. */
  margin-top: 0;
}

/* Extra air above fold 2 (Don Norman), on top of margin-top: 0 above.
   6vw is not a new number: it is .section's own top/bottom padding — the
   landing screen's one documented value for "how much room a piece of
   content gets from its neighbour" (see .section, padding: 6vw
   var(--page-margin)). Reused here rather than inventing a fresh figure, so
   the overlay's rhythm reads as the same rhythm as the rest of the site.

   +200px flat on top of that (100px, then another 100px on top of that):
   at narrower desktop widths 6vw alone shrinks fast enough (54px at 900px
   wide) that the two folds read as one block running together rather than
   two. The flat addition holds a real minimum gap at any width, where 6vw
   alone could not. */
.more-fold--profile {
  /* Was calc(6vw + 200px), hand-dialled in two 100px steps. Now the shared
     rhythm — see --mo-section-gap on .more-overlay. */
  margin-top: var(--mo-section-gap);
  /* Overrides .more-fold's min-height: 88svh, and this is what makes the
     rhythm exact rather than approximate.

     .more-fold is a centred flex column, so any height the fold has beyond
     its content is split above and below it as slack that no rule names. At
     1440 that slack was 12px — the unit (448px) plus the old 320px tail came
     to 768 inside a 792px box, close enough to hide the problem. It does not
     stay 12px: it is (88svh - content) / 2, so it grows on a short viewport
     and the gaps above and below the unit drift apart as the window resizes.
     With min-height: auto the fold is exactly its content, and the margin and
     padding above are the whole story.

     The other fold is unaffected: .more-fold--orbit runs past 88svh on its
     own (the reel's stage is 88svh by itself), so the min-height was already
     inert there. This is the only fold it was reaching. */
  min-height: auto;
  /* position: relative is kept for the fold's own children, not for a field.
     The dot field that used to live here (a ::before with inset: 0, plus
     z-index: 1 on this fold to contain its z-index: -1) was retired on
     2026-09-29 when the grid was extended over the whole overlay — see
     .more-overlay. The z-index: 1 went with it, since nothing here needs a
     stacking context any more. */
  position: relative;
  /* The instruction that produced the halved dot opacity here — "make the
     pixel grid a lot lighter here so it doesn't clash with the text" — is not
     lost with the local field: it is what set the 16% in --dot-ink on
     .more-overlay, which is now the strength of the grid everywhere in the
     overlay, including behind this copy. :root keeps 0.32 for the page's own
     two fields. */
}

/* The tail below the photo-and-bio unit, written to the same section rhythm
   as the three gaps above it so the composition closes the way it opens.

   Doubled class, not for weight-for-its-own-sake: this has to outrank
   .more-block:last-of-type { padding-bottom: var(--mo-gap) } near the top of
   the overlay's rules, which is (0,2,0) against a bare .more-fold--profile's
   (0,1,0). That generic 320px tail predates this composition — it is the gap
   BETWEEN blocks in the overlay's original stack, reused as a tail because
   whichever block came last inherited it — and it is still correct for any
   other last block, so it is overridden here rather than changed there.
   Measured: the tail read 320px with the single class and 198px with two. */
.more-block.more-fold--profile {
  padding-bottom: var(--mo-section-gap);
}

/* The dot field that stood behind this photo-and-text unit was removed on
   2026-09-29, when the grid was extended over the entire overlay. Its box was
   the FOLD's, not the unit's — inset by var(--mo-margin) on both sides so the
   dot columns landed on the overlay's equivalent of --page-margin, running
   wider than the content sitting on it, the way #testimonials::before does
   behind its cards. The overlay-wide field inherits that rhythm for free: it
   tiles from the scroll container's own origin, which is the same origin the
   margins are measured from. */

/* Fold 1 — the portrait and the line that belongs with it, side by side. The
   statement no longer stands alone on a full-height slide: it is anchored to a
   person, which is what the fold is for. */
.more-fold--intro {
  flex-direction: row;
  /* Centred, which is what keeps the pair balanced in the fold rather than
     clustered against its top edge — this element is both the 88svh fold and
     the row, so once it became a row, flex-start pinned the content up there.
     Centring costs nothing in alignment: setPortraitHeight makes the frame and
     the headline exactly equal in height, and two items of equal height centre
     and top-align to the same place. Their top edges still line up. */
  align-items: center;
  gap: clamp(32px, 10.625vw, 170px);
}

.more-portrait {
  /* THE RULE: the frame's height always equals the height of the headline
     beside it, so the two share a top and a bottom edge at every width.

     Driven by JS (see setPortraitHeight in script.js), which writes the
     measured headline height into --portrait-h. Flexbox cannot express this on
     its own: in a row, item widths resolve BEFORE the line's height is known,
     so aspect-ratio has no height to work from and the frame collapses to zero
     — and align-items: stretch sizes children to this element's own 88svh
     rather than to each other.

     The WIDTH stays fixed rather than following the height. That is deliberate:
     if width tracked height, the frame would narrow, the headline column would
     widen, the headline would reflow shorter, the frame would shrink again —
     a loop with no stable answer. A fixed width keeps the text column constant,
     so one measurement settles it. */
  /* 488 x 520 at 1600, chosen in the dial kit. Kept as vw rather than the flat
     px the export gives, so the frame still shrinks on narrower desktops — at a
     fixed 488 the frame plus its 170px gap would leave almost nothing for the
     headline between 860 and 1200px. */
  flex: 0 0 clamp(260px, 30.5vw, 488px);
  /* An EXPLICIT height, which overrides --portrait-h and so supersedes the
     match-the-headline rule. That is the point: matching forced a landscape
     frame out of a portrait photograph and cover discarded 57% of it. At this
     shape (0.94 against the file's 0.75) 80% survives. The frame no longer
     shares its bottom edge with the headline. */
  height: clamp(300px, 32.5vw, 520px);
  margin: 0;
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
}

.more-portrait img {
  width: 100%;
  height: 100%;
  /* The frame is now a different shape from the file (matching the headline's
     height means a wider, shorter box than the portrait's own 563x750), so the
     photograph is cropped to fill it. Positioned toward the top: the subjects
     sit in the upper half of this frame, and centre would cut their heads. */
  object-fit: cover;
  object-position: 88% 54%;
  display: block;
}

.more-portrait figcaption {
  /* Hangs below the frame rather than sitting inside it — in flow it would add
     its own height to the line and break the match by exactly its own size. */
  position: absolute;
  top: 100%;
  left: 0;
  white-space: nowrap;
  margin-top: 12px;
  font-family: var(--font-ui);
  font-size: var(--text-meta);
  letter-spacing: -0.01em;
  /* Was cream at 55% over the dark ground; now dark at 65% over cream. Not a
     literal mirror, for the reason spelled out on .more-caption above: the
     same 55% flips from 5.47:1 to 3.67:1 when the ground does, which fails AA
     at this size. 65% restores it to 4.99:1. Inert like the rest of
     .more-fold--intro, kept in step so a revival is not invisible. */
  color: color-mix(in srgb, var(--color-dark) 65%, var(--color-cream));
}

/* The line sits on the same optical baseline as the portrait rather than
   filling the column, so the two read as a pair. */
.more-fold--intro .more-title {
  flex: 1 1 auto;
  /* Scoped deliberately, rather than editing --text-headline: that token also
     sets the about fold on the main page and the album title, and neither
     should move. Down from ~71px at 1600 so the sentence reads confident
     instead of being the only thing in the room. */
  /* Sized for THIS sentence. At ~30 words a 17ch measure ran to eight lines and
     read as a tall narrow slab; letting it use the column it has, at a slightly
     smaller size, brings it to five or six long lines — which is what a
     statement of this length wants. Still scoped, never --text-headline: that
     token also drives the about fold and the album title. */
  font-size: clamp(1.55rem, 3.125vw, 3.125rem);
  line-height: 1.5;
  max-width: 22ch;
  text-wrap: pretty;
  margin: 0;
}

/* Folds 3 and 4 — text alone, held to a readable measure and set left rather
   than centred, matching how the rest of the site sets long copy. */
.more-fold--text {
  align-items: flex-start;
}

.more-fold--text .more-bio {
  max-width: 62ch;
}

.more-fold--text .more-bio + .more-bio {
  margin-top: 1.15em;
}

/* Fold 1 — the statement, alone. No photo shares this fold with it any more,
   so it owns the full column: larger and wider than the paired version this
   replaced, which was scoped to .more-fold--intro and tuned to sit beside a
   488x520 frame. That selector is no longer used anywhere in index.html —
   its rules (below, .more-fold--intro / .more-portrait) are dead but left in
   place rather than deleted, in case the pairing comes back.
   justify-content overrides .more-fold's default centring: weighted toward
   the lower part of the fold rather than dead-centre, so there is real air
   above the sentence before it arrives, matching the reference. */
.more-fold--statement {
  justify-content: flex-end;
  padding-bottom: clamp(48px, 8vw, 120px);
}

.more-fold--statement .more-title {
  font-size: clamp(2rem, 4.8vw, 4.75rem);
  line-height: 1.2;
  max-width: 23ch;
  margin: 0;
  /* Dial-kit final. Not vw-retargeted: this is a literal nudge, not a value
     tied to viewport width, so there is no clamp() to preserve. */
  transform: translate(0px, 100px);
}

@media (max-width: 860px) {
  /* Portrait above the line — side by side stops working once the line has to
     wrap to four or five lines against a shrinking image. */
  .more-fold--intro {
    flex-direction: column;
    align-items: flex-start;
    gap: clamp(24px, 5vw, 40px);
  }
  .more-portrait { flex: 0 0 auto; width: min(280px, 62vw); }
  .more-fold--intro .more-title { max-width: none; }
  /* min-height: auto so a fold is its content at narrow widths. The
     `padding: 12svh 0` that used to ride along here was removed on
     2026-09-29: the overlay's vertical rhythm is now --mo-section-gap, and
     the two live folds each write it themselves (.more-fold--orbit as
     padding-top, .more-fold--profile as margin-top plus padding-bottom), so
     the 12svh was reaching exactly one edge — the profile fold's top — and
     adding 97px to a gap that was meant to match the three above it.
     Measured at 375: 232px where the others read 135. If one of the inert
     folds (.more-fold--intro / --text / --statement) is ever revived, it
     needs its own spacing written to --mo-section-gap; it will not inherit
     a mobile padding from here any more. */
  .more-fold { min-height: auto; }
}

/* ─── Orbital photoreel ──────────────────────
   Fold 3 of the About me overlay. Photographs orbit an invisible ellipse with
   a single line of type in the negative space at its centre. All geometry is
   computed in orbit-reel.js and written to inline transforms each frame —
   nothing here positions a photo, only styles it. */

/* Full-bleed: .more-fold insets both sides by var(--mo-margin), and the reel
   is meant to run edge to edge. Cancelling the inset is enough here because
   .more-overlay itself carries no padding. */
.more-fold--orbit {
  margin-left: 0;
  margin-right: 0;
  /* Top air, and nothing on the other three sides — this fold is deliberately
     full-bleed (the margins above are zeroed so the reel spans the viewport),
     so the padding is written per-side rather than as a shorthand that would
     also inset the stage. It sits INSIDE the fold, so it pushes the content
     down without moving the fold's own background.

     6vw, reduced on instruction on 2026-09-29. This fold now opens the
     overlay: the statement spread that used to sit above it became its
     headline, taking most of a viewport of air out of the top of the page
     with it. 6vw is not a new number — it is .section's own top/bottom
     padding, the landing screen's one documented figure for how much room a
     piece of content gets from its neighbour, and the same value
     .more-fold--profile builds its gap from. 86px at 1440, which clears the
     fixed .more-close pill (top: 5.56vw) beside it.

     +100px flat on top of that, on instruction on 2026-09-29: 6vw alone put
     the headline level with the close pill, and the fold read as starting at
     the very top of the overlay rather than being given room. Flat rather
     than folded into the vw term because it is a literal nudge — the gap is
     meant to hold at any width, and 6vw alone shrinks to 54px at 900px wide.
     Same reasoning, and the same shape, as .more-fold--profile's
     calc(6vw + 200px).

     The 40px that used to be here separated the reel from whatever fold sat
     above it; that job now belongs to .more-orbit-head's bottom margin.

     2026-09-29, later the same day: the hand-dialled calc(6vw + 100px) — the
     186px described above — became var(--mo-section-gap) when the overlay's
     three sections were put on one rhythm. 198px at 1440, so this gap grew by
     12px rather than changing character, and it still clears the fixed
     .more-close pill (top: 5.56vw). See --mo-section-gap on .more-overlay. */
  padding: var(--mo-section-gap) 0 0;
}

/* The reel's headline. Was the overlay's opening statement — a left-aligned
   line in a full-height fold of its own — until it was turned into this
   section's head on 2026-09-29.

   Sized to --text-case-h2, the case studies' section-head scale (.cs-h2 in
   case-study.css), on instruction: the same size the Jupiter onboarding piece
   gives its section heads. Weight stays .more-title's 400 rather than .cs-h2's
   500 — only the size was asked for, and 400 is what every other line of
   display type in this overlay carries.

   margin-inline: auto against a max-width is what centres the block.
   .more-fold--orbit zeroes the fold's side insets for the full-bleed reel, so
   at narrow widths the max-width is not enough on its own — min() holds the
   same --mo-margin gutter the rest of the overlay uses once the em measure
   stops fitting.

   22em, widened from .cs-h2's own 18em on instruction ("increase the width of
   the text container so that it all fits in 3 lines"). 18em set this copy in
   four lines with "alive" alone on the last. Measured at 1440, sweeping the
   measure a full em at a time: 18em gives 4 lines, 20em through 26em all give
   3, and 28em gives 2. Both the measure and the font size are viewport-
   relative (em against a vw clamp), so that ratio — and therefore the line
   count — is the same at every width above the clamp's floor; this is not a
   1440-only result. 22em is picked from the middle of the 3-line band rather
   than at 20em's edge, so a font-metric difference cannot tip it back to 4.

   text-wrap: balance is the other half of "avoid leaving one orphan word":
   the measure alone fixes the line COUNT, balance is what stops the natural
   break from dumping a short remainder on the last line. It is a no-op in
   browsers that do not support it, and the 22em measure already gives 3
   lines without it. */
.more-orbit-head {
  font-size: var(--text-case-h2);
  line-height: 1.1;
  max-width: min(22em, 100% - var(--mo-margin) * 2);
  /* The bottom margin is the headline-to-reel gap and was the flat 40px this
     head inherited when it stopped being a fold of its own. That was by far
     the tightest of the overlay's three section gaps — 40px against 186 above
     and 298 below — and is what the 2026-09-29 instruction was about. It is
     now the shared --mo-section-gap, 198px at 1440. `auto` on the sides is
     unchanged and is what centres the measure. */
  margin: 0 auto var(--mo-section-gap);
  text-align: center;
  text-wrap: balance;
}

.orbit {
  position: relative;   /* NOT fixed — see the note at the top of orbit-reel.js */
  width: 100%;
  min-height: 88svh;
  overflow: hidden;
  /* --color-cream: the same ground as the rest of the overlay and the rest of
     the page, on instruction.

     This drops an intent the reel carried from the beginning, so it is worth
     being explicit about. It was #0E0E0E, "a shade UNDER the overlay's own
     #1E1E20, so the reel reads as a lit stage rather than more of the page",
     and on the first light-mode pass it became --color-white — the same step
     away from the ground, taken in the other direction. The reel is now
     deliberately NOT a separate surface: it is the page, with photographs
     orbiting on it. If a future brief wants the stage to read as its own
     surface again, --color-white plus the card shadow is the value to come
     back to, not a new grey.

     transparent, not --color-cream, since 2026-09-29: the overlay's dot field
     is painted on .more-overlay's own background box, so ANY opaque ground on
     a child punches a rectangular hole in it. Cream here is the same colour
     as the overlay's, which made the hole invisible as colour but visible as
     a gap in the grid across the reel's full 88svh. Letting the ground show
     through is what keeps the field unbroken. If the stage ever needs to be
     its own surface again, that is now a deliberate hole in the grid, and the
     value to use is --color-white plus the card shadow. */
  background: transparent;
}

.orbit-stage {
  position: absolute;
  inset: 0;
}

.orbit-photo {
  position: absolute;
  top: 50%;
  left: 50%;
  margin: 0;
  /* width comes from CONFIG.baseWidth (applied on change, not per frame);
     transform/opacity/z-index are written every frame by render(). Every
     photo crops to one shared shape (--orbit-ratio, written by
     applyAspectRatio() — default 4:5) rather than keeping its own native
     dimensions: a mixed set of portrait/landscape/square source photos read
     as a pile, uniform rectangles read as one object rotating. overflow:
     hidden is what lets the cover-fit image below crop to this box. */
  aspect-ratio: var(--orbit-ratio, 4 / 5);
  overflow: hidden;
  /* No border, on instruction on 2026-09-29. There was a 1px hairline here
     — cream at 18% on the old dark stage, then dark at 12% once the stage
     went light. It was drawn to stop a photo's edge dissolving into the
     ground behind it, but at these scales it read as a white mount around
     every picture, which is what it was asked to lose. The shadow below
     already does the separating, so nothing replaces it.

     8px radius, also on instruction. --radius is 4px (the pills and the
     dial's own fields), --radius-lg is 16px (the cards), so this sits
     between the two named steps and is written as a literal. It is on the
     4px grid, which is the rule that matters. If the reel ever needs to
     match the cards exactly, --radius-lg is the token to reach for.

     The shadow was 0 18px 48px black at 55%, which is a dark-room shadow: on
     white it would be a grey smear, the same failure CLAUDE.md records for
     --nav-shadow on cream. --card-shadow-hover is the site's own token for a
     lifted surface and its geometry is near-identical to what was here
     (0 16px 40px -12px against 0 18px 48px), so the photos keep the same
     float without inventing a second shadow scale.

     The background is only what shows through before an image decodes;
     --color-cream keeps that a warm placeholder instead of a black plate. */
  border-radius: 8px;
  box-shadow: var(--card-shadow-hover);
  background: var(--color-cream);
  will-change: transform, opacity;
}

.orbit-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;   /* crops to the box's shared aspect-ratio, not its own */
  user-select: none;
  -webkit-user-drag: none;
}

/* The caption clips its own inner span, so type slides away behind the edge
   rather than fading in place.

   This position is now the only one there is. render() used to overwrite
   the transform every frame to park the caption under whichever photo held
   focus; on instruction on 2026-09-29 it is pinned dead centre instead —
   both axes, translate(-50%, -50%) against top/left 50% — and render() does
   not touch it at all. The centre of the ellipse is the one point on the
   stage no photo ever travels through, so nothing can pass over the line.
   See the "Caption anchoring" note at the top of orbit-reel.js. */
.orbit-cap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 50;
  pointer-events: none;
  overflow: hidden;
  max-width: 80%;
  text-align: center;
  text-transform: uppercase;
  /* Was #FFFFFF. --color-dark on the white stage: 16.6:1. This line sits in
     the negative space at the centre of the ellipse with photographs passing
     around it, so it needs the full-strength ink rather than a mix. */
  color: var(--color-dark);
  line-height: 1.5;
  white-space: nowrap;
  /* font-family, font-style, font-size and letter-spacing are set by
     applyCaptionType() so the dial kit can drive them live */
}

.orbit-cap span { display: block; }

/* Dial only: the photo whose caption row is under the pointer. !important is
   load-bearing here — render() writes opacity inline on this node every frame,
   so a plain declaration would be overridden within ~16ms. The outline is not
   a border: it is drawn outside the box, so it cannot change the photo's size
   or its crop while it is lit. Nothing on the public site ever sets this
   class; it is added and removed by buildCapRows() in orbit-reel.js. */
.orbit-photo.is-dial-lit {
  opacity: 1 !important;
  outline: 3px solid #E8574B;
  outline-offset: 2px;
}

/* Reduced motion: no orbit at all. The photos fall into a plain centred grid
   and the caption sits under them as ordinary type — a static, legible
   layout rather than a stopped animation. render() is never called in this
   mode, so no inline transform is written and none needs overriding. */
.orbit.is-static .orbit-stage {
  position: static;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
  justify-content: center;
  padding: 10svh var(--mo-margin) 0;
}

.orbit.is-static .orbit-photo {
  position: static;
  width: clamp(140px, 18vw, 220px);
}

.orbit.is-static .orbit-cap {
  position: static;
  transform: none;
  max-width: none;
  margin: 32px auto 10svh;
  white-space: normal;
}

/* ── Orbit dial kit ──
   Bottom-LEFT. The hero dial kit (dial-kit.js) is pinned top:12px to
   bottom:12px, so it owns the entire right edge — bottom-right was tried and
   measured as overlapping it. The overlay's close button sits top-left and is
   a small circle, so the bottom of that edge is the one clear corner when
   both panels are open. Same ?dial gate as the hero panel. */
#orbit-dial {
  position: fixed;
  left: 12px;
  bottom: 12px;
  width: 250px;
  max-height: min(78svh, 720px);
  display: flex;
  flex-direction: column;
  z-index: 2147482000;          /* above the overlay (200), below the hero panel */
  background: rgba(14, 14, 14, 0.92);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(237, 234, 227, 0.16);
  border-radius: var(--radius);
  color: #e8e6e1;
  font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}

#orbit-dial, #orbit-dial * { cursor: auto; }
#orbit-dial button, #orbit-dial input, #orbit-dial select { cursor: pointer; font-family: inherit; }
#orbit-dial.is-collapsed .od-body,
#orbit-dial.is-collapsed .od-foot { display: none; }

.od-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 11px;
  border-bottom: 1px solid rgba(237, 234, 227, 0.14);
  flex-shrink: 0;
}

.od-title {
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 10px;
}

.od-head button {
  background: none;
  border: 1px solid rgba(237, 234, 227, 0.22);
  color: #e8e6e1;
  border-radius: var(--radius);
  padding: 3px 8px;
  font-size: 10px;
}

.od-body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 4px 11px 11px;
  flex: 1;
}

.od-group { margin-top: 11px; }

.od-group-name {
  font-size: 9px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(237, 234, 227, 0.5);
  margin-bottom: 5px;
}

.od-row {
  display: grid;
  grid-template-columns: 88px 1fr 34px;
  gap: 6px;
  align-items: center;
  margin-bottom: 5px;
}

.od-row label { color: rgba(237, 234, 227, 0.78); }
.od-row input[type=range] { width: 100%; accent-color: #7bb389; min-width: 0; }
.od-row output { text-align: right; font-variant-numeric: tabular-nums; }

.od-row--wide { grid-template-columns: 88px 1fr; }

.od-row select {
  background: #0f1012;
  border: 1px solid rgba(237, 234, 227, 0.22);
  color: #e8e6e1;
  border-radius: var(--radius);
  padding: 3px 4px;
  font-size: 10px;
  width: 100%;
}

.od-cap-row {
  display: grid;
  /* Third column added for the thumbnail: index, crop, field. */
  grid-template-columns: 20px 44px 1fr;
  gap: 6px;
  align-items: center;
  margin-bottom: 4px;
}

.od-cap-n { color: rgba(237, 234, 227, 0.42); }

/* Square, to match the stage's own 1:1 crop, so the row looks like the photo
   it names rather than a differently-framed version of it. Hard-coded rather
   than read from --orbit-ratio: this is a dev panel, and it should stay
   legible while that ratio is being dialled through 16:9. */
.od-cap-thumb {
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: var(--radius);
  display: block;
}

/* Rows are hoverable targets now, so give the pointer something back. */
.od-cap-row:hover .od-cap-thumb,
.od-cap-row:focus-within .od-cap-thumb {
  outline: 2px solid rgba(237, 234, 227, 0.65);
  outline-offset: 1px;
}

.od-cap-row input {
  background: #0f1012;
  border: 1px solid rgba(237, 234, 227, 0.22);
  color: #e8e6e1;
  border-radius: var(--radius);
  padding: 3px 5px;
  font-size: 10px;
  width: 100%;
  min-width: 0;
}

.od-foot {
  display: flex;
  gap: 6px;
  padding: 9px 11px;
  border-top: 1px solid rgba(237, 234, 227, 0.14);
  flex-shrink: 0;
}

.od-btn {
  flex: 1;
  min-width: 0;
  background: rgba(237, 234, 227, 0.08);
  border: 1px solid rgba(237, 234, 227, 0.22);
  color: #e8e6e1;
  border-radius: var(--radius);
  padding: 6px 4px;
  font-size: 10px;
}

.od-btn:hover { background: rgba(237, 234, 227, 0.14); }

@media (max-width: 700px) {
  #orbit-dial { width: min(250px, calc(100vw - 24px)); }
}

/* ─── Site loader ───────────────────────────────────────────────────────────
   After a supplied reference (frame-by-frame, 2026-10-06), in dark mode so
   the hand-over to the cream page reads: corner marks frame "Where ideas
   bloom", which blurs into focus word by word; the line dissolves into the
   name; the name dims as two curved streams of photo tiles sweep through;
   then, as the tiles head for the edges, the ground turns from dark to the
   page's cream and the loader fades out over the hero. loader.js runs it;
   the head script in index.html decides whether it runs at all. */
.site-loader { display: none; }
html.has-loader { overflow: hidden; }
html.has-loader .site-loader {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 10000;
}
.sl-bg { position: absolute; inset: 0; background: var(--color-dark); }
.sl-content { position: absolute; inset: 0; overflow: hidden; color: var(--color-cream); }

/* Four dots at the corners of a centred frame, like crop marks.
   Centred by inset + auto margins, never by a translate: GSAP animates this
   element, and it folds a CSS translate into its own transform, which then
   applies the offset twice (it put the frame 480px left and 162px up, two
   dots off-screen). The same rule holds for the line and the name below. */
.sl-marks {
  position: absolute;
  inset: 0;
  margin: auto;
  width: min(72vw, 960px);
  height: 36vh;
}
.sl-marks i {
  position: absolute;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-cream) 56%, transparent);
}
.sl-marks i:nth-child(1) { left: 0; top: 0; }
.sl-marks i:nth-child(2) { right: 0; top: 0; }
.sl-marks i:nth-child(3) { left: 0; bottom: 0; }
.sl-marks i:nth-child(4) { right: 0; bottom: 0; }

.sl-words, .sl-name {
  position: absolute;
  inset: 0;
  margin: 0;
  text-align: center;
  color: var(--color-cream);
}
/* The line: its words are flex items, so the gap stands in for spaces. */
.sl-words {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.26em;
}
.sl-name { display: grid; place-items: center; }
/* Sized to the reference: there the line spans ~30% of the screen's width,
   about 430px at 1440, which is --text-case-h2 (48px at 1440). Set in Clash
   Display 500 on instruction, the same face and weight as the name it turns
   into. Words start display: none; loader.js shows them one at a time so
   the line stays centred on what is there. */
.sl-words {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--text-case-h2);
  line-height: 1.1;
  letter-spacing: -0.02em;
}
.sl-words span { display: none; }
/* The name: the onboarding case study's section-head type (.cs-h2: Clash
   Display 500, 1.1 leading, -0.02em), a quarter larger on instruction, so
   60px at 1440 against the line's 48. */
.sl-name {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: calc(var(--text-case-h2) * 1.25);
  line-height: 1.1;
  letter-spacing: -0.02em;
  opacity: 0;
}
/* Tiles: the hero card's system, without its content. Same proportions as
   .g-card (280 / 408), its 14px corner, and the Industries card's surface:
   --color-charcoal under the .g-field dot grid (cream at 22%, 1px dots on
   the 16px tile, offset 8px), at the same scale, so a loader card reads as
   one of the hero's cards seen in passing.

   The tone: each card runs top to bottom from its base into a deeper shade
   of it, and its base is set by --tile-depth (loader.js), 0 for the
   Industries charcoal and 1 for the deepest card in a stream. Both darken
   toward #101115, a near-black with the charcoal's own blue in it, and
   never past 30% of the way there, so the deepest card is still a rich
   charcoal rather than black. The 1px edge is cream at 6%: the loader's
   ground is --color-dark, and without it the deepest cards would dissolve
   into it. */
.sl-tile {
  --tile-base: color-mix(in srgb, var(--color-charcoal) calc(100% - var(--tile-depth, 0) * 30%), #101115);
  --tile-deep: color-mix(in srgb, var(--tile-base) 78%, #101115);
  position: absolute;
  left: 0; top: 0;
  width: 14vw;
  aspect-ratio: 280 / 408;
  border-radius: 14px;
  background-color: var(--tile-base);
  background-image:
    radial-gradient(circle, color-mix(in srgb, var(--color-cream) 22%, transparent) 1px, transparent 1px),
    linear-gradient(180deg, var(--tile-base), var(--tile-deep));
  background-size: 16px 16px, 100% 100%;
  background-position: 8px 8px, 0 0;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-cream) 6%, transparent),
              0 20px 48px -16px rgba(0, 0, 0, .6);
  opacity: 0;
  will-change: transform, opacity;
}
/* The right stream's first card: the hero's photo card (.g-media img and
   .g-title), drawn at the tile's size. The title keeps the hero's
   proportions, 22px on a 256px card, by sizing against the tile's width. */
.sl-tile--photo {
  container-type: inline-size;
  overflow: hidden;
  background: var(--color-charcoal);
  box-shadow: 0 20px 48px -16px rgba(0, 0, 0, .6);
}
.sl-tile--photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sl-tile-title {
  position: absolute; left: 0; right: 0;
  top: 88%;
  transform: translateY(-50%);
  text-align: center;
  font-family: 'Clash Display Variable', var(--font-display);
  font-weight: 600;
  font-size: calc(100cqw * 22 / 256);
  line-height: 1;
  letter-spacing: -0.01em;
  word-spacing: .14em;
  white-space: nowrap;
  color: var(--color-cream);
  text-shadow: 0 8px 24px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.2);
}

/* ─── Prism folder (#visual) ───────────────────────────────────────────────
   The visual design section, right after the work. Recreated from the Framer
   component "Prism Folder" (a supplied recording), in --color-green instead
   of blue and centred, on instruction. Markup and behaviour are built by
   prism-folder.js; the flap's light is its WebGL2 canvas, laid over the CSS
   glass below.

   Sizes are the recording's, read off it at 1x: a 308px folder, a 116px
   tab, a 184px glass flap, and pictures 148 x 112 peeking out. */
#visual.section { padding-block: 0; }
.prism-folder {
  --pf-color: var(--color-green);
  --pf-window: #fff;
  --pf-back:  color-mix(in srgb, var(--pf-color) 80%, #0d1f14);
  --pf-front: color-mix(in srgb, var(--pf-color) 70%, #fff);
  width: 100%;
}
.pf-stage {
  position: relative;
  /* The whole viewport, edge to edge: the windows spread right out to it. */
  margin-inline: calc(-1 * var(--page-margin));
  height: 100svh;
  display: grid;
  place-items: center;
}
.pf-anchor {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

/* The folder. A button, so it takes focus and Enter/Space open it. */
.pf-folder {
  position: relative;
  width: 308px;
  height: 264px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.pf-folder:focus-visible { outline: 2px solid var(--color-dark); outline-offset: 8px; border-radius: 20px; }
.pf-folder > span { position: absolute; left: 0; right: 0; display: block; }
.pf-tab {
  top: 0;
  width: 116px;
  height: 48px;
  border-radius: 14px;
  background: var(--pf-back);
}
.pf-back {
  top: 32px;
  bottom: 0;
  border-radius: 16px;
  background: var(--pf-back);
  box-shadow: 0 24px 48px -24px rgba(0, 0, 0, .35);
}

/* The first pictures, fanned in the folder's mouth. They rise a little on
   hover, and leave when the folder opens (the windows carry them out). */
.pf-peek { top: 36px; height: 160px; }
.pf-peek-item {
  display: block;
  position: absolute;
  left: 0; top: 0;
  /* The whole picture, scaled down: --w and --h come from its own ratio
     (prism-folder.js, layoutPeek), so nothing is cropped. */
  width: var(--w, 148px); height: var(--h, 112px);
  object-fit: contain;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, .6);
  box-shadow: 0 8px 20px -8px rgba(0, 0, 0, .4);
  transform: translate(var(--x), var(--y)) rotate(var(--r));
  transition: transform .5s cubic-bezier(.34, 1.4, .64, 1), opacity .25s ease;
}
.pf-folder:hover .pf-peek-item,
.pf-folder:focus-visible .pf-peek-item {
  transform: translate(var(--x), calc(var(--y) - 12px)) rotate(calc(var(--r) * 1.4));
}
.is-open .pf-peek-item {
  opacity: 0;
  transform: translate(var(--x), calc(var(--y) + 24px)) rotate(var(--r)) scale(.9);
  transition-duration: .25s;
}

/* The flap: tinted glass. The pictures behind it blur through; it is
   clearer at the top, where it catches the light, and denser toward the
   foot. The edge carries a fine light ring and a top highlight; the
   WebGL2 canvas adds the moving light on top. */
.pf-flap {
  bottom: 0;
  height: 184px;
  border-radius: 16px;
  overflow: hidden;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--pf-front) 34%, transparent) 0%,
    color-mix(in srgb, var(--pf-front) 58%, transparent) 36%,
    color-mix(in srgb, var(--pf-color) 86%, transparent) 100%);
  -webkit-backdrop-filter: blur(3px) saturate(130%);
          backdrop-filter: blur(3px) saturate(130%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .6),
    inset 0 0 0 1.5px rgba(255, 255, 255, .3),
    inset 0 -2px 0 rgba(0, 0, 0, .06),
    0 12px 28px -14px rgba(0, 0, 0, .4);
  transition: transform .5s cubic-bezier(.34, 1.4, .64, 1);
}
.pf-folder:hover .pf-flap { transform: translateY(1px); }
.pf-gl { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; }

.pf-label {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-family: var(--font-ui);
  transition: opacity .25s ease;
}
.pf-title { font-family: var(--font-display); font-size: calc(var(--text-meta) + 8px); font-weight: 500; color: var(--color-dark); letter-spacing: -0.01em; }
.pf-count { font-size: var(--text-label); color: color-mix(in srgb, var(--color-dark) 55%, transparent); }
.is-open .pf-label { opacity: 0; }

/* The hint, after the pictures land: a quiet line in the label's place,
   muted and small, that fades up and away. Never a chip or a pop-up. */
.pf-hint {
  position: absolute;
  left: 50%;
  top: 0;
  margin: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  translate: -50% 4px;
  font-family: var(--font-ui);
  font-size: var(--text-label);
  color: color-mix(in srgb, var(--color-dark) 50%, transparent);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .6s ease, translate .6s cubic-bezier(.22, 1, .36, 1);
}
.pf-hint.is-on { opacity: 1; translate: -50% 0; }

/* The windows: the picture in a plain white frame with the project
   cards' corner radius (--radius, as .pro-card), untilted, spread round
   the folder. Dragged from anywhere; a press brings one to the front. */
.pf-windows { position: absolute; inset: 0; pointer-events: none; }
.pf-win {
  position: absolute;
  /* No frame (on instruction): the picture itself, 16px corners and a soft shadow. */
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .08), 0 12px 28px -10px rgba(0, 0, 0, .28);
  pointer-events: auto;
  cursor: grab;
  /* Hover lifts a picture a little (its own scale, so it composes with the
     fly-in's transform), on a light spring. */
  transition: scale .45s cubic-bezier(.34, 1.56, .64, 1), box-shadow .45s ease;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}
@media (hover: hover) {
  .pf-win:hover { scale: 1.05; box-shadow: 0 1px 2px rgba(0, 0, 0, .1), 0 20px 40px -12px rgba(0, 0, 0, .34); }
}
.pf-win.is-dragging { cursor: grabbing; box-shadow: 0 1px 2px rgba(0, 0, 0, .1), 0 20px 40px -12px rgba(0, 0, 0, .34); }
.pf-media {
  display: block;
  width: var(--pf-img-w, 120px);
  height: var(--pf-img-h, 150px);
  object-fit: cover;
  border-radius: var(--radius-lg);
  -webkit-user-drag: none;
  pointer-events: none;
}

/* A picture with a hover animation: its canvas sits over the picture (or,
   for the orb, over just the globe, positioned in % by prism-folder.js). */
.pf-fx { position: relative; display: block; }
.pf-fxc { position: absolute; pointer-events: none; }
.pf-fxc--full { inset: 0; width: 100%; height: 100%; border-radius: var(--radius-lg); }
.pf-fxc--orb { aspect-ratio: 1; height: auto; }

/* Placeholder pictures, until the real ones arrive: a soft tinted block of
   the picture's shape, with its name. */
.pf-ph {
  display: grid;
  place-items: center;
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--pf-color) 14%, var(--color-cream)), color-mix(in srgb, var(--pf-color) 30%, var(--color-cream)));
  color: color-mix(in srgb, var(--color-dark) 55%, transparent);
  font-family: var(--font-ui);
  font-size: var(--text-label);
}
.pf-peek-item.pf-ph { aspect-ratio: auto; }

@media (max-width: 640px) {
  .pf-stage { height: 100svh; }
  /* Open on a phone, the pictures fill the stage and the folder steps back
     behind them (prism-folder.js lays them out in two columns). */
  .is-open .pf-anchor { opacity: .18; transition: opacity .3s ease; }
  .pf-folder { width: 252px; height: 216px; }
  .pf-tab { width: 96px; height: 40px; }
  .pf-back { top: 26px; }
  .pf-peek { top: 30px; }
  .pf-flap { height: 152px; }
}

@media (prefers-reduced-motion: reduce) {
  .pf-peek-item, .pf-flap, .pf-label { transition: none; }
}

/* ─── Phone fixes (mobile audit, 2026-10-13) ────────────────────────────────
   Phones only (640px and below); desktop is untouched. Kept together and
   last in the file so they win over the rules they adjust.

   1. Headings. The landing headings share --landing-h-size, dialled to a
      fixed 56px for desktop. On a phone that ran the testimonials heading to
      five lines (starting under the nav) and the hero headline was capped to
      its column at a small 25px. One phone size for all three, and the hero's
      three sentences each on their own line(s), not run together mid-phrase.
      The eyebrow comes down to sit under the headline in scale.
   2. The folder section's stage and the testimonials strip bleed to the
      screen edges by --page-margin (31px at 375), while a section's own
      padding here is 6vw (22.5px), so both poked 9px past each edge and made
      the whole page scroll sideways. They bleed by 6vw instead.
   3. The project filter pills: four on one row instead of three and an
      orphan. */
@media (max-width: 640px) {
  :root {
    --landing-h-size: clamp(30px, 8.6vw, 36px);
    --text-eyebrow: 22px;
  }
  .g-head { font-size: var(--landing-h-size); line-height: 1.12; }
  .g-line { display: block; text-wrap: balance; }   /* a long sentence splits evenly, never leaving one word alone */
  .g-line:nth-of-type(2)::after { content: none; }

  .pf-stage { margin-inline: -6vw; }
  /* Same overreach on the testimonials strip, which bleeds by --page-margin. */
  .tm-viewport { width: calc(100% + 12vw); margin-left: -6vw; }

  .pro-filters { gap: 6px; }
  .pro-filter { padding: 10px 14px; }
}
