/*!
 * Yooku Obiri — Brand Designer
 * GENERATED FILE — do not edit.
 * Edit /css/partials/*.css and run `node build.js`.
 * Built 2026-07-27T17:08:18.967Z
 * Sources: 01-tokens.css, 02-reset.css, 03-base.css, 04-layout.css, 05-components.css, 06-sections.css, 07-utilities.css
 */

/* ===== 01-tokens.css =============================================== */
/* ==========================================================================
   01 — TOKENS
   Single source of truth. Nothing below this file should hard-code a value.
   ========================================================================== */

:root {
  /* ---------------------------------------------------------------------
     COLOR — monochrome ramp + one accent.
     The ramp is intentionally not "pure grey at even steps"; the dark end is
     compressed so near-blacks stay separable on cheap laptop panels.
     --------------------------------------------------------------------- */
  --c-black: #000000;
  --c-ink-900: #0a0a0a; /* page background (dark) — softer than pure black */
  --c-ink-800: #141414;
  --c-ink-700: #1f1f1f;
  --c-ink-600: #2e2e2e;
  --c-ink-500: #4a4a4a;
  --c-ink-400: #6e6e6e; /* 4.5:1 on white — lowest safe body grey on light */
  --c-ink-300: #9a9a9a; /* 4.5:1 on --c-ink-900 — lowest safe body grey on dark */
  --c-ink-200: #c9c9c9;
  --c-ink-100: #e6e6e6;
  --c-ink-050: #f4f4f4;
  --c-ink-025: #f9f9f9;
  --c-paper: #eeeeee; /* 238 — chosen to equal the FLOOR of the illustration
                         video's decoded ground (measured 238 across the whole
                         clip). The figure blends with `darken`, so min(238,
                         >=238) resolves to exactly this value and the video's
                         opaque plate is mathematically invisible. Raising this
                         reveals the plate as a lighter rectangle. */
  --c-white: #ffffff;

  /* ACCENT
     Constraint: 4.5:1 against BOTH #fff and #000, so one accent works on
     either surface without a light/dark variant swap.
     That window is tiny (relative luminance must sit ~0.175–0.183).
       #008846 → 4.55:1 on white, 4.61:1 on black.  ✅ both
     Variants below are for *known* backgrounds only (hover, large display
     type, decorative fills) — they do not pass on both surfaces. */
  --c-accent: #008846;
  --c-accent-bright: #00b85e; /* on dark surfaces only (7.0:1 on #0a0a0a) */
  --c-accent-deep: #00622f; /* on light surfaces only (7.4:1 on #fff) */
  --c-accent-wash: rgba(0, 136, 70, 0.12); /* tints, focus halos, hover fills */

  /* Semantic aliases — components use these, never the raw ramp.
     DEFAULT IS LIGHT. The reference direction is near-black type on an
     off-white ground; dark is the override, not the baseline. */
  --c-bg: var(--c-paper);
  --c-bg-raised: var(--c-ink-025);
  --c-fg: var(--c-ink-900);
  --c-fg-muted: var(--c-ink-400);
  --c-line: var(--c-ink-200);
  --c-focus: var(--c-accent-deep);
  --c-curtain: var(--c-ink-900); /* page-transition wipe */
  --c-selection-bg: var(--c-accent);
  --c-selection-fg: var(--c-white);

  /* =====================================================================
     TYPE  —  ★ FONT SWAP HAPPENS HERE AND NOWHERE ELSE ★

     To change a typeface you edit exactly TWO things:
       1. --font-family-display / --font-family-body below
       2. the Google Fonts <link> in each HTML <head>
          (one line, identical on every page — see README "Changing fonts")

     Nothing else in the codebase names a font. Every heading, label and
     paragraph resolves through --font-display or --font-sans.

     CURRENT PICK
       Display : DM Sans   — geometric, low-contrast grotesk. The closest
                             free match to the reference's Neue-Montreal-ish
                             feel: tight apertures, near-circular bowls,
                             holds up at 11rem without looking like Helvetica.
       Body    : DM Sans   — same family. One typeface, hierarchy from
                             weight and tracking. Fewer files, more discipline.

     DROP-IN ALTERNATIVES (all Google, all variable, all one-line swaps):
       Space Grotesk  more character, quirky 'g' — good if you want edge
       General Sans   (Fontshare, not Google) — closest of all to the ref
       Archivo        wider, has a true Expanded axis for display
       Inter          the safe default; more neutral, less personality
     ===================================================================== */
  --font-family-display: "DM Sans";
  --font-family-body: "DM Sans";

  --font-stack-fallback: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;

  --font-display: var(--font-family-display), var(--font-stack-fallback);
  --font-sans: var(--font-family-body), var(--font-stack-fallback);
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;

  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  /* Fluid scale. Ratio ~1.25 mobile / ~1.333 desktop, clamped so the top end
     never outruns the viewport. Base stays 16px at every width — only the
     display sizes flex. */
  --fs-100: 0.75rem; /* 12 — eyebrow / meta / labels */
  --fs-200: 0.875rem; /* 14 — captions, nav */
  --fs-300: 1rem; /* 16 — body */
  --fs-400: clamp(1.125rem, 0.5vw + 1rem, 1.25rem); /* lead paragraph */
  --fs-500: clamp(1.5rem, 1.2vw + 1.1rem, 2rem); /* h4 / large lead */
  --fs-600: clamp(1.875rem, 2vw + 1.2rem, 2.75rem); /* h3 */
  --fs-700: clamp(2.25rem, 3.5vw + 1.2rem, 4rem); /* h2 / section head */
  --fs-800: clamp(3rem, 7vw + 0.5rem, 6.5rem); /* h1 */
  --fs-900: clamp(2.75rem, 12vw, 11rem); /* hero display — min sized so
                                            "Yooku Obiri" stays one line on a
                                            360px phone */

  --lh-tight: 0.95; /* display only */
  --lh-snug: 1.1;
  --lh-heading: 1.15;
  --lh-body: 1.6;
  --lh-loose: 1.75;

  --ls-display: -0.03em; /* big type needs negative tracking */
  --ls-heading: -0.015em;
  --ls-body: 0;
  --ls-label: 0.12em; /* uppercase eyebrows */

  --measure: 68ch; /* max readable line length */
  --measure-narrow: 46ch;

  /* ---------------------------------------------------------------------
     SPACE — 4px base, roughly geometric so vertical rhythm stays coarse.
     --------------------------------------------------------------------- */
  --sp-3xs: 0.25rem; /* 4 */
  --sp-2xs: 0.5rem; /* 8 */
  --sp-xs: 0.75rem; /* 12 */
  --sp-sm: 1rem; /* 16 */
  --sp-md: 1.5rem; /* 24 */
  --sp-lg: 2.5rem; /* 40 */
  --sp-xl: 4rem; /* 64 */
  --sp-2xl: 6rem; /* 96 */
  --sp-3xl: 9rem; /* 144 */
  --sp-4xl: 14rem; /* 224 */

  /* Fluid section rhythm — the only vertical spacing that should scale. */
  --sp-section: clamp(4rem, 10vw, 12rem);
  --sp-section-tight: clamp(2.5rem, 6vw, 6rem);

  /* ---------------------------------------------------------------------
     LAYOUT
     --------------------------------------------------------------------- */
  --container-max: 90rem; /* 1440 */
  --container-text: 46rem; /* prose column */
  --gutter: clamp(1.25rem, 4vw, 4rem);
  --grid-columns: 4; /* mobile default; layout partial bumps this up */
  --grid-gap: clamp(1rem, 2vw, 2rem);

  --header-height: 4.5rem;
  --border-width: 1px;
  --radius-none: 0; /* the design language is square. Kept as a token so a
                       future rebrand is one line, not a find-and-replace. */
  --radius-sm: 2px;
  --radius-pill: 999px;

  /* ---------------------------------------------------------------------
     MOTION — GSAP reads these via getComputedStyle in gsap-setup.js so JS and
     CSS transitions stay in sync.
     --------------------------------------------------------------------- */
  --dur-instant: 0.12s;
  --dur-fast: 0.25s;
  --dur-base: 0.45s;
  --dur-slow: 0.8s;
  --dur-transition-out: 0.5s; /* page leave */
  --dur-transition-in: 0.7s; /* page enter */

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* expo-ish out — the house ease */
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-linear: linear;

  /* ---------------------------------------------------------------------
     Z-INDEX — named, so nothing is ever "z-index: 9999".
     --------------------------------------------------------------------- */
  --z-canvas: 0; /* persistent WebGL layer, behind everything */
  --z-content: 1;
  --z-header: 100;
  --z-overlay: 200; /* page-transition curtain */
  --z-cursor: 300; /* always on top */
  --z-skip-link: 400;
}

/* --------------------------------------------------------------------------
   THEME OVERRIDE — dark surfaces.
   Applied per-section (<section data-theme="dark">) or per-page on <body>.
   Only semantic aliases flip; the ramp never changes.
   -------------------------------------------------------------------------- */
[data-theme="dark"] {
  --c-bg: var(--c-ink-900);
  --c-bg-raised: var(--c-ink-800);
  --c-fg: var(--c-white);
  --c-fg-muted: var(--c-ink-300);
  --c-line: var(--c-ink-700);
  --c-focus: var(--c-accent-bright);
  --c-curtain: var(--c-paper);
}

/* Explicit light, for flipping a single section back inside a dark page. */
[data-theme="light"] {
  --c-bg: var(--c-paper);
  --c-bg-raised: var(--c-ink-025);
  --c-fg: var(--c-ink-900);
  --c-fg-muted: var(--c-ink-400);
  --c-line: var(--c-ink-200);
  --c-focus: var(--c-accent-deep);
  --c-curtain: var(--c-ink-900);
}

/* --------------------------------------------------------------------------
   REDUCED MOTION — the class is set on <html> by motion-preferences.js, and
   the media query is the no-JS backstop. Durations collapse to ~0 so any
   CSS transition we forgot to guard becomes an instant state change.
   -------------------------------------------------------------------------- */
.reduced-motion {
  --dur-instant: 0.01ms;
  --dur-fast: 0.01ms;
  --dur-base: 0.01ms;
  --dur-slow: 0.01ms;
  --dur-transition-out: 0.01ms;
  --dur-transition-in: 0.01ms;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-instant: 0.01ms;
    --dur-fast: 0.01ms;
    --dur-base: 0.01ms;
    --dur-slow: 0.01ms;
    --dur-transition-out: 0.01ms;
    --dur-transition-in: 0.01ms;
  }
}


/* ===== 02-reset.css ================================================ */
/* ==========================================================================
   02 — RESET
   Opinionated, minimal. Not normalize.css — only what this site actually needs.
   ========================================================================== */

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

* {
  margin: 0;
  padding: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  -moz-tab-size: 2;
  tab-size: 2;
}

body {
  min-height: 100svh;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Media defaults to block so it never sits on a text baseline. */
img,
picture,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
}

img,
video {
  height: auto;
}

/* Form controls do not inherit type by default. Make them. */
input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  background: none;
  border: none;
}

button {
  cursor: pointer;
}

textarea {
  resize: vertical;
}

/* Avoid overflow from long unbroken strings (URLs in case-study copy). */
p,
h1,
h2,
h3,
h4,
h5,
h6,
li,
dd,
dt {
  overflow-wrap: break-word;
}

ul[role="list"],
ol[role="list"] {
  list-style: none;
}

a {
  color: inherit;
  text-decoration: none;
}

table {
  border-collapse: collapse;
}

/* Anchor scrolling accounts for the fixed header. Disabled under reduced
   motion (see tokens) because smooth scroll is itself vestibular motion. */
:where(html:not(.reduced-motion)) {
  scroll-behavior: smooth;
}

html {
  scroll-padding-top: calc(var(--header-height) + var(--sp-md));
}

::selection {
  background-color: var(--c-selection-bg);
  color: var(--c-selection-fg);
}


/* ===== 03-base.css ================================================= */
/* ==========================================================================
   03 — BASE
   Element-level typography and document defaults. No classes here except the
   two accessibility primitives (.skip-link, .visually-hidden).
   ========================================================================== */

html {
  font-family: var(--font-sans);
  font-size: 100%; /* respect the user's browser setting — never px here */
  background-color: var(--c-bg);
  color: var(--c-fg);
}

body {
  font-size: var(--fs-300);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-body);
  font-weight: var(--fw-regular);
  background-color: var(--c-bg);
  color: var(--c-fg);
  /* The persistent WebGL canvas is fixed behind content; body must not paint
     over it, so page-level backgrounds live on sections, not here.
     `transparent` is set by the .has-webgl class in 05-components. */
  transition: background-color var(--dur-base) var(--ease-out),
    color var(--dur-base) var(--ease-out);
}

/* --------------------------------------------------------------------------
   HEADINGS — tight, negative-tracked, weight-driven hierarchy.
   -------------------------------------------------------------------------- */
h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-heading);
  text-wrap: balance;
}

h1 {
  font-size: var(--fs-800);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-display);
  font-weight: var(--fw-bold);
}

h2 {
  font-size: var(--fs-700);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-display);
}

h3 {
  font-size: var(--fs-600);
}

h4 {
  font-size: var(--fs-500);
}

h5 {
  font-size: var(--fs-400);
}

h6 {
  font-size: var(--fs-200);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: var(--ls-label);
}

p {
  max-width: var(--measure);
  text-wrap: pretty;
}

/* --------------------------------------------------------------------------
   LINKS — inline links in PROSE get an underline.

   `a:not([class])` is the opt-out mechanism: a link with a class is a
   component (card, button, nav item) and owns its own styling, so it never
   picks up prose decoration just because it happens to sit inside a <li>.
   Unclassed links are, by definition, body copy.
   -------------------------------------------------------------------------- */
:where(main p, main li) a:not([class]) {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--c-accent);
  transition: color var(--dur-fast) var(--ease-out),
    text-decoration-color var(--dur-fast) var(--ease-out);
}

:where(main p, main li) a:not([class]):hover {
  color: var(--c-accent);
}

/* --------------------------------------------------------------------------
   FOCUS — visible, high contrast, never removed.
   :focus-visible only, so mouse users don't see rings, but the fallback below
   covers browsers without :focus-visible support.
   -------------------------------------------------------------------------- */
:focus {
  outline: 2px solid var(--c-focus);
  outline-offset: 3px;
}

:focus:not(:focus-visible) {
  outline: none;
}

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

/* --------------------------------------------------------------------------
   ACCESSIBILITY PRIMITIVES
   -------------------------------------------------------------------------- */
.skip-link {
  position: fixed;
  top: var(--sp-2xs);
  left: var(--sp-2xs);
  z-index: var(--z-skip-link);
  padding: var(--sp-2xs) var(--sp-sm);
  background-color: var(--c-white);
  color: var(--c-ink-900);
  font-size: var(--fs-200);
  font-weight: var(--fw-medium);
  /* Off-screen until focused — kept in the a11y tree, unlike display:none. */
  transform: translateY(calc(-100% - var(--sp-sm)));
  transition: transform var(--dur-fast) var(--ease-out);
}

.skip-link:focus {
  transform: translateY(0);
}

.visually-hidden:not(:focus):not(:active) {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Programmatic focus target after a page transition. transition-manager.js
   focuses <main tabindex="-1">; suppress the ring there since the user did
   not tab to it — the page change is already visible. */
main:focus {
  outline: none;
}


/* ===== 04-layout.css =============================================== */
/* ==========================================================================
   04 — LAYOUT
   Structural primitives only. Mobile-first; breakpoints below.

   BREAKPOINTS (em-based so they respect browser zoom):
     sm   40em   640px   large phone / small tablet
     md   48em   768px   tablet portrait
     lg   64em  1024px   tablet landscape / small laptop  <- main layout shift
     xl   80em  1280px   desktop
     2xl 100em  1600px   large desktop
   ========================================================================== */

/* --------------------------------------------------------------------------
   CONTAINER
   -------------------------------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

.container--flush {
  padding-inline: 0;
}

/* --------------------------------------------------------------------------
   SECTION — the vertical rhythm unit. Sections own their background so the
   persistent canvas can show through where they don't.
   -------------------------------------------------------------------------- */
.section {
  position: relative;
  z-index: var(--z-content);
  padding-block: var(--sp-section);
}

.section--tight {
  padding-block: var(--sp-section-tight);
}

.section--flush {
  padding-block: 0;
}

/* Opaque sections hide the WebGL layer behind them; transparent ones reveal
   it. Defaulting to transparent keeps the canvas the visual base layer. */
.section--solid {
  background-color: var(--c-bg);
}

/* --------------------------------------------------------------------------
   GRID — a single 4/8/12-column grid used site-wide.
   -------------------------------------------------------------------------- */
.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
  gap: var(--grid-gap);
}

@media (min-width: 48em) {
  :root {
    --grid-columns: 8;
  }
}

@media (min-width: 64em) {
  :root {
    --grid-columns: 12;
  }
}

/* Column span helpers. Mobile: everything is full width by default, so only
   the wider breakpoints need spans. */
.grid > * {
  grid-column: span var(--grid-columns);
}

@media (min-width: 64em) {
  .col-3 {
    grid-column: span 3;
  }
  .col-4 {
    grid-column: span 4;
  }
  .col-5 {
    grid-column: span 5;
  }
  .col-6 {
    grid-column: span 6;
  }
  .col-7 {
    grid-column: span 7;
  }
  .col-8 {
    grid-column: span 8;
  }
  .col-9 {
    grid-column: span 9;
  }
  .col-12 {
    grid-column: span 12;
  }
  .col-start-2 {
    grid-column-start: 2;
  }
  .col-start-4 {
    grid-column-start: 4;
  }
  .col-start-7 {
    grid-column-start: 7;
  }
}

/* --------------------------------------------------------------------------
   STACK / CLUSTER — the two flow primitives that replace ad-hoc margins.
   -------------------------------------------------------------------------- */
.stack > * + * {
  margin-block-start: var(--stack-space, var(--sp-md));
}

.stack--sm {
  --stack-space: var(--sp-2xs);
}
.stack--lg {
  --stack-space: var(--sp-lg);
}
.stack--xl {
  --stack-space: var(--sp-xl);
}

.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--cluster-space, var(--sp-sm));
}

.cluster--between {
  justify-content: space-between;
}

/* --------------------------------------------------------------------------
   PAGE SHELL — header / main / footer scaffolding.
   -------------------------------------------------------------------------- */
.page {
  display: flex;
  flex-direction: column;
  min-height: 100svh;
  position: relative;
  z-index: var(--z-content);
}

.page__main {
  flex: 1 0 auto;
}

.site-header {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: var(--z-header);
  min-height: var(--header-height);
  display: flex;
  align-items: center;
  /* No background by default — the canvas reads through. A scrolled state
     class can add one later. */
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-md);
  width: 100%;
}

.site-footer {
  border-block-start: var(--border-width) solid var(--c-line);
  padding-block: var(--sp-xl);
}

/* --------------------------------------------------------------------------
   ScrollSmoother requires this exact wrapper/content nesting. Both elements
   exist in every page shell even when smoothing is disabled, so the DOM
   shape does not change between capability tiers.
   -------------------------------------------------------------------------- */
#smooth-wrapper {
  position: relative;
}

#smooth-content {
  will-change: transform;
}

/* When smoothing is off (reduced motion / low tier), drop the will-change so
   we are not promoting a full-page layer for nothing. */
.reduced-motion #smooth-content,
.no-smooth-scroll #smooth-content {
  will-change: auto;
}


/* ===== 05-components.css =========================================== */
/* ==========================================================================
   05 — COMPONENTS
   Structural basics only: the engine's own UI (canvas layer, cursor,
   transition curtain) plus bare nav/button skeletons.
   No case-study, card, or hero styling yet — that arrives with the creative.
   ========================================================================== */

/* --------------------------------------------------------------------------
   PERSISTENT WEBGL LAYER
   One fixed canvas, created once, never re-created across page transitions.
   -------------------------------------------------------------------------- */
.webgl-layer {
  position: fixed;
  inset: 0;
  z-index: var(--z-canvas);
  pointer-events: none; /* never steals clicks; Observer listens on window */
  /* CSS fallback is painted here and simply stays visible if WebGL is off. */
  background-color: var(--c-bg);
}

.webgl-layer canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Fade the canvas in once the first frame has rendered, so a black flash or
   a half-compiled shader is never visible. scene-manager.js adds .is-ready. */
.webgl-layer canvas {
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}

.webgl-layer.is-ready canvas {
  opacity: 1;
}

/* FALLBACK — used when WebGL is unsupported, reduced-motion is set, or the
   capability tier is too low. Pure CSS, no image required, but a real cover
   image can be layered on top via --fallback-image. */
.webgl-layer--fallback {
  /* FLAT on purpose, for now. The home illustration is an opaque H.264 video
     blended with `darken`, which is only invisible against a uniform ground —
     any gradient here outlines it as a rectangle. Re-introduce a gradient
     (or a real shader) once the illustration is alpha-capable, or scope it to
     routes that do not carry the video. */
  background-color: var(--c-bg);
}

.webgl-layer--fallback[style*="--fallback-image"] {
  background-image: var(--fallback-image);
  background-size: cover;
  background-position: center;
}

/* When the canvas is live, the body must not paint over it. */
.has-webgl body {
  background-color: transparent;
}

/* --------------------------------------------------------------------------
   MAGNETIC CURSOR
   Two elements: a small dot that tracks 1:1, and a ring that lags and scales.
   Hidden entirely on touch / no-hover devices and under reduced motion.
   -------------------------------------------------------------------------- */
.cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: var(--z-cursor);
  pointer-events: none;
  will-change: transform;
  /* Positioned by GSAP quickTo via translate; centred on its own origin. */
  margin-block-start: -0.5px;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}

.cursor.is-active {
  opacity: 1;
}

.cursor__dot {
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: var(--radius-pill);
  background-color: var(--c-accent);
}

.cursor__ring {
  width: 40px;
  height: 40px;
  margin: -20px 0 0 -20px;
  border: var(--border-width) solid var(--c-ink-400);
  border-radius: var(--radius-pill);
  transition: background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

/* State classes are toggled by cursor.js, scaling is done in GSAP so the two
   systems never fight over `transform`. */
.cursor__ring.is-hovering {
  border-color: var(--c-accent);
  background-color: var(--c-accent-wash);
}

/* Hide the native cursor only when ours is genuinely running. */
.has-custom-cursor,
.has-custom-cursor a,
.has-custom-cursor button,
.has-custom-cursor [data-cursor] {
  cursor: none;
}

/* Kill switches — no custom cursor without a real pointer. */
@media (hover: none), (pointer: coarse) {
  .cursor {
    display: none;
  }
  .has-custom-cursor,
  .has-custom-cursor a,
  .has-custom-cursor button {
    cursor: auto;
  }
}

/* --------------------------------------------------------------------------
   PAGE-TRANSITION CURTAIN
   Owned by transition-manager.js. Inert and non-blocking at rest.
   -------------------------------------------------------------------------- */
.transition-curtain {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  pointer-events: none;
  background-color: var(--c-curtain);
  transform: scaleY(0);
  transform-origin: bottom center;
  visibility: hidden;
}

.transition-curtain.is-active {
  visibility: visible;
}

/* Non-visual signal for assistive tech / debugging while a route loads. */
.is-transitioning {
  cursor: progress;
}

/* --------------------------------------------------------------------------
   NAV — skeleton only.
   -------------------------------------------------------------------------- */
.site-nav__list {
  display: flex;
  align-items: center;
  gap: var(--sp-md);
  list-style: none;
}

.site-nav__link {
  display: inline-block;
  font-size: var(--fs-200);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-fg-muted);
  transition: color var(--dur-fast) var(--ease-out);
}

.site-nav__link:hover {
  color: var(--c-fg);
}

/* aria-current="page" is the single source of truth for the active route —
   transition-manager.js updates it after every swap. */
.site-nav__link[aria-current="page"] {
  color: var(--c-fg);
}

.site-nav__link[aria-current="page"]::after {
  content: "";
  display: block;
  height: 1px;
  margin-block-start: 2px;
  background-color: var(--c-accent);
}

.brand {
  font-size: var(--fs-200);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-heading);
  text-transform: uppercase;
}

/* --------------------------------------------------------------------------
   BUTTON / LINK PRIMITIVES — unstyled-but-structural.
   -------------------------------------------------------------------------- */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2xs);
  min-height: 2.75rem; /* 44px — touch target floor */
  padding: var(--sp-2xs) var(--sp-md);
  border: var(--border-width) solid currentColor;
  border-radius: var(--radius-none);
  font-size: var(--fs-200);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  transition: background-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.button--primary {
  background-color: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-white);
}

.button--primary:hover {
  background-color: var(--c-accent-bright);
  border-color: var(--c-accent-bright);
  color: var(--c-ink-900);
}

.button--ghost:hover {
  background-color: var(--c-fg);
  color: var(--c-bg);
}

.button[disabled],
.button[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
}

/* --------------------------------------------------------------------------
   MEDIA — figures and cover images used by the case-study template.
   -------------------------------------------------------------------------- */
.media {
  position: relative;
  overflow: hidden;
  background-color: var(--c-line); /* holds space before the image loads */
}

.media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.media--16x9 {
  aspect-ratio: 16 / 9;
}
.media--4x3 {
  aspect-ratio: 4 / 3;
}
.media--1x1 {
  aspect-ratio: 1 / 1;
}


/* ===== 06-sections.css ============================================= */
/* ==========================================================================
   06 — SECTIONS
   Page-level composition. Home page (hero / stage / projects / CTA) plus the
   real site header and mobile menu.

   Runs AFTER components so it can compose them, BEFORE utilities so a
   utility class still wins.
   ========================================================================== */

/* ==========================================================================
   SITE HEADER — logo left, nav right, menu button on small screens.
   ========================================================================== */

.site-header {
  padding-block: var(--sp-md);
  /* Non-blocking by default so the header never eats clicks on the hero;
     the children opt back in. */
  pointer-events: none;
}

.site-header > * {
  pointer-events: auto;
}

/* LOGO -------------------------------------------------------------------- */
.logo {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2xs);
  font-family: var(--font-display);
  font-size: var(--fs-400);
  font-weight: var(--fw-bold);
  letter-spacing: -0.04em;
  line-height: 1;
  color: var(--c-fg);
}

.logo__mark {
  /* The YO monogram. A real logotype drops in here later — keep the box
     dimensions and nothing else needs to move. */
  display: inline-block;
}

.logo__c {
  font-size: 0.55em;
  vertical-align: super;
  font-weight: var(--fw-medium);
  color: var(--c-fg-muted);
}

/* DESKTOP NAV ------------------------------------------------------------- */
.site-nav {
  display: none; /* mobile-first: the menu button is the default */
}

.site-nav__list {
  gap: var(--sp-lg);
}

.site-nav__link {
  position: relative;
  font-size: var(--fs-200);
  letter-spacing: 0.02em;
  text-transform: none; /* the reference uses sentence case, not caps */
  color: var(--c-fg);
}

/* Underline that wipes in from the left on hover. Uses a pseudo-element
   rather than text-decoration so the timing is controllable. */
.site-nav__link::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-end: -0.25em;
  height: 1px;
  background-color: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-fast) var(--ease-out);
}

.site-nav__link:hover::after,
.site-nav__link:focus-visible::after {
  transform: scaleX(1);
}

.site-nav__link[aria-current="page"]::after {
  transform: scaleX(1);
  background-color: var(--c-accent);
}

/* MENU BUTTON ------------------------------------------------------------- */
.menu-toggle {
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 2.75rem; /* 44px touch target */
  height: 2.75rem;
  align-items: flex-end;
  color: var(--c-fg);
}

.menu-toggle__bar {
  display: block;
  width: 22px;
  height: 1.5px;
  background-color: currentColor;
  transition: transform var(--dur-fast) var(--ease-out),
    opacity var(--dur-fast) var(--ease-out), width var(--dur-fast) var(--ease-out);
}

/* nth-child(2), not :last-child — the button's real last child is the
   .visually-hidden "Menu" span, so :last-child never matched the 2nd bar and
   the X only half-formed. */
.menu-toggle__bar:nth-child(2) {
  width: 14px;
}

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:first-child {
  transform: translateY(3.25px) rotate(45deg);
}

.menu-toggle[aria-expanded="true"] .menu-toggle__bar:nth-child(2) {
  width: 22px;
  transform: translateY(-3.25px) rotate(-45deg);
}

/* MOBILE MENU PANEL ------------------------------------------------------- */
.menu-panel {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-header) - 1);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--sp-2xl) var(--gutter) var(--sp-xl);
  background-color: var(--c-bg);
  /* `visibility` (not display) so the panel can be animated and so focus
     management has something real to move into. */
  visibility: hidden;
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out),
    visibility 0s linear var(--dur-base);
}

.menu-panel.is-open {
  visibility: visible;
  opacity: 1;
  transition-delay: 0s;
}

.menu-panel__list {
  list-style: none;
  display: grid;
  gap: var(--sp-sm);
}

.menu-panel__link {
  display: flex;
  align-items: baseline;
  justify-content: flex-end; /* right-align each item within its full-width row */
  gap: var(--sp-sm);
  font-family: var(--font-display);
  font-size: var(--fs-700);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-display);
  line-height: 1.1;
  color: var(--c-fg);
}

.menu-panel__index {
  font-family: var(--font-sans);
  font-size: var(--fs-100);
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-label);
  color: var(--c-fg-muted);
}

.menu-panel__footer {
  margin-block-start: var(--sp-2xl);
  font-size: var(--fs-200);
  color: var(--c-fg-muted);
  text-align: right;
}

/* CLOSE (X) — injected into the panel by nav.js (the header toggle is covered
   by the opaque panel once open, so it can't close it). Sits where the
   hamburger is, so opening reads as the icon becoming an X. */
.menu-panel__close {
  position: absolute;
  inset-block-start: var(--sp-md);
  inset-inline-end: var(--gutter);
  display: grid;
  place-items: center;
  width: 2.75rem; /* 44px touch target, matches the toggle */
  height: 2.75rem;
  color: var(--c-fg);
}

.menu-panel__close-bar {
  grid-area: 1 / 1; /* stack both bars on one cell, then cross them */
  width: 22px;
  height: 1.5px;
  background-color: currentColor;
  transition: transform var(--dur-fast) var(--ease-out);
}

.menu-panel__close-bar:first-child {
  transform: rotate(45deg);
}

.menu-panel__close-bar:last-child {
  transform: rotate(-45deg);
}

/* Quarter-turn on hover — small, deliberate. */
.menu-panel__close:hover .menu-panel__close-bar:first-child,
.menu-panel__close:focus-visible .menu-panel__close-bar:first-child {
  transform: rotate(135deg);
}

.menu-panel__close:hover .menu-panel__close-bar:last-child,
.menu-panel__close:focus-visible .menu-panel__close-bar:last-child {
  transform: rotate(45deg);
}

/* Lock scroll behind the open panel without the iOS jump-to-top bug. */
.menu-open {
  overflow: hidden;
}

@media (min-width: 64em) {
  .site-nav {
    display: block;
  }
  .menu-toggle,
  .menu-panel {
    display: none;
  }
}

/* ==========================================================================
   HOME — STAGE
   Hero and about share ONE section. The video travels from centre (hero) to
   the right (about) as you scroll, revealing the about copy on the left.

   BASELINE (mobile, no-JS, reduced motion): a plain stacked, centred column —
   name, role, video, caption, about copy. Everything visible, nothing pinned.
   The pin + travel is added only on desktop with motion allowed (home.js).
   ========================================================================== */

.stage {
  position: relative;
  z-index: var(--z-content);
}

.stage__pin {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-xl);
  text-align: center;
  padding-block: calc(var(--header-height) + var(--sp-xl)) var(--sp-2xl);
}

.stage__hero {
  width: 100%;
}

.stage__figure {
  width: min(100%, 30rem);
}

.stage__about {
  text-align: start; /* paragraphs read left-aligned even in the centred stack */
}

/* Arrow is a desktop scroll affordance for the travel; hidden in the stack. */
.stage .scroll-cue {
  display: none;
}

/* ---- DESKTOP: pin + travel ----------------------------------------------
   From 64em up, with JS and motion allowed. The pin is a two-column END state
   (about left, video right, vertically centred); home.js starts the video in
   the centre with the about hidden, and scrubs to this resting layout. If JS
   never runs, none of this applies and the stack above is what shows. */
@media (min-width: 64em) {
  .js:not(.reduced-motion) .stage {
    /* Scroll length of the travel. ~1.4 viewports reads as deliberate. */
    height: 240vh;
  }

  .js:not(.reduced-motion) .stage__pin {
    position: sticky;
    inset-block-start: 0;
    height: 100svh;
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    align-content: center;
    gap: 0;
    padding-block: 0;
    text-align: start;
    overflow: hidden;
  }

  /* About: left column, vertically centred. */
  .js:not(.reduced-motion) .stage__about {
    grid-column: 1 / span 5;
    grid-row: 1;
    align-self: center;
  }

  /* Video: right column, vertically centred. home.js translates it to the
     viewport centre for the hero state, then back to here. */
  .js:not(.reduced-motion) .stage__figure {
    grid-column: 8 / -1;
    grid-row: 1;
    align-self: center;
    width: 100%;
    margin: 0;
  }

  /* Hero name/role: a centred overlay pinned to the TOP of the viewport, so it
     sits above the video at rest. NOTE: no grid placement — an absolutely
     positioned grid item is contained by its (vertically-centred) grid cell,
     which would drop the name to mid-height right onto the video. Without a
     grid row its containing block is the pin's padding box, so inset-block-start
     measures from the top. home.js pushes the video down to clear it, and both
     fade/travel apart on scroll. */
  .js:not(.reduced-motion) .stage__hero {
    position: absolute;
    inset-block-start: calc(var(--header-height) + var(--sp-lg));
    inset-inline: 0;
    text-align: center;
    pointer-events: none;
  }

  /* Arrow: bottom-centre affordance. */
  .js:not(.reduced-motion) .stage .scroll-cue {
    display: flex;
    position: absolute;
    inset-inline: 0;
    inset-block-end: var(--sp-lg);
    justify-content: center;
    margin: 0;
  }
}

/* HERO TITLE -------------------------------------------------------------- */
.hero__title {
  font-family: var(--font-display);
  font-size: var(--fs-900);
  font-weight: var(--fw-medium);
  line-height: 0.9;
  letter-spacing: -0.045em;
  text-align: center;
  /* white-space is the reliable way to keep "Yooku Obiri" on one line; the
     masks are inline-block, and text-wrap:nowrap does not always suppress the
     break opportunity at the whitespace between them. */
  white-space: nowrap;
  margin: 0;
}

/* The mask clips its word so the intro reads as type being REVEALED rather
   than type sliding around. The padding/negative-margin pair stops the clip
   from cutting descenders — without it the 'y' in "Yooku" loses its tail. */
.hero__mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-block-end: 0.14em;
  margin-block-end: -0.14em;
}

/* Per-word spans exist so GSAP can stagger them. Inline-block is required
   for transforms to apply. */
.hero__word {
  display: inline-block;
  will-change: transform;
}

/* Flex column rather than text-align, so the uppercase tracking (which adds a
   trailing space after the last glyph) can't pull the line off-centre — a
   difference that shows at the desktop title size and not at the mobile one. */
.hero__role {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-block-start: var(--sp-sm);
  /* Base CSS caps every <p> at 68ch; on a wide title block that shrinks this
     below full width and — because a block does not auto-centre — parks it on
     the left. That is why the role looked centred on mobile (container already
     < 68ch) but not on desktop. Lift the cap so align-items can do its job. */
  max-width: none;
  font-size: var(--fs-200);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-fg);
  /* Cancel the trailing letter-spacing so the visual centre matches the box
     centre. */
  text-indent: var(--ls-label);
}

.hero__role-sub {
  margin-block-start: var(--sp-3xs);
  font-size: var(--fs-100);
  font-weight: var(--fw-regular);
  font-style: italic;
  letter-spacing: 0.02em;
  text-indent: 0.02em;
  text-transform: none;
  color: var(--c-fg-muted);
}

/* ---- SCROLL CUE ---------------------------------------------------------
   A downward arrow that bobs on a loop. Positioned by the stage rules above
   (hidden in the stacked baseline, bottom-centre on desktop). Decorative. */
.scroll-cue {
  color: var(--c-fg-muted);
}

.scroll-cue__arrow {
  width: 0.9rem;
  height: auto;
  animation: scroll-cue-bob 1.8s var(--ease-in-out) infinite;
}

@keyframes scroll-cue-bob {
  0%,
  100% {
    transform: translateY(-15%);
    opacity: 0.45;
  }
  50% {
    transform: translateY(25%);
    opacity: 1;
  }
}

/* Reduced motion: show the arrow, drop the bob. */
.reduced-motion .scroll-cue__arrow {
  animation: none;
  opacity: 0.6;
}

/* ILLUSTRATION ------------------------------------------------------------ */
.figure {
  margin: 0;
  width: min(100%, 34rem);
  margin-inline: auto;
}

.figure__inner {
  /* Reserve the aspect ratio up front so nothing reflows when the video (or a
     Lottie JSON) swaps in over the poster. Matches the artwork's 720x405. */
  aspect-ratio: 16 / 9;
  width: 100%;
  position: relative;

  /* The video is H.264, which cannot carry alpha, so it arrives as artwork on
     an opaque near-white plate. Two things kill that plate exactly:

       isolation      — makes this element the blending backdrop, instead of
                        the page (#smooth-content is a stacking context with a
                        transparent background, so blending against it is a
                        no-op and the plate stays visible).
       background     — a known, exact token colour to blend against.

     `darken` on the media then takes the per-channel minimum, so the plate
     resolves to exactly var(--c-bg) while every dark line survives. Matching
     --c-paper to the video by eye is not enough on its own: H.264 chroma
     compression drifts the decoded ground a level or two frame to frame. */
  isolation: isolate;
  background-color: var(--c-bg);
}

/* One rule for all three renderers: the poster <img>, the <video>, and the
   <svg> Lottie injects. They are swapped in and out of the same box. */
.figure__media,
.figure__inner svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  mix-blend-mode: darken;
}

/* On a dark ground `darken` would erase the artwork entirely (the plate is
   lighter than the backdrop everywhere), so invert instead: the near-white
   plate becomes near-black and the ink becomes white. One asset, both themes. */
[data-theme="dark"] .figure__media {
  mix-blend-mode: lighten;
  filter: invert(1);
}

.figure__caption {
  margin-block-start: var(--sp-sm);
  text-align: center;
  font-size: var(--fs-200);
  color: var(--c-fg-muted);
}

/* ABOUT COPY (in the stage's left column, or stacked on mobile). */
.about {
  max-width: var(--measure-narrow);
}

.about__title {
  font-size: var(--fs-600);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-heading);
  line-height: var(--lh-snug);
  margin-block-end: var(--sp-md);
}

.about__body > * + * {
  margin-block-start: var(--sp-sm);
}

.about__link {
  display: inline-block;
  margin-block-start: var(--sp-lg);
}

/* ==========================================================================
   HOME — PROJECTS
   ========================================================================== */

.projects {
  background-color: var(--c-bg);
  position: relative;
  z-index: var(--z-content);
}

.projects__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-sm);
  margin-block-end: var(--sp-xl);
  padding-block-end: var(--sp-md);
  border-block-end: var(--border-width) solid var(--c-line);
}

.projects__title {
  font-size: var(--fs-500);
  font-weight: var(--fw-medium);
}

.projects__list {
  list-style: none;
  display: grid;
  gap: var(--sp-xl) var(--grid-gap);
}

@media (min-width: 48em) {
  .projects__list {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-lg) var(--grid-gap);
  }
}

/* CARD -------------------------------------------------------------------- */
.card {
  display: block;
  color: var(--c-fg);
}

.card__media {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 5;
  background-color: var(--c-bg-raised);
  border: var(--border-width) solid var(--c-line);
}

.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out),
    filter var(--dur-base) var(--ease-out);
}

/* Grayscale by default, colour on hover — keeps the grid monochrome at rest,
   which is the whole point of the palette, without losing the real imagery. */
.card:hover .card__media img,
.card:focus-visible .card__media img {
  transform: scale(1.04);
  filter: grayscale(0);
}

.card__media img {
  filter: grayscale(1);
}

/* Placeholder shown until real cover images exist. Delete .card__placeholder
   and its rules once assets/images/ is populated. */
.card__placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: var(--fs-100);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-fg-muted);
  background-image: repeating-linear-gradient(
    -45deg,
    transparent 0 10px,
    var(--c-line) 10px 11px
  );
}

.card__index {
  display: block;
  margin-block-start: var(--sp-sm);
  font-size: var(--fs-100);
  letter-spacing: var(--ls-label);
  color: var(--c-fg-muted);
}

.card__title {
  margin-block-start: var(--sp-3xs);
  font-size: var(--fs-400);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-heading);
  line-height: 1.25;
}

.card__meta {
  margin-block-start: var(--sp-2xs);
  font-size: var(--fs-200);
  color: var(--c-fg-muted);
}

/* ==========================================================================
   HOME — CTA
   ========================================================================== */

.cta {
  text-align: center;
  border-block-start: var(--border-width) solid var(--c-line);
}

/* Both paragraphs keep their readable max-width but centre as blocks —
   text-align alone can't, because the capped <p> box itself sits left. */
.cta__eyebrow,
.cta__mail {
  margin-inline: auto;
}

.cta__eyebrow {
  margin-block-end: var(--sp-md);
}

.cta__link {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--fs-800);
  font-weight: var(--fw-bold);
  line-height: 1;
  letter-spacing: var(--ls-display);
  color: var(--c-fg);
}

/* Underline drawn as a pseudo-element so it can wipe rather than blink. */
.cta__link::after {
  content: "";
  display: block;
  height: 2px;
  margin-block-start: 0.15em;
  background-color: currentColor;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform var(--dur-base) var(--ease-out);
}

.cta__link:hover::after,
.cta__link:focus-visible::after {
  transform: scaleX(1);
}

.cta__mail {
  margin-block-start: var(--sp-lg);
  font-size: var(--fs-300);
  color: var(--c-fg-muted);
}

/* ==========================================================================
   FOOTER
   ========================================================================== */

.site-footer {
  padding-block: var(--sp-lg);
  font-size: var(--fs-200);
  color: var(--c-fg-muted);
}

.site-footer a:hover {
  color: var(--c-fg);
}

/* ==========================================================================
   PROJECTS PAGE — page header, filter bar, card grid.
   Reuses the .card component (defined above for the homepage).
   ========================================================================== */

.page-head {
  padding-block: calc(var(--header-height) + var(--sp-2xl)) var(--sp-lg);
}

.page-head__title {
  font-size: var(--fs-800);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-display);
  line-height: var(--lh-tight);
}

.page-head__lead {
  margin-block-start: var(--sp-md);
  max-width: var(--measure);
  font-size: var(--fs-400);
  color: var(--c-fg-muted);
}

/* FILTER BAR ------------------------------------------------------------- */
.filter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2xs);
  padding-block-end: var(--sp-lg);
  margin-block-end: var(--sp-xl);
  border-block-end: var(--border-width) solid var(--c-line);
}

.filter__chip {
  padding: var(--sp-2xs) var(--sp-md);
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-pill);
  font-size: var(--fs-200);
  color: var(--c-fg-muted);
  transition: color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

.filter__chip:hover {
  color: var(--c-fg);
  border-color: var(--c-fg-muted);
}

/* aria-pressed is the single source of truth for the active chip. */
.filter__chip[aria-pressed="true"] {
  color: var(--c-bg);
  background-color: var(--c-fg);
  border-color: var(--c-fg);
}

.filter__count {
  margin-inline-start: var(--sp-xs);
  font-variant-numeric: tabular-nums;
  opacity: 0.6;
}

/* GRID ------------------------------------------------------------------- */
.projects-grid {
  list-style: none;
  display: grid;
  gap: var(--sp-xl) var(--grid-gap);
  padding-block-end: var(--sp-section);
}

@media (min-width: 40em) {
  .projects-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64em) {
  .projects-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--sp-2xl) var(--grid-gap);
  }
}

/* A card hidden by the active filter. JS toggles the attribute; the reveal
   animation is handled in projects.js. */
.projects-grid__item[hidden] {
  display: none;
}

.projects-empty {
  padding-block: var(--sp-2xl);
  color: var(--c-fg-muted);
}

/* Shared placeholder for any media box with no real image yet. Striped so it
   reads as an intentional placeholder, not a broken image. */
.media__placeholder,
.card__placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: var(--fs-100);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-fg-muted);
  background-image: repeating-linear-gradient(
    -45deg,
    transparent 0 10px,
    var(--c-line) 10px 11px
  );
}

/* ==========================================================================
   CASE STUDY — the generated /projects/[slug].html pages.
   ========================================================================== */

/* FLOATING CLOSE — a persistent "×" that returns to the projects index. Fixed
   top-right, below the header so it never overlaps the nav / menu button. It
   lives inside <main>, so it only exists on case studies and ScrollSmoother
   pins it the same way it pins the header. Uses semantic tokens so it adapts to
   a dark case-study theme. */
.case-study__close {
  position: fixed;
  inset-block-start: calc(var(--header-height) + var(--sp-xs));
  inset-inline-end: var(--gutter);
  z-index: var(--z-header);
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-pill);
  background-color: var(--c-bg);
  color: var(--c-fg);
  transition: background-color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.case-study__close:hover,
.case-study__close:focus-visible {
  background-color: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-white);
}

/* Two bars crossed in one grid cell = the ×. */
.case-study__close-bar {
  grid-area: 1 / 1;
  width: 16px;
  height: 1.5px;
  background-color: currentColor;
  transition: transform var(--dur-fast) var(--ease-out);
}

.case-study__close-bar:nth-child(1) {
  transform: rotate(45deg);
}

.case-study__close-bar:nth-child(2) {
  transform: rotate(-45deg);
}

.case-study__close:hover .case-study__close-bar:nth-child(1),
.case-study__close:focus-visible .case-study__close-bar:nth-child(1) {
  transform: rotate(135deg);
}

.case-study__close:hover .case-study__close-bar:nth-child(2),
.case-study__close:focus-visible .case-study__close-bar:nth-child(2) {
  transform: rotate(45deg);
}

/* Hint label, revealed to the LEFT on hover/focus. Absolute so it never widens
   the button's hit area. */
.case-study__close-label {
  position: absolute;
  inset-inline-end: 100%;
  margin-inline-end: var(--sp-sm);
  white-space: nowrap;
  font-size: var(--fs-100);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-fg-muted);
  opacity: 0;
  transform: translateX(var(--sp-2xs));
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out);
}

.case-study__close:hover .case-study__close-label,
.case-study__close:focus-visible .case-study__close-label {
  opacity: 1;
  transform: none;
}

.case-study__hero {
  padding-block: calc(var(--header-height) + var(--sp-2xl)) var(--sp-lg);
  max-width: var(--container-max);
}

.case-study__hero h1 {
  font-size: var(--fs-800);
  max-width: 20ch;
}

.case-study__hero .lead {
  margin-block-start: var(--sp-md);
}

.case-study__meta {
  margin-block-start: var(--sp-xl);
  padding-block-start: var(--sp-md);
  border-block-start: var(--border-width) solid var(--c-line);
  row-gap: var(--sp-md);
}

.case-study__meta dt {
  margin-block-end: var(--sp-3xs);
}

.case-study__cover {
  margin-block: var(--sp-lg) var(--sp-section);
}

/* SOLUTION — image cards. */
.case-study__solution-head {
  margin-block-end: var(--sp-lg);
}

.case-study__solution {
  display: grid;
  gap: var(--grid-gap);
}

@media (min-width: 48em) {
  .case-study__solution {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* A "full" layout image spans both columns. */
  .case-study__figure--full {
    grid-column: 1 / -1;
  }
}

.case-study__figure {
  margin: 0;
}

.case-study__figure figcaption {
  margin-block-start: var(--sp-2xs);
  font-size: var(--fs-200);
  color: var(--c-fg-muted);
}

/* PROCESS — ordered steps. */
.case-study__process {
  counter-reset: step;
}

.case-study__step-title {
  font-size: var(--fs-400);
  font-weight: var(--fw-medium);
  margin-block-end: var(--sp-2xs);
}

.case-study__step-title .eyebrow {
  display: block;
  margin-block-end: var(--sp-3xs);
}

/* RESULTS — the hard-number row in the outcome block. */
.results {
  margin-block-start: var(--sp-lg);
  padding-block-start: var(--sp-md);
  border-block-start: var(--border-width) solid var(--c-line);
  gap: var(--sp-xl);
}

.results__item {
  display: flex;
  flex-direction: column-reverse;
  gap: var(--sp-3xs);
}

.results__value {
  font-family: var(--font-display);
  font-size: var(--fs-600);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-heading);
  line-height: 1;
}

/* PROJECT PAGER — previous / next studies. Each link stacks a direction arrow
   over the project name; prev sits at the start edge, next at the end edge. */
.case-study__pager {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-lg);
  margin-block-start: var(--sp-section);
  padding-block: var(--sp-xl);
  border-block: var(--border-width) solid var(--c-line);
}

.case-study__pager-link {
  display: inline-flex;
  flex-direction: column;
  gap: var(--sp-2xs);
  min-width: 0; /* let long names wrap instead of forcing the row wider */
  color: var(--c-fg);
}

.case-study__pager-link--prev {
  align-items: flex-start;
  text-align: start;
}

.case-study__pager-link--next {
  align-items: flex-end;
  text-align: end;
}

.case-study__pager-arrow {
  font-size: var(--fs-500);
  line-height: 1;
  transition:
    transform var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.case-study__pager-link--prev:hover .case-study__pager-arrow {
  transform: translateX(-0.2em);
  color: var(--c-accent);
}

.case-study__pager-link--next:hover .case-study__pager-arrow {
  transform: translateX(0.2em);
  color: var(--c-accent);
}

.case-study__pager-title {
  font-family: var(--font-display);
  font-size: var(--fs-500);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-display);
  line-height: var(--lh-snug);
  overflow-wrap: anywhere;
  transition: color var(--dur-fast) var(--ease-out);
}

.case-study__pager-link:hover .case-study__pager-title {
  color: var(--c-accent);
}

/* ==========================================================================
   ABOUT PAGE — hand-authored (about.html). Deliberately low-density: the only
   display-scale text is the "About" title; every subsection is marked with a
   small eyebrow (h2.eyebrow in the markup), matching the section-name
   treatment used site-wide. New block namespaces so nothing collides with the
   home stage's .about.* / .figure.* classes.
   ========================================================================== */

/* ABOUT LEDE — title + portrait + intro + "What I do" + "How I work".
   Below 64em: a plain vertical stack in DOM order (this base block). From 64em
   up: one ~100vh grid so the whole opening reads on the first screen. */
.about-lede {
  /* Clear the fixed header (the old page-head carried this offset). */
  padding-block: calc(var(--header-height) + var(--sp-lg)) var(--sp-section);
}

.about-lede__grid {
  display: grid;
  gap: var(--sp-xl);
}

/* Intro (bio + status) and "What I do" travel together as the aside beside the
   portrait on desktop, stacked on mobile. */
.about-lede__aside {
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.about-lede__intro .lead {
  margin-block-end: var(--sp-lg);
}

/* Tighter line spacing than the site default across the About body copy. */
.about-lede__intro .lead,
.approach__card p,
.exp-row__note,
.about-cta__personal {
  line-height: 1.45;
}

/* DESKTOP: one screen. A 12-col grid places the big title across the top, the
   square photo at left, the intro + "What I do" beside it, and the "How I work"
   cards full-width beneath. The composition is vertically centred in the
   viewport, below the fixed header. */
@media (min-width: 64em) {
  .about-lede {
    display: flex;
    align-items: center;
    min-height: 100svh;
    padding-block: var(--header-height) var(--sp-md);
  }

  .about-lede .container {
    width: 100%;
  }

  .about-lede__grid {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--grid-gap);
    row-gap: var(--sp-lg);
    align-items: start;
  }

  .about-lede__title {
    grid-column: 1 / -1;
    grid-row: 1;
  }

  /* Square photo, left. Held to 4 columns so its height leaves room for the
     rows above and below to all land on one screen. */
  .about-lede__portrait {
    grid-column: 1 / span 4;
    grid-row: 2;
  }

  /* Bio + status + "What I do": top-aligned with the top of the photo. */
  .about-lede__aside {
    grid-column: 6 / -1;
    grid-row: 2;
    gap: var(--sp-md);
  }

  /* "How I work" spans full width beneath, so the three cards stay roomy. */
  .about-lede__work {
    grid-column: 1 / -1;
    grid-row: 3;
  }
}

/* STATUS PILL ------------------------------------------------------------- */
.status-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2xs);
  max-width: max-content; /* hug content — overrides the base <p> measure cap */
  padding: var(--sp-2xs) var(--sp-md);
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-pill);
  font-size: var(--fs-200);
  color: var(--c-fg-muted);
}

.status-pill__loc {
  color: var(--c-fg);
  font-weight: var(--fw-medium);
}

.status-pill__sep {
  color: var(--c-line);
}

/* A live-availability dot with a soft outward pulse. */
.status-pill__dot {
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-pill);
  background-color: var(--c-accent);
  animation: status-pulse 2.4s var(--ease-in-out) infinite;
}

@keyframes status-pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(0, 136, 70, 0.35);
  }
  70% {
    box-shadow: 0 0 0 0.6rem rgba(0, 136, 70, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(0, 136, 70, 0);
  }
}

.reduced-motion .status-pill__dot {
  animation: none;
}

/* PORTRAIT ---------------------------------------------------------------- */
.portrait {
  margin: 0;
}

.portrait__frame {
  position: relative; /* containing block for .media__placeholder / .portrait__img */
  aspect-ratio: 1 / 1;
  border: var(--border-width) solid var(--c-line);
  background-color: var(--c-bg-raised);
  overflow: hidden;
}

/* Real portrait, once it exists. Monochrome at rest to match the palette. */
.portrait__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1);
}

.portrait__caption {
  margin-block-start: var(--sp-2xs);
  font-size: var(--fs-200);
  color: var(--c-fg-muted);
}

/* HOW I WORK — three steps as cards with a flow arrow between them. Titles
   stay at body scale (fs-400) so nothing competes with the "About" title. */
.approach__flow {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  margin-block-start: var(--sp-md);
}

@media (min-width: 48em) {
  .approach__flow {
    flex-direction: row;
    align-items: stretch; /* cards share the tallest card's height */
  }
}

.approach__card {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2xs);
  padding: var(--sp-md);
  border: var(--border-width) solid var(--c-line);
  background-color: var(--c-bg-raised);
}

.approach__step {
  font-size: var(--fs-100);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  color: var(--c-accent);
}

.approach__title {
  font-size: var(--fs-400);
  font-weight: var(--fw-medium);
}

.approach__card p {
  color: var(--c-fg-muted);
}

/* Flow arrow: points down between stacked cards (mobile) and right between
   columns (desktop). Decorative — aria-hidden in the markup. */
.approach__arrow {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  color: var(--c-fg-muted);
  transform: rotate(90deg);
}

@media (min-width: 48em) {
  .approach__arrow {
    transform: none;
  }
}

/* WHAT I DO — kept inline and minimal, not a heavy grid. */
.about-lede__do .inline-list {
  margin-block-start: var(--sp-sm);
}

.inline-list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2xs) var(--sp-sm);
  font-size: var(--fs-400);
}

/* Middot between items so the inline set reads as a list, not stray words. */
.inline-list li {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2xs) var(--sp-sm);
}

.inline-list li:not(:last-child)::after {
  content: "·";
  color: var(--c-fg-muted);
}

/* EXPERIENCE -------------------------------------------------------------- */
.experience__list {
  list-style: none;
  margin-block-start: var(--sp-md);
}

.exp-row {
  display: grid;
  gap: var(--sp-3xs);
  padding-block: var(--sp-md);
  border-block-start: var(--border-width) solid var(--c-line);
}

/* Close the table with a bottom rule on the final row. */
.exp-row:last-child {
  border-block-end: var(--border-width) solid var(--c-line);
}

/* Compact two-column table from md up: year, then role on one line. */
@media (min-width: 48em) {
  .exp-row {
    grid-template-columns: 8rem minmax(0, 1fr);
    align-items: baseline;
    gap: var(--grid-gap);
  }
}

.exp-row__year {
  font-size: var(--fs-200);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--ls-label);
  color: var(--c-fg-muted);
}

.exp-row__role {
  font-size: var(--fs-400);
  font-weight: var(--fw-medium);
  transition: color var(--dur-fast) var(--ease-out);
}

.exp-row:hover .exp-row__role {
  color: var(--c-accent);
}

.exp-row__note {
  color: var(--c-fg-muted);
}

/* PERSONAL + CTA ---------------------------------------------------------- */
/* A full-bleed dark band. The section carries data-theme="dark", which flips
   the semantic tokens for everything inside it, so --c-bg resolves to the dark
   ink here and the whole closing block reads as a distinct surface between the
   light page above and the light footer below. */
.about-cta {
  text-align: center;
  background-color: var(--c-bg);
}

.about-cta__personal {
  max-width: var(--measure);
  margin-inline: auto;
  margin-block-end: var(--sp-2xl);
  font-size: var(--fs-400);
  color: var(--c-fg-muted);
}

.about-cta__title {
  margin-block: var(--sp-2xs) var(--sp-lg);
  font-size: var(--fs-500);
  font-weight: var(--fw-medium);
  /* Explicit so it re-resolves against the section's data-theme="dark" tokens.
     Headings otherwise inherit body's already-computed (light) colour and would
     render dark-on-dark here. */
  color: var(--c-fg);
}

.about-cta__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-sm);
}

/* ==========================================================================
   ARCHIVE PAGE — a masonry "scrapbook" of posters and general design work.
   Hand-authored (archive.html): a full-height hero, then a CSS multi-column
   gallery whose card heights follow each image's ratio.
   ========================================================================== */

/* HERO — "Archive" label top-centre, a large (not bold) statement in the
   middle, a scroll cue at the bottom. One viewport tall. */
.archive-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100svh;
  padding-block: calc(var(--header-height) + var(--sp-lg)) var(--sp-lg);
  text-align: center;
}

/* "Archive" reuses .page-head__title for the exact section-header format used
   site-wide; this just removes the default heading margin inside the flex hero. */
.archive-hero__heading {
  margin: 0;
}

/* Large but deliberately NOT bold — a quiet, editorial statement. The auto
   block margins push it to the vertical centre and shove the cue to the base. */
.archive-hero__title {
  margin-block: auto;
  max-width: 22ch;
  font-size: var(--fs-700);
  font-weight: var(--fw-regular);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-heading);
}

.archive-hero__cue {
  display: flex;
  justify-content: center;
}

/* GALLERY — CSS multi-column masonry. Column widths are equal; each card's
   height follows its media ratio, so the grid reads organic without a rigid
   row structure. Order flows down each column, then to the next. */
.archive-grid {
  list-style: none;
  column-count: 2;
  column-gap: var(--grid-gap);
  padding-block-end: var(--sp-section);
}

@media (min-width: 48em) {
  .archive-grid {
    column-count: 3;
  }
}

@media (min-width: 64em) {
  .archive-grid {
    column-count: 4;
  }
}

.archive-card {
  position: relative;
  /* Keep each card whole within its column; the bottom margin is the vertical
     gap (column-gap only spaces columns horizontally). */
  break-inside: avoid;
  margin-block-end: var(--grid-gap);
}

/* Hover-lift lives on the inner element, NOT the card: the GSAP reveal sets an
   inline transform on .archive-card[data-animate], which would otherwise win
   over a :hover transform on the same element. */
.archive-card__inner {
  display: block;
  transform-origin: center;
  transition: transform var(--dur-base) var(--ease-out);
}

.archive-card:hover {
  z-index: 1; /* lift above neighbours while scaled */
}

.archive-card:hover .archive-card__inner {
  transform: scale(1.03);
}

.archive-card__media {
  position: relative; /* containing block for the placeholder */
  overflow: hidden;
  background-color: var(--c-bg-raised);
}

/* Real artwork fills the column width; height follows the image's own ratio —
   this is what makes the masonry organic. */
.archive-card__img {
  display: block;
  width: 100%;
  height: auto;
}

/* Meta row: year bottom-left, category + name stacked bottom-right. */
.archive-card__meta {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-sm);
  margin-block-start: var(--sp-2xs);
}

.archive-card__year {
  font-size: var(--fs-100);
  font-variant-numeric: tabular-nums;
  color: var(--c-fg-muted);
}

.archive-card__detail {
  display: flex;
  flex-direction: column;
  text-align: right;
}

.archive-card__category {
  font-size: var(--fs-100);
  color: var(--c-fg-muted);
}

.archive-card__name {
  font-size: var(--fs-200);
  color: var(--c-fg);
}

/* ==========================================================================
   CONTACT PAGE — "Let's talk" hero (statement left, email-send animation
   right), an interactive Email button, and the address shown plainly below.
   ========================================================================== */

/* Full-white ground so the animation's white envelope shapes blend in (only
   its dark line-art reads). Overriding --c-bg on <body> cascades to the fixed
   .webgl-layer that actually paints the page. */
body[data-page="contact"] {
  --c-bg: var(--c-white);
  --c-bg-raised: var(--c-white);
}

/* HERO — one screen; content vertically centred below the fixed header. */
.contact-hero {
  display: grid;
  align-items: center;
  min-height: 100svh;
  padding-block: calc(var(--header-height) + var(--sp-lg)) var(--sp-2xl);
  position: relative;
  z-index: var(--z-content);
}

.contact-hero__grid {
  display: grid;
  gap: var(--sp-xl);
  align-items: center;
}

/* Desktop: grid-areas rebuild the two-column look — title over body on the
   left (pushed right toward the animation, empty space on the far left), the
   animation spanning both rows on the right. */
@media (min-width: 64em) {
  .contact-hero__grid {
    grid-template-columns: 1.05fr 0.95fr;
    grid-template-areas:
      "title media"
      "body  media";
    column-gap: var(--sp-xl);
    row-gap: 0;
    align-items: start;
  }
  .contact-hero__title {
    grid-area: title;
    justify-self: end;
    width: min(100%, 30rem);
  }
  .contact-hero__body {
    grid-area: body;
    justify-self: end;
    width: min(100%, 30rem);
  }
  .contact-hero__media {
    grid-area: media;
    align-self: center;
  }
}

/* "Let's Talk" — large, Title case, bold. No top margin: nothing sits above it
   now (the eyebrow was removed). */
.contact-hero__title {
  margin-block: 0 var(--sp-lg);
  font-size: var(--fs-800);
  font-weight: var(--fw-bold);
  line-height: 0.95;
  letter-spacing: -0.02em;
}

/* Each line is a clip mask; the inner span is what slides. The padding /
   negative-margin pair keeps descenders (the apostrophe tail, the 'y' hooks in
   other words) from being shaved by the mask. */
.contact-hero__line {
  display: block;
  overflow: hidden;
  padding-block-end: 0.08em;
  margin-block-end: -0.08em;
}

.contact-hero__line-inner {
  display: block;
  will-change: transform;
}

/* Pre-hide the lines for the reveal ONLY when JS is alive and motion allowed —
   same guard as [data-animate], so a JS/GSAP failure never hides the title. */
.js:not(.reduced-motion) .contact-hero__line-inner {
  transform: translateY(110%);
}

.contact-hero__lead {
  max-width: 42ch;
  line-height: 1.5;
  color: var(--c-fg-muted);
}

.contact-actions {
  margin-block-start: var(--sp-lg);
}

/* MEDIA — the looping email-send animation. Vector (SMIL) on a transparent
   ground, so it floats with no frame and stays crisp at any size. Its dark
   line-art reads on the light hero; whites blend into the paper. */
.contact-hero__media {
  display: flex;
  justify-content: center;
}

.contact-hero__svg {
  display: block;
  width: 100%;
  max-width: 30rem;
  aspect-ratio: 1 / 1;
  height: auto;
}

/* On a dark ground the black artwork would vanish — invert so the line-art
   flips to light (mirrors the home illustration's per-theme handling). */
[data-theme="dark"] .contact-hero__svg {
  filter: invert(1);
}

/* MOBILE (single column) — centre the stack and make the title the focal
   point. The animation is shrunk so a larger, centred "Let's Talk" AND the
   animation both land inside one screen (the hero already centres the stack
   vertically via align-items:center + min-height:100svh). */
@media (max-width: 63.999em) {
  .contact-hero__grid {
    gap: var(--sp-lg);
  }

  .contact-hero__title {
    text-align: center;
    font-size: clamp(3.5rem, 15vw, 5rem);
  }

  .contact-hero__body {
    text-align: center;
  }

  /* The lead is measure-capped; centre the block itself, not just its text. */
  .contact-hero__lead {
    margin-inline: auto;
  }

  .contact-actions {
    text-align: center;
  }

  .contact-hero__svg {
    max-width: 16rem;
  }
}

/* EMAIL BUTTON — says "Email"; hover slides up to reveal the address, fills
   with the accent, and nudges the arrow. Magnetic via [data-magnetic]. */
.email-button {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-md);
  padding: var(--sp-sm) var(--sp-lg);
  border: var(--border-width) solid var(--c-fg);
  border-radius: var(--radius-pill);
  color: var(--c-fg);
  overflow: hidden;
  transition: background-color var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    color var(--dur-base) var(--ease-out);
}

.email-button:hover,
.email-button:focus-visible {
  background-color: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-white);
}

/* Single-line window over the two stacked faces. Its width follows the wider
   face (the address), so the reveal never reflows the button. */
.email-button__viewport {
  position: relative;
  display: block;
  height: 1.5em;
  overflow: hidden;
  font-size: var(--fs-300);
  font-weight: var(--fw-medium);
  letter-spacing: 0.01em;
}

.email-button__face {
  display: block;
  height: 1.5em;
  line-height: 1.5em;
  white-space: nowrap;
  transition: transform var(--dur-base) var(--ease-out);
}

.email-button:hover .email-button__face,
.email-button:focus-visible .email-button__face {
  transform: translateY(-100%);
}

.email-button__arrow {
  display: inline-flex;
  flex: none;
  transition: transform var(--dur-base) var(--ease-out);
}

.email-button__arrow svg {
  width: 1.15em;
  height: 1.15em;
}

.email-button:hover .email-button__arrow,
.email-button:focus-visible .email-button__arrow {
  transform: translateX(4px);
}

/* DETAILS — plain address, socials, location. */
.contact-details {
  padding-block-start: var(--sp-xl);
  padding-block-end: var(--sp-section);
  border-block-start: var(--border-width) solid var(--c-line);
}

.contact-details__grid {
  display: grid;
  gap: var(--sp-lg);
}

@media (min-width: 48em) {
  .contact-details__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--grid-gap);
  }
}

.contact-detail .eyebrow {
  margin-block-end: var(--sp-sm);
}

.contact-email-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-sm);
}

.contact-email {
  font-size: var(--fs-400);
  color: var(--c-fg);
  transition: color var(--dur-fast) var(--ease-out);
}

.contact-email:hover {
  color: var(--c-accent);
}

.contact-copy {
  padding: var(--sp-3xs) var(--sp-sm);
  border: var(--border-width) solid var(--c-line);
  border-radius: var(--radius-pill);
  font-size: var(--fs-100);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-fg-muted);
  transition: color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
}

.contact-copy:hover {
  color: var(--c-fg);
  border-color: var(--c-fg-muted);
}

/* contact.js toggles this after a successful clipboard write. */
.contact-copy.is-copied {
  color: var(--c-accent);
  border-color: var(--c-accent);
}

.contact-socials {
  list-style: none;
  display: grid;
  gap: var(--sp-2xs);
}

.contact-social {
  display: inline-block;
  font-size: var(--fs-400);
  color: var(--c-fg);
  transition: color var(--dur-fast) var(--ease-out);
}

.contact-social:hover {
  color: var(--c-accent);
}

.contact-detail__value {
  font-size: var(--fs-400);
  color: var(--c-fg-muted);
}


/* ===== 07-utilities.css ============================================ */
/* ==========================================================================
   06 — UTILITIES
   Last in the cascade. Single-purpose, no nesting, safe to add to any element.
   ========================================================================== */

/* TEXT ------------------------------------------------------------------- */
.text-muted {
  color: var(--c-fg-muted);
}
.text-accent {
  color: var(--c-accent-bright);
}
[data-theme="light"] .text-accent {
  color: var(--c-accent-deep);
}

.eyebrow {
  display: block;
  font-size: var(--fs-100);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--c-fg-muted);
}

.lead {
  font-size: var(--fs-400);
  line-height: var(--lh-loose);
  max-width: var(--measure);
}

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

.text-center {
  text-align: center;
}

/* LAYOUT ----------------------------------------------------------------- */
.flow-none > * + * {
  margin-block-start: 0;
}

.full-bleed {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

.hide {
  display: none !important;
}

@media (max-width: 47.999em) {
  .hide-below-md {
    display: none !important;
  }
}

@media (min-width: 64em) {
  .hide-above-lg {
    display: none !important;
  }
}

/* MOTION ----------------------------------------------------------------- */

/* Elements GSAP will animate in. Pre-hidden in CSS so there is no flash of
   un-animated content — but ONLY when JS is confirmed alive (.js on <html>,
   set inline in <head>) and motion is allowed. Without that guard, a JS
   failure would leave the page permanently invisible. */
.js:not(.reduced-motion) [data-animate] {
  opacity: 0;
}

/* Reduced motion: reveal immediately, no transform, no stagger. */
.reduced-motion [data-animate] {
  opacity: 1 !important;
  transform: none !important;
}

/* Belt-and-braces for anything not routed through GSAP. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* PRINT ------------------------------------------------------------------ */
@media print {
  .webgl-layer,
  .cursor,
  .transition-curtain,
  .site-header,
  .skip-link {
    display: none !important;
  }

  html,
  body {
    background: #fff !important;
    color: #000 !important;
  }
}
