/* D3 Web Development: entry stylesheet. build.mjs inlines each import, in this order. */

/* Reset: the parts of Tailwind's preflight the site's styles were written against. */

*,
::before,
::after {
  margin: 0;
  padding: 0;
  border: 0 solid;
}

h1, h2, h3, h4, h5, h6 {
  font-size: inherit;
  font-weight: inherit;
}

ol, ul, menu {
  list-style: none;
}

input, textarea, select, button {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  background-color: transparent;
  border-radius: 0;
}

textarea {
  resize: vertical;
}

::placeholder {
  opacity: 1;
  color: color-mix(in oklab, currentColor 50%, transparent);
}

[hidden]:where(:not([hidden="until-found"])) {
  display: none !important;
}

html {
  scroll-behavior: smooth;
  /* Anchor jumps and focus scrolling stop below the fixed nav instead of under it. */
  scroll-padding-top: 6rem;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

/* ===========================================================
   Design tokens — D3 Web Development
   Colors are OKLCH (P3-aware where the browser supports it).
   =========================================================== */

/* Register --angle so it can be animated as an angle type (used by the
   rotating-conic border on .btn--primary). Without @property, CSS would
   not interpolate between var(--angle) values. */
@property --angle {
    syntax: '<angle>';
    initial-value: 0deg;
    inherits: false;
}

:root,
[data-theme="dark"] {
  /* surface */
  --bg:           oklch(0.14 0.005 60);
  --bg-elevated:  oklch(0.18 0.005 60);
  --bg-overlay:   oklch(0.25 0.005 60 / 0.55);
  --border:       oklch(0.25 0.005 60);

  /* type — contrast ratios against --bg verified for WCAG AA. */
  --fg:           oklch(0.96 0.005 80);
  --fg-muted:     oklch(0.70 0.010 80);
  --fg-subtle:    oklch(0.65 0.010 80);

  /* accent — deep electric blue. Pairs with the mesh palette. */
  --accent:       oklch(0.42 0.28 248);
  --accent-soft:  oklch(0.42 0.28 248 / 0.18);
  --fg-on-accent: oklch(0.99 0 0);

  /* Conic-border ring — pure white at the highlight, near-black at the
     opposite point, mid grays in between. Same in both modes; the visible
     sector naturally swaps (white pops on dark bg, black pops on cream). */
  --ring-bright: oklch(1.00 0 0);
  --ring-mid:    oklch(0.55 0 0);
  --ring-dim:    oklch(0.10 0 0);
}

[data-theme="light"] {
  --bg:           oklch(0.97 0.01 80);
  --bg-elevated:  oklch(0.94 0.01 80);
  --bg-overlay:   oklch(0.97 0.01 80 / 0.78);
  --border:       oklch(0.85 0.01 80);

  --fg:           oklch(0.18 0.01 60);
  --fg-muted:     oklch(0.40 0.01 60);
  --fg-subtle:    oklch(0.42 0.01 60);

  /* accent, spectrum, and ring tokens unchanged across modes */
}

:root {
  /* spectrum gradient — pulled from D3 logo (kept for the wordmark text fill). */
  --spectrum: linear-gradient(
    90deg,
    oklch(0.65 0.27 25),    /* red */
    oklch(0.78 0.22 60),    /* orange */
    oklch(0.92 0.20 100),   /* yellow */
    oklch(0.78 0.20 145),   /* green */
    oklch(0.78 0.18 200),   /* cyan */
    oklch(0.55 0.22 260),   /* blue */
    oklch(0.55 0.27 320)    /* violet */
  );

  /* Animated mesh gradient — used on .bg-mesh and .text-mesh.
     OKLCH-interpolated for perceptually smooth blending (no muddy
     sRGB midpoints between vibrant blues and purples). 9 stops with
     matching first+last so tile seam is invisible.
     This is the LIGHT-mode variant. Dark mode overrides --mesh-grad
     below with a brighter palette since these L 0.46-0.50 stops read
     dim against the dark page bg. */
  --mesh-grad: linear-gradient(
    115deg in oklch,
    /* Lightness held tight (0.46-0.50) to avoid brightness banding at fast
       animation speeds. Hue sweep widened to 232-305° (cyan-leaning blue
       → deep magenta-purple) for richer color variety than the previous
       narrow 240-295° band. Chroma also breathes 0.22-0.27 so saturation
       isn't uniform. */
    oklch(0.46 0.22 232),    /* cyan-blue */
    oklch(0.48 0.24 246),    /* blue */
    oklch(0.50 0.25 258),    /* electric blue */
    oklch(0.49 0.25 270),    /* blue-indigo */
    oklch(0.47 0.26 283),    /* indigo-purple */
    oklch(0.46 0.27 296),    /* royal purple */
    oklch(0.46 0.25 305),    /* deep magenta-purple — POP */
    oklch(0.47 0.26 290),    /* royal purple */
    oklch(0.48 0.25 275),    /* blue-indigo */
    oklch(0.50 0.25 260),    /* electric */
    oklch(0.48 0.24 246),    /* blue */
    oklch(0.46 0.22 232)     /* loop back to cyan-blue */
  );
}

/* Dark mode mesh: brighter (L 0.68-0.72) so it reads cleanly against
   the dark page bg. Same hue sweep, slightly trimmed chroma so the
   brighter hues don't clip out of sRGB. Overrides --mesh-grad for any
   element that pulls it via var() — nav links, body text-link mesh,
   stat numbers, button bg, badge bg, etc. */
[data-theme="dark"] {
  --mesh-grad: linear-gradient(
    115deg in oklch,
    oklch(0.68 0.18 232),
    oklch(0.70 0.20 246),
    oklch(0.72 0.20 258),
    oklch(0.71 0.20 270),
    oklch(0.69 0.21 283),
    oklch(0.68 0.22 296),
    oklch(0.68 0.20 305),
    oklch(0.69 0.21 290),
    oklch(0.70 0.20 275),
    oklch(0.72 0.20 260),
    oklch(0.70 0.20 246),
    oklch(0.68 0.18 232)
  );
}

:root {
  /* fluid type scale */
  --type-display:  clamp(3rem,    8vw + 1rem,    9rem);
  --type-h1:       clamp(2.5rem,  5vw + 1rem,    6rem);
  --type-h2:       clamp(1.875rem, 3vw + 1rem,   3.5rem);
  --type-h3:       clamp(1.25rem, 1.5vw + 0.75rem, 2rem);
  --type-body:     1.0625rem;
  --type-small:    0.875rem;
  --type-mono:     0.75rem;

  --leading-tight: 1.1;
  --leading-snug:  1.3;
  --leading-body:  1.6;

  --tracking-mono: 0.08em;

  /* motion */
  --motion-instant:    100ms;
  --motion-fast:       250ms;
  --motion-base:       400ms;
  --motion-slow:       700ms;
  --motion-cinematic:  1200ms;

  --ease-default:   cubic-bezier(0.65, 0, 0.35, 1);
  --ease-out-expo:  cubic-bezier(0.16, 1, 0.3, 1);

  /* layout */
  --gutter-page:    clamp(1rem, 4vw, 4rem);
  --max-page:       1440px;
  --max-prose:      68ch;

  /* Nav reserved layout height — used by .site-nav-wrap to reserve flow
     space for the position:fixed nav, and by full-bleed heroes that want
     to extend up under it via negative margin. Keep mobile/desktop in
     sync with .site-nav__brand--svg height + padding-block. */
  --nav-h:          4.25rem;

  --radius-sm:      4px;
  --radius-md:      10px;
  --radius-lg:      24px;
}

@media (min-width: 1024px) {
  :root {
    --nav-h:        9rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-instant:   0ms;
    --motion-fast:      0ms;
    --motion-base:      0ms;
    --motion-slow:      0ms;
    --motion-cinematic: 0ms;
  }
}

/* Self-hosted font faces — Latin subset only.
   Fraunces: variable woff2 (wght axis 100-900, opsz 9-144), roman + italic.
   Geist: static woff2, weights 400/500/600 (the only weights used on-site).
   Geist Mono: static woff2, weights 200/400/500 (the only weights used on-site).
   All served from /assets/fonts/ — no third-party requests. */

/* ── Fraunces (variable — covers all weights in a single file per style) ── */

@font-face {
  font-family: "Fraunces";
  src: url("/assets/fonts/fraunces-latin-variable-normal.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Fraunces";
  src: url("/assets/fonts/fraunces-latin-variable-italic.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}

/* ── Geist (static per-weight; only weights referenced in CSS) ── */

@font-face {
  font-family: "Geist";
  src: url("/assets/fonts/geist-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist";
  src: url("/assets/fonts/geist-latin-500-normal.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist";
  src: url("/assets/fonts/geist-latin-600-normal.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/* ── Geist Mono (static per-weight; only weights referenced in CSS) ── */

@font-face {
  font-family: "Geist Mono";
  src: url("/assets/fonts/geist-mono-latin-200-normal.woff2") format("woff2");
  font-weight: 200;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("/assets/fonts/geist-mono-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Geist Mono";
  src: url("/assets/fonts/geist-mono-latin-500-normal.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* Base — small reset deltas + body defaults */

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

html {
  -webkit-text-size-adjust: 100%;
  text-rendering: optimizeLegibility;
}

html,
body {
  /* `clip` (not `hidden`) prevents horizontal overflow WITHOUT making the
     element a scroll container — keeps native scroll-driven animations
     (animation-timeline: view()) working on the body content. */
  overflow-x: clip;
}

body {
  background: var(--bg);
  color: var(--fg);
  font-family: "Geist", system-ui, -apple-system, sans-serif;
  font-size: var(--type-body);
  line-height: var(--leading-body);
  font-feature-settings: "ss01", "cv11";
  margin: 0;
  min-height: 100dvh;
  transition:
    background var(--motion-base) var(--ease-default),
    color var(--motion-base) var(--ease-default);
}

::selection {
  background: var(--accent);
  color: var(--fg-on-accent);
}

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

button {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  font: inherit;
  padding: 0;
}

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

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

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.eyebrow {
  font-family: "Geist Mono", ui-monospace, monospace;
  font-size: var(--type-mono);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.display {
  font-family: "Fraunces", Georgia, serif;
  font-size: var(--type-display);
  line-height: var(--leading-tight);
  font-weight: 500;
  letter-spacing: -0.02em;
}

.h1 { font-family: "Fraunces", Georgia, serif; font-size: var(--type-h1); line-height: var(--leading-tight); font-weight: 500; letter-spacing: -0.02em; }
.h2 { font-family: "Fraunces", Georgia, serif; font-size: var(--type-h2); line-height: var(--leading-snug); font-weight: 500; letter-spacing: -0.015em; }
.h3 { font-family: "Geist", system-ui, sans-serif; font-size: var(--type-h3); line-height: var(--leading-snug); font-weight: 600; letter-spacing: -0.01em; }

.page {
  width: 100%;
  max-width: var(--max-page);
  margin-inline: auto;
  padding-inline: var(--gutter-page);
}

.site-footer {
    margin-top: 8rem;
    padding-top: 3rem;
    padding-bottom: 4rem;
    border-top: 1px solid var(--border);
    color: var(--fg-muted);
    font-size: var(--type-small);
}
@media (max-width: 767.98px) {
    .site-footer {
        /* Mobile: shrink the gap above the footer. Combined reduction of
         * 100px from the previous spacing — drop margin-top to 0 and
         * pull padding-top in from 3rem to ~12px. */
        margin-top: 0;
        padding-top: 12px;
    }
    /* Mobile: thin separator between major home sections. Each section
     * already has its own padding-block, so a top border here visually
     * splits them without adding extra space. .contact already has its
     * own border-top — this matches the rest to it. */
    .bento,
    .work-grid,
    .about {
        border-top: 1px solid var(--border);
    }
    /* Mobile: small top space on the footer brand so it doesn't sit
     * flush against the footer's top border. */
    .site-footer__brand {
        margin-top: 15px;
    }
}
.site-footer__inner {
    display: grid;
    gap: 2rem;
    grid-template-columns: 1fr;
}
.site-footer__brand {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    line-height: 0.9;
}
/* Brand became a clickable home link (<a> wrapping the mark + sub spans).
 * The anchor needs the flex-column treatment so the spans stack instead
 * of sitting inline next to each other. */
.site-footer__brand a {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
}
.site-footer__brand-mark {
    font-family: "Fraunces", Georgia, serif;
    font-size: 3rem;
    font-weight: 600;
    font-style: italic;
    letter-spacing: -0.05em;
    line-height: 0.82;
}
.site-footer__brand-sub {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.65rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--fg-muted);
    margin-top: 0.2em;
}
.site-footer__nav {
    display: flex;
    flex-wrap: wrap;
    gap: 1.25rem;
}
.site-footer__nav a:hover { color: var(--fg); }
.site-footer__legal {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}
.site-footer__credit { font-size: 0.75rem; color: var(--fg-subtle); }

@media (min-width: 1024px) {
    .site-footer__inner {
        grid-template-columns: auto 1fr auto;
        align-items: center;
    }
}

/* About section */

.about {
    padding-block: 6rem;
}
.about__inner {
    display: grid;
    grid-template-columns: 1fr;
    gap: 3rem;
}
.about__copy {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    max-width: 56ch;
}
.about__copy h2 {
    margin-bottom: 0.25rem;
}
.about__copy p {
    margin: 0;
    color: var(--fg);
    font-size: 1.0625rem;
    line-height: 1.65;
}
.about__copy em {
    font-family: "Fraunces", Georgia, serif;
    font-style: italic;
    color: var(--fg);
}

.about__portrait {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    align-items: flex-start;
}
.about__portrait-frame {
    width: 100%;
    aspect-ratio: 1 / 1.2;
    max-width: 360px;
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}
/* On mobile (≤768px), use a landscape 16:9 frame above the copy. */
@media (max-width: 768px) {
    .about__portrait-frame {
        aspect-ratio: 16 / 9;
        max-width: 100%;
    }
}
.about__portrait-stack {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: 2rem;
    gap: 0.25em;
}
.about__portrait-eyebrow {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.7rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--fg-subtle);
}
.about__portrait-mark {
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(3.5rem, 9vw, 6rem);
    font-weight: 500;
    line-height: 0.9;
    letter-spacing: -0.03em;
    color: var(--fg);
}
.about__portrait-suffix {
    font-family: "Fraunces", Georgia, serif;
    font-style: italic;
    font-weight: 400;
    font-size: clamp(2rem, 5.5vw, 3.5rem);
    /* Color comes from .text-mesh utility. */
    line-height: 1;
    margin-left: 0.05em;
}
@media (min-width: 960px) {
    .about__inner {
        grid-template-columns: 1.4fr 1fr;
        gap: 4rem;
        align-items: start;
    }
    /* Bigger portrait at large breakpoints, nudged down + right so it
     * sits with breathing room next to the copy column. */
    .about__portrait-frame {
        max-width: 440px;
        margin-top: 2rem;
        margin-left: 2rem;
    }
}


/* resources\css\components\nav.css */

/* Animated pill theme toggle.
   Track = sky (light blue gradient → night navy gradient).
   Thumb = sun (yellow glowing) in light mode, slides right and morphs
   into a crescent moon (cream with inset shadow notch) in dark mode.
   Stars twinkle in the track when dark mode is active. */
.mode-toggle {
    --track-w: 76px;
    --track-h: 42px;             /* matches .btn block height (0.85rem padding × 2 + 15px text) */
    --thumb-size: 30px;
    --gap: 6px;

    position: relative;
    width: var(--track-w);
    height: var(--track-h);
    border-radius: 999px;
    overflow: hidden;
    cursor: pointer;
    border: 1px solid var(--border);
    padding: 0;
    /* Twilight-blue sky — same hue family as the button's mesh (240-260)
       but lighter and less saturated so it reads as "day sky" without
       competing with the button's color. */
    background: linear-gradient(180deg, oklch(0.48 0.17 252), oklch(0.68 0.13 228));
    transition: background 600ms ease, border-color 400ms ease;
    flex-shrink: 0;
}
[data-theme="dark"] .mode-toggle {
    background: linear-gradient(180deg, oklch(0.18 0.05 270), oklch(0.10 0.04 270));
    /* The default --border token (~0.30 lightness) is too close to the
       toggle's dark sky gradient. Bumping to ~0.50 keeps it in the cool
       hue family but separates the edge clearly from the bg. */
    border-color: oklch(0.42 0.02 270);
}
.mode-toggle:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Clouds layer — visible only in light mode; drift right→left at different
   speeds. Each cloud is built from overlapping white circles via box-shadow. */
.mode-toggle__clouds {
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: 1;
    transition: opacity 600ms ease;
    /* Soft mask at both edges — wider fade zone on the left (where clouds
       exit) so they gradually disappear instead of popping. Right edge fade
       is narrower since clouds enter from off-screen and just need a quick
       fade-in. */
    -webkit-mask-image: linear-gradient(90deg, transparent 0%, black 28%, black 90%, transparent 100%);
            mask-image: linear-gradient(90deg, transparent 0%, black 28%, black 90%, transparent 100%);
}
[data-theme="dark"] .mode-toggle__clouds {
    opacity: 0;
}
.mode-toggle__cloud {
    position: absolute;
    background: white;
    border-radius: 50%;
    opacity: 0.92;
}

/* Cloud 1 — medium, upper, faster (bumped from small so we don't have a
   tiny trailing cloud in the rotation) */
.mode-toggle__cloud--1 {
    top: 7px;
    width: 6px;
    height: 4px;
    box-shadow:
        5px 0 0 0 white,
        9px 0 0 -1px white,
        4px -2px 0 -1px white,
        -3px 0 0 -1px white;
    animation: cloud-drift 14s linear infinite;
}

/* Cloud 2 — LARGE, lower, slower */
.mode-toggle__cloud--2 {
    top: 23px;
    width: 9px;
    height: 6px;
    box-shadow:
        8px 0 0 0 white,
        13px 0 0 -1px white,
        8px -3px 0 -1px white,
        3px -3px 0 -2px white,
        -3px 0 0 0 white,
        -6px -1px 0 -2px white;
    animation: cloud-drift 22s linear infinite;
    animation-delay: -10s;
}

/* Cloud 3 — medium, middle, mid-speed */
.mode-toggle__cloud--3 {
    top: 14px;
    width: 6px;
    height: 4px;
    box-shadow:
        5px 0 0 0 white,
        9px 0 0 -1px white,
        4px -2px 0 -1px white,
        -3px 0 0 -1px white;
    animation: cloud-drift 19s linear infinite;
    animation-delay: -5s;
}

/* Cloud 4 — medium-large, upper-mid, fills the gap behind cloud 1 */
.mode-toggle__cloud--4 {
    top: 11px;
    width: 7px;
    height: 5px;
    box-shadow:
        6px 0 0 0 white,
        11px 0 0 -1px white,
        5px -2px 0 -1px white,
        2px -2px 0 -2px white,
        -3px 0 0 -1px white;
    animation: cloud-drift 16s linear infinite;
    animation-delay: -11s;
}

/* Cloud 5 — medium-large, lower, fills the gap behind cloud 2 */
.mode-toggle__cloud--5 {
    top: 28px;
    width: 7px;
    height: 5px;
    box-shadow:
        6px 0 0 0 white,
        10px 0 0 -1px white,
        5px -2px 0 -1px white,
        2px -2px 0 -1px white,
        -3px 0 0 -1px white;
    animation: cloud-drift 24s linear infinite;
    animation-delay: -16s;
}

/* Cloud 6 — small, near the top, different speed */
.mode-toggle__cloud--6 {
    top: 5px;
    width: 4px;
    height: 3px;
    box-shadow:
        3px 0 0 0 white,
        6px 0 0 -1px white,
        2px -2px 0 -1px white;
    animation: cloud-drift 13s linear infinite;
    animation-delay: -4s;
}

/* Cloud 7 — medium, mid-low, overlaps with cloud 2's vertical area */
.mode-toggle__cloud--7 {
    top: 24px;
    width: 5px;
    height: 4px;
    box-shadow:
        4px 0 0 0 white,
        7px 0 0 -1px white,
        3px -2px 0 -1px white,
        -2px 0 0 -1px white;
    animation: cloud-drift 17s linear infinite;
    animation-delay: -7s;
}

/* Cloud 8 — LARGE, lower, fills the empty bottom area below the big cloud */
.mode-toggle__cloud--8 {
    top: 32px;
    width: 8px;
    height: 5px;
    box-shadow:
        7px 0 0 0 white,
        12px 0 0 -1px white,
        7px -3px 0 -1px white,
        3px -2px 0 -1px white,
        -3px 0 0 -1px white,
        -5px -1px 0 -2px white;
    animation: cloud-drift 20s linear infinite;
    animation-delay: -3s;
}

@keyframes cloud-drift {
    from { transform: translateX(85px); }
    to   { transform: translateX(-40px); }
}

/* Stars layer — fades in only in dark mode, each twinkles on its own beat. */
.mode-toggle__stars {
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity 600ms ease;
}
[data-theme="dark"] .mode-toggle__stars {
    opacity: 1;
}
.mode-toggle__star {
    position: absolute;
    width: 2px;
    height: 2px;
    background: white;
    border-radius: 50%;
    box-shadow: 0 0 3px rgba(255, 255, 255, 0.85);
    animation: mode-toggle-twinkle 2.4s ease-in-out infinite;
}
.mode-toggle__star:nth-child(1) { top: 10px; left: 14px; animation-delay: 0s; }
.mode-toggle__star:nth-child(2) { top: 27px; left: 24px; animation-delay: 0.8s; }
.mode-toggle__star:nth-child(3) { top: 13px; left: 38px; animation-delay: 1.6s; }

@keyframes mode-toggle-twinkle {
    0%, 100% { opacity: 0.4; transform: scale(0.8); }
    50%      { opacity: 1;   transform: scale(1.2); }
}

/* Thumb — slides between left (sun) and right (moon). */
.mode-toggle__thumb {
    position: absolute;
    top: 50%;
    left: var(--gap);
    width: var(--thumb-size);
    height: var(--thumb-size);
    border-radius: 50%;
    transform: translateY(-50%);
    background: radial-gradient(circle at 32% 30%, oklch(0.96 0.15 90), oklch(0.78 0.20 65));
    box-shadow:
        inset 0 0 0 0 transparent,                                /* no notch in light mode */
        0 0 8px 1px oklch(0.95 0.18 75 / 0.55),                   /* sun glow inner */
        0 0 14px 3px oklch(0.95 0.18 75 / 0.25);                  /* sun glow outer */
    transition:
        left 540ms cubic-bezier(0.4, 0, 0.2, 1),
        background 540ms cubic-bezier(0.4, 0, 0.2, 1),
        box-shadow 540ms cubic-bezier(0.4, 0, 0.2, 1);
}
[data-theme="dark"] .mode-toggle__thumb {
    left: calc(var(--track-w) - var(--thumb-size) - var(--gap));
    background: radial-gradient(circle at 28% 30%, oklch(0.96 0.01 80), oklch(0.78 0.01 80));
    box-shadow:
        inset -11px -3px 0 0 oklch(0.18 0.05 270),                /* bigger notch — more dramatic crescent */
        0 0 8px 1px oklch(0.90 0.01 80 / 0.4),                    /* moon glow inner */
        0 0 14px 3px oklch(0.90 0.01 80 / 0.2);                   /* moon glow outer */
}

/* Hover/focus — sun glows brighter and a touch larger; moon gets a cooler
   stronger halo. Clouds speed up so the sky feels active. The transform
   composes with the thumb's existing translateY(-50%). */
.mode-toggle:hover .mode-toggle__thumb,
.mode-toggle:focus-visible .mode-toggle__thumb {
    transform: translateY(-50%) scale(1.08);
    box-shadow:
        inset 0 0 0 0 transparent,
        0 0 12px 2px oklch(0.95 0.18 75 / 0.75),
        0 0 22px 5px oklch(0.95 0.18 75 / 0.42),
        0 0 32px 9px oklch(0.95 0.18 75 / 0.18);
}
[data-theme="dark"] .mode-toggle:hover .mode-toggle__thumb,
[data-theme="dark"] .mode-toggle:focus-visible .mode-toggle__thumb {
    transform: translateY(-50%) scale(1.08);
    box-shadow:
        inset -11px -3px 0 0 oklch(0.18 0.05 270),
        0 0 14px 3px oklch(0.90 0.01 80 / 0.65),
        0 0 26px 8px oklch(0.90 0.01 80 / 0.4),
        0 0 40px 14px oklch(0.90 0.01 80 / 0.2);
}

/* Clouds don't speed up on hover — changing animation-duration mid-cycle
   creates a visible jump as the browser recalculates each cloud's keyframe
   position. Sun glow alone handles the hover feedback. */

@media (prefers-reduced-motion: reduce) {
    .mode-toggle,
    .mode-toggle__stars,
    .mode-toggle__star,
    .mode-toggle__clouds,
    .mode-toggle__cloud,
    .mode-toggle__thumb {
        transition: none !important;
        animation: none !important;
    }
}

.skip-link {
    position: fixed;
    top: -100px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--accent);
    color: var(--fg-on-accent);
    padding: 0.6rem 1.1rem;
    border-radius: var(--radius-md);
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.8125rem;
    letter-spacing: 0.04em;
    text-decoration: none;
    z-index: 200;  /* above everything, including the open drawer */
    transition: top var(--motion-fast) var(--ease-default);
}
.skip-link:focus,
.skip-link:focus-visible {
    top: 12px;
    outline: 2px solid var(--fg);
    outline-offset: 3px;
}

/* Wrap reserves a constant-height flow placeholder so the nav (fixed)
   can shrink visually on scroll without dragging the page content up.
   Height matches the initial (non-scrolled) nav: padding-block 1.75rem
   on each side + brand-svg height (3.25rem mobile / 5.5rem desktop).
   Was display: contents + sticky nav before — that caused the hero to
   jump up by ~60px when the nav shrank, because sticky elements still
   occupy their current rendered height in flow. */
.site-nav-wrap {
    position: relative;
    height: var(--nav-h);
}

.site-nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 50;
    border-bottom: 1px solid transparent;
    transition: border-color var(--motion-fast) var(--ease-default);
}
/* Bg + blur live on a ::before pseudo so we can fade the LAYER (opacity)
   instead of the color (background). Animating opacity means the
   scroll-state in/out is still smooth, but a theme swap (where
   --bg-overlay's resolved color changes) snaps instantly — no
   crossfade through the old theme's color.
   Unprefixed backdrop-filter only — see the historical comment in git
   blame; the prod minifier drops the standard form when paired with
   -webkit-. All modern browsers support unprefixed. */
.site-nav::before {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--bg-overlay);
    backdrop-filter: blur(20px) saturate(140%);
    opacity: 0;
    transition: opacity var(--motion-fast) var(--ease-default);
    pointer-events: none;
}
.site-nav.is-scrolled::before {
    opacity: 1;
}
.site-nav.is-scrolled {
    border-bottom-color: var(--border);
}

/* On full-bleed-hero pages (case studies), the hero image extends up
   under the nav. Force the nav's bg + blur on from the start so the nav
   stays legible against whatever the image is, instead of being invisible
   at scrollY=0. data-page="project" is set on <body> for case-study pages. */
body[data-page="project"] .site-nav::before {
    opacity: 1;
}
body[data-page="project"] .site-nav {
    border-bottom-color: var(--border);
}
/* Dark mode --bg-overlay is only ~55% alpha — bright hero images
   (laptop screens with cream/white content) show through enough to
   wash out the nav buttons. Override with a near-opaque dark fill
   on case-study pages so the nav stays legible regardless of image. */
[data-theme="dark"] body[data-page="project"] .site-nav::before {
    background: oklch(0.12 0.01 60 / 0.88);
}

.site-nav__inner {
    /* position: relative + z-index: 1 puts the inner above .site-nav::before
       (which holds the bg + backdrop-filter). Without this the absolutely-
       positioned ::before paints over in-flow content and hides the logo. */
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 2rem;
    padding-block: 0.5rem;
    transition: padding-block var(--motion-base) var(--ease-out-expo);
}
@media (min-width: 1024px) {
    .site-nav__inner {
        padding-block: 1.75rem;
    }
}
.site-nav.is-scrolled .site-nav__inner {
    padding-block: 0.75rem;
}
/* Mobile-only inner-padding override for the scrolled state. The
   brand-svg portion of this is set later in the file (after the global
   .is-scrolled .brand--svg rule) so the mobile override actually wins
   the cascade. */
@media (max-width: 767px) {
    .site-nav.is-scrolled .site-nav__inner {
        padding-block: 0.5rem;
    }
}

.site-nav__brand {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.05em;
    line-height: 0.9;
    transition: opacity var(--motion-fast) var(--ease-default);
}
.site-nav__brand:hover,
.site-nav__brand:focus-visible {
    opacity: 0.92;
}

.site-nav__brand-mark {
    font-family: "Fraunces", Georgia, serif;
    font-size: 2.75rem;            /* mobile size */
    font-weight: 600;
    font-style: italic;
    letter-spacing: -0.05em;
    line-height: 0.82;
    /* text-mesh utility provides the animated gradient fill. */
    transition:
        font-size var(--motion-base) var(--ease-out-expo);
}
/* Desktop "hero" size kicks in only past iPad — at 768-1024 the brand
 * needs to share the row with 4 nav links, the mode toggle, and the CTA,
 * so it has to stay at the smaller mobile size. */
@media (min-width: 1024px) {
    .site-nav__brand-mark {
        font-size: 6rem;
    }
}
.site-nav.is-scrolled .site-nav__brand-mark {
    /* Sub is hidden in scrolled state, so D3 can run a bit bigger as the
       only brand element. */
    font-size: 2.5rem;
}
@media (min-width: 1024px) {
    .site-nav.is-scrolled .site-nav__brand-mark {
        font-size: 3.5rem;
    }
}

.site-nav__brand-sub {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.5rem;             /* mobile — mini, etched-label feel */
    font-weight: 500;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--fg-muted);
    margin-top: 0.4em;
    /* Subtle "breath" animation — letter-spacing wobbles, opacity fades. */
    animation: brand-sub-breath 8s ease-in-out infinite;
    transition:
        font-size var(--motion-base) var(--ease-out-expo),
        letter-spacing var(--motion-base) var(--ease-out-expo);
}
@media (min-width: 1024px) {
    .site-nav__brand-sub {
        font-size: 0.55rem;
    }
}
.site-nav.is-scrolled .site-nav__brand-sub {
    /* Hidden once the user scrolls — D3 alone carries the brand at that point. */
    display: none;
}

/* The D and the 3 each get their own .text-mesh wrapper so they animate
   independently. The 3 reverses direction + runs faster so it visually
   distinguishes from the D.

   Tiny padding-right on each: italic ink (and even upright glyph curves)
   can extend past the glyph's advance width. The bg gradient only fills
   the element's padding-box, so without padding the right edge of each
   glyph has nothing to fill from and renders unfilled/cut. 0.05em is
   small enough that any bg leak isn't visible. */
/* Both D and 3 sit as inline-block with vertical-align: top — their TOP edges
   align with each other (and with the line top). 3 is much smaller so it
   visually appears at the upper-right of the D's cap area, with the explicit
   margin-left providing clear separation (no overlap). */
.brand-mark-d {
    display: inline-block;
    vertical-align: top;
    padding-right: 0.2em;
}
.brand-mark-exp {
    display: inline-block;
    vertical-align: top;
    margin-left: -3px;
    padding-right: 0.1em;
    font-size: 0.5em;
    font-style: normal;
    line-height: 1;
}
/* Override animation on the 3 — compound selector beats single-class
   .text-mesh from utilities/mesh.css regardless of import order. */
.brand-mark-exp.text-mesh {
    animation-direction: reverse;
    animation-duration: 12s;
}

/* Hover-driven shine — a bright white gradient line sweeps across each
   letter when the brand is hovered, then sweeps back the opposite way
   when the cursor leaves. No looping. Implemented as a ::after pseudo
   that uses attr(data-text) to duplicate the letter, layered on top of
   the mesh. Transition (not animation) gives us the natural reverse on
   un-hover. The 3 staggers 0.2s after the D in both directions. */
.brand-mark-d,
.brand-mark-exp {
    position: relative;
}
.brand-mark-d::after,
.brand-mark-exp::after {
    content: attr(data-text);
    position: absolute;
    inset: 0;
    /* Wide soft fade zones — 18% gradient width on each side of the peak —
       so the shine reads as a glowing band, not a hard white line. Both
       transparent stops are rgba(255,255,255,0) (white at alpha 0) to
       avoid the sRGB interpolation smudge that 'transparent' (rgba 0,0,0,0)
       creates between dark and white at zero alpha. */
    background-image: linear-gradient(
        110deg,
        rgba(255, 255, 255, 0) 32%,
        rgba(255, 255, 255, 0.95) 50%,
        rgba(255, 255, 255, 0) 68%
    );
    background-size: 200% 100%;
    background-repeat: no-repeat;
    background-position: -50% 50%;     /* idle: peak well past right edge, soft fade also off-screen */
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    pointer-events: none;
    transition: background-position 0.08s linear;
}
.brand-mark-exp::after {
    transition-delay: 0.05s;
}
.site-nav__brand:hover .brand-mark-d::after,
.site-nav__brand:hover .brand-mark-exp::after,
.site-footer__brand:hover .brand-mark-d::after,
.site-footer__brand:hover .brand-mark-exp::after,
.site-nav__brand:focus-visible .brand-mark-d::after,
.site-nav__brand:focus-visible .brand-mark-exp::after {
    background-position: 150% 50%;     /* hovered: peak well past left edge, fade zones also off-screen */
}

@keyframes brand-sub-breath {
    0%, 100% { letter-spacing: 0.22em; opacity: 0.78; }
    50%      { letter-spacing: 0.27em; opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .site-nav__brand-sub,
    .brand-mark-d::after,
    .brand-mark-exp::after {
        animation: none !important;
        opacity: 0 !important;
    }
}

.site-nav__links {
    display: none;
    gap: 1.75rem;
    font-size: 0.9375rem;
}
.site-nav__links a,
.site-nav__drawer a:not(.btn) {
    /* Default state: muted text. Mesh gradient sits behind, clipped to glyph
       shape but invisible because text color is opaque. On hover, color goes
       transparent and the mesh shows through + animates. Same tile setup as
       .bg-mesh so the loop is seamless. */
    color: var(--fg-muted);
    background-image: var(--mesh-grad);
    background-size: 200% 100%;
    background-repeat: repeat;
    background-position: 0% 50%;
    -webkit-background-clip: text;
            background-clip: text;
    transition: color var(--motion-fast) var(--ease-default);
}
.site-nav__links a:hover,
.site-nav__links a:focus-visible,
.site-nav__drawer a:not(.btn):hover,
.site-nav__drawer a:not(.btn):focus-visible {
    color: transparent;
    animation: mesh-flow 4s linear infinite;
}

/* Active route — same mesh-gradient fill as hover, but permanent. The link
 * stays "lit up" while the user is anywhere within that section. */
.site-nav__links a.is-active,
.site-nav__drawer a.is-active:not(.btn) {
    color: transparent;
    animation: mesh-flow 4s linear infinite;
}

.site-nav__cta {
    display: inline-flex;
    align-items: center;
    gap: 1rem;
}

.site-nav__hamburger {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    width: 48px;
    height: 42px;
    gap: 8px;
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
}
.site-nav__hamburger span {
    display: block;
    width: 32px;
    height: 3px;
    background: var(--fg);
    border-radius: 2px;
    transition:
        transform var(--motion-fast) var(--ease-default),
        opacity var(--motion-fast) var(--ease-default),
        background var(--motion-fast) var(--ease-default);
}
/* Closed state: middle bar pops in blue. */
.site-nav__hamburger span:nth-child(2) {
    background: oklch(0.62 0.18 250);
}
/* X animation: top + bottom rotate, middle fades. The two rotated bars
 * shift to red so the X reads as a "close/cancel" affordance. The
 * translateY value is gap + bar height so the bars meet at the center
 * (8px gap + 3px bar = 11px). */
.site-nav.is-open .site-nav__hamburger span:nth-child(1) {
    transform: translateY(11px) rotate(45deg);
    background: oklch(0.6 0.22 25);
}
.site-nav.is-open .site-nav__hamburger span:nth-child(2) {
    opacity: 0;
}
.site-nav.is-open .site-nav__hamburger span:nth-child(3) {
    transform: translateY(-11px) rotate(-45deg);
    background: oklch(0.6 0.22 25);
}

.site-nav__drawer-cta {
    /* Book button + theme toggle on the same row at the bottom of the drawer. */
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-top: auto;        /* push to the bottom of the flex column */
    padding-top: 1.5rem;
    border-top: 1px solid var(--border);
}
.site-nav__drawer-cta .btn {
    flex: 1 1 auto;          /* book button takes the available space */
}

.site-nav__drawer {
    /* Full-screen drawer — fills viewport below the header. */
    position: fixed;
    top: 5.5rem;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 49;
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    padding: 2rem var(--gutter-page) 2rem;
    background: var(--bg);
    border-top: 1px solid var(--border);
    font-size: 2rem;
    font-family: "Fraunces", Georgia, serif;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.site-nav-wrap.is-scrolled .site-nav__drawer {
    top: 3.5rem;
}

/* Fade the drawer in when it opens. */
.site-nav__drawer:not([hidden]) {
    animation: drawer-in 300ms ease;
}
@keyframes drawer-in {
    from { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
    .site-nav__drawer:not([hidden]) { animation: none; }
}

@media (min-width: 768px) {
    .site-nav__links { display: inline-flex; }
    .site-nav__hamburger { display: none; }
    .site-nav__drawer { display: none !important; }
    /* Keep nav-link text and CTA labels on a single line at iPad widths. */
    .site-nav__links a,
    .site-nav__cta .btn {
        white-space: nowrap;
    }
}

/* Mobile: hide the Book button in the header — it's available in the
 * drawer below. The theme toggle and hamburger remain visible side-by-side.
 * Chained .site-nav__cta selector (and !important on the property) so the
 * later, less-specific .btn { display: inline-flex } rule can't override. */
@media (max-width: 767.98px) {
    .site-nav__cta .site-nav__book { display: none !important; }
    .site-nav__cta { gap: 0.75rem; }
}

/* Tablet (768-1023px): tighten the gap and CTA padding so all the items
 * still fit on one row alongside the brand mark. */
@media (min-width: 768px) and (max-width: 1023.98px) {
    .site-nav__links { gap: 1.25rem; }
    .site-nav__cta { gap: 0.75rem; }
    .site-nav__cta .btn {
        padding-inline: 1rem;
        font-size: 0.875rem;
    }
}

/* Buttons used by the CTA. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5em;
    padding: 0.85rem 1.5rem;
    border-radius: 999px;
    font-family: "Geist", system-ui, sans-serif;
    font-size: 0.9375rem;
    font-weight: 500;
    letter-spacing: 0.04em;
    line-height: 1;
    isolation: isolate;
}
.btn--primary {
    color: var(--fg-on-accent);
    /* Just the mesh — no conic-gradient border, no default drop shadow. */
    background-image: var(--mesh-grad);
    background-size: 200% 100%;
    background-repeat: repeat;
    background-position: 0% 50%;
    /* Position context + overflow clip for the shine ::after. */
    position: relative;
    overflow: hidden;
    will-change: background-position, transform;
    animation: mesh-flow 18s linear infinite;
    transition:
        transform var(--motion-base) cubic-bezier(0.16, 1, 0.3, 1),
        box-shadow var(--motion-base) cubic-bezier(0.16, 1, 0.3, 1),
        filter var(--motion-fast) ease;
}

/* Shine sweep on hover — same technique as the D3 letters but here it
   paints across the full button surface (no bg-clip: text). White band
   slides left → right on hover, right → left on leave via transition. */
.btn--primary::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: linear-gradient(
        110deg,
        rgba(255, 255, 255, 0) 32%,
        rgba(255, 255, 255, 0.55) 50%,
        rgba(255, 255, 255, 0) 68%
    );
    background-size: 200% 100%;
    background-repeat: no-repeat;
    background-position: -50% 50%;
    pointer-events: none;
    border-radius: inherit;
    transition: background-position 0.25s linear;
}

.btn--primary:hover,
.btn--primary:focus-visible {
    animation-duration: 7s;
    filter: saturate(1.15) brightness(1.06);
    transform: translateY(-3px);
    /* Subtle accent glow under the button — toned down from the previous
     * loud halo (was 22px/60px @ 50% accent). */
    box-shadow: 0 8px 24px color-mix(in oklch, var(--accent) 22%, transparent);
}

.btn--primary:hover::after,
.btn--primary:focus-visible::after {
    background-position: 150% 50%;
}

@media (prefers-reduced-motion: reduce) {
    .btn--primary {
        animation: none !important;
        background-position: 50% 50% !important;
    }
}

/* ---------------------------------------------------------------
   SVG brand mark (nav + footer). Outlined paths from Illustrator,
   shipped via src/partials/brand.html. Paths inherit color
   from the surrounding text color via currentColor, so dark-mode
   handling needs no extra rules. overflow: hidden clips Illustrator
   export artifacts that sit outside the viewBox.

   Sizing is height-driven; width auto preserves the 326x271.6 ratio.
   --------------------------------------------------------------- */
.brand-svg {
    display: block;
    height: 100%;
    width: auto;
    overflow: hidden;
    color: var(--fg);
    transition: filter var(--motion-base) var(--ease-default);
}
/* ========= MESH STOP COLORS ========= */
/* Light mode = the standard --mesh-grad palette (L 0.46-0.50). Dark
   mode brightens each stop to L 0.68-0.72 (matching the --mesh-grad
   dark-mode override in tokens.css) so the logo's mesh fill reads
   cleanly against the dark page bg. */
.brand-svg .mesh-stop-0  { stop-color: oklch(0.46 0.22 232); }
.brand-svg .mesh-stop-1  { stop-color: oklch(0.48 0.24 246); }
.brand-svg .mesh-stop-2  { stop-color: oklch(0.50 0.25 258); }
.brand-svg .mesh-stop-3  { stop-color: oklch(0.49 0.25 270); }
.brand-svg .mesh-stop-4  { stop-color: oklch(0.47 0.26 283); }
.brand-svg .mesh-stop-5  { stop-color: oklch(0.46 0.27 296); }
.brand-svg .mesh-stop-6  { stop-color: oklch(0.46 0.25 305); }
.brand-svg .mesh-stop-7  { stop-color: oklch(0.47 0.26 290); }
.brand-svg .mesh-stop-8  { stop-color: oklch(0.48 0.25 275); }
.brand-svg .mesh-stop-9  { stop-color: oklch(0.50 0.25 260); }
.brand-svg .mesh-stop-10 { stop-color: oklch(0.48 0.24 246); }
.brand-svg .mesh-stop-11 { stop-color: oklch(0.46 0.22 232); }

[data-theme="dark"] .brand-svg .mesh-stop-0  { stop-color: oklch(0.68 0.18 232); }
[data-theme="dark"] .brand-svg .mesh-stop-1  { stop-color: oklch(0.70 0.20 246); }
[data-theme="dark"] .brand-svg .mesh-stop-2  { stop-color: oklch(0.72 0.20 258); }
[data-theme="dark"] .brand-svg .mesh-stop-3  { stop-color: oklch(0.71 0.20 270); }
[data-theme="dark"] .brand-svg .mesh-stop-4  { stop-color: oklch(0.69 0.21 283); }
[data-theme="dark"] .brand-svg .mesh-stop-5  { stop-color: oklch(0.68 0.22 296); }
[data-theme="dark"] .brand-svg .mesh-stop-6  { stop-color: oklch(0.68 0.20 305); }
[data-theme="dark"] .brand-svg .mesh-stop-7  { stop-color: oklch(0.69 0.21 290); }
[data-theme="dark"] .brand-svg .mesh-stop-8  { stop-color: oklch(0.70 0.20 275); }
[data-theme="dark"] .brand-svg .mesh-stop-9  { stop-color: oklch(0.72 0.20 260); }
[data-theme="dark"] .brand-svg .mesh-stop-10 { stop-color: oklch(0.70 0.20 246); }
[data-theme="dark"] .brand-svg .mesh-stop-11 { stop-color: oklch(0.68 0.18 232); }

/* ========= D AND 3: STROKE DRAW-ON + MESH FILL-IN ========= */
/* On first paint, D and 3 outline themselves like ink, then the mesh
   fades in underneath. pathLength=100 (set in the SVG markup) lets us
   keep stroke-dasharray/-offset normalized to 100 regardless of actual
   path length. The 3 staggers a quarter-second behind the D. */
.brand-svg .brand-d,
.brand-svg .brand-3 {
    stroke: currentColor;
    stroke-width: 1.5;
    stroke-dasharray: 100;
    stroke-dashoffset: 100;
    fill-opacity: 0;
    animation:
        d3-stroke-draw 1.0s ease-out both,
        d3-fill-in    0.5s ease-out 1.0s both;
}
.brand-svg .brand-d { fill: url(#d3-mesh-flow); }
.brand-svg .brand-3 {
    fill: url(#d3-mesh-flow-rev);
    animation:
        d3-stroke-draw 1.0s ease-out 0.2s both,
        d3-fill-in    0.5s ease-out 1.2s both;
}
@keyframes d3-stroke-draw {
    from { stroke-dashoffset: 100; stroke-opacity: 1; }
    to   { stroke-dashoffset: 0;   stroke-opacity: 1; }
}
@keyframes d3-fill-in {
    from { fill-opacity: 0; stroke-opacity: 1; }
    to   { fill-opacity: 1; stroke-opacity: 0; }
}

/* ========= MESH GRADIENT FLOW ========= */
/* Translate the gradient by one tile width (200 SVG units, matching
   x2 on the linearGradient). spreadMethod=repeat makes the visible
   window land on an identical tile, so the loop is seamless. The 3
   runs reverse to mirror the old text-mesh override behavior. */
.brand-svg #d3-mesh-flow {
    animation: d3-mesh-translate 18s linear infinite;
    transform-origin: 0 0;
}
.brand-svg #d3-mesh-flow-rev {
    animation: d3-mesh-translate 12s linear infinite reverse;
    transform-origin: 0 0;
}
@keyframes d3-mesh-translate {
    from { transform: translateX(0); }
    to   { transform: translateX(-200px); }
}

/* ========= SUBTITLE: PER-LETTER SCRAMBLE-IN + ROLLING WAVE ========= */
/* Each letter is wrapped in a <g class="brand-sub-letter"> in the SVG;
   the path inside carries the load-in scramble + continuous wave, while
   the wrapper hosts the hover-glitch (further down). Splitting them
   keeps the hover rule from restarting the load animations on hover-out.
   --i (letter index) is set on the wrapper and inherits to the path. */
.brand-svg .brand-sub path {
    fill: currentColor;
    opacity: 0;
    transform-box: fill-box;
    transform-origin: 50% 50%;
    animation:
        sub-scramble-in 0.55s cubic-bezier(0.34, 1.4, 0.64, 1) calc(1.2s + var(--i, 0) * 0.04s) both,
        sub-letter-wave 4.5s  ease-in-out                      calc(2.0s + var(--i, 0) * 0.04s) infinite;
}
.brand-svg .brand-sub-letter {
    transform-box: fill-box;
    transform-origin: 50% 50%;
}
@keyframes sub-scramble-in {
    from {
        opacity: 0;
        transform: translate(var(--scramble-x, 0), var(--scramble-y, 0)) rotate(var(--scramble-r, 0deg));
    }
    to {
        opacity: 0.55;
        transform: translate(0, 0) rotate(0deg);
    }
}
@keyframes sub-letter-wave {
    0%, 100% { opacity: 0.55; }
    50%      { opacity: 0.95; }
}
/* Pseudo-random scramble offsets per letter — alternating signs and
   varied magnitudes so the reveal feels typed by hand, not mechanical. */
.brand-svg .brand-sub > g:nth-child(1)  { --scramble-x:  6px;  --scramble-y: -10px; --scramble-r: -8deg; }
.brand-svg .brand-sub > g:nth-child(2)  { --scramble-x: -9px;  --scramble-y:  12px; --scramble-r:  6deg; }
.brand-svg .brand-sub > g:nth-child(3)  { --scramble-x:  4px;  --scramble-y:  14px; --scramble-r: -4deg; }
.brand-svg .brand-sub > g:nth-child(4)  { --scramble-x: 10px;  --scramble-y: -8px;  --scramble-r: 10deg; }
.brand-svg .brand-sub > g:nth-child(5)  { --scramble-x: -6px;  --scramble-y: -14px; --scramble-r: -6deg; }
.brand-svg .brand-sub > g:nth-child(6)  { --scramble-x:  8px;  --scramble-y:  10px; --scramble-r:  4deg; }
.brand-svg .brand-sub > g:nth-child(7)  { --scramble-x: -4px;  --scramble-y: -12px; --scramble-r:-10deg; }
.brand-svg .brand-sub > g:nth-child(8)  { --scramble-x:  7px;  --scramble-y:  9px;  --scramble-r:  8deg; }
.brand-svg .brand-sub > g:nth-child(9)  { --scramble-x: -8px;  --scramble-y:  13px; --scramble-r: -5deg; }
.brand-svg .brand-sub > g:nth-child(10) { --scramble-x:  5px;  --scramble-y: -11px; --scramble-r:  7deg; }
.brand-svg .brand-sub > g:nth-child(11) { --scramble-x: -7px;  --scramble-y: -9px;  --scramble-r: -3deg; }
.brand-svg .brand-sub > g:nth-child(12) { --scramble-x:  9px;  --scramble-y:  11px; --scramble-r:  9deg; }
.brand-svg .brand-sub > g:nth-child(13) { --scramble-x: -5px;  --scramble-y: -13px; --scramble-r: -7deg; }
.brand-svg .brand-sub > g:nth-child(14) { --scramble-x:  6px;  --scramble-y:  8px;  --scramble-r:  5deg; }

/* ========= HOVER SUBTITLE GLITCH ========= */
/* One-shot per-letter scramble that fires when the user hovers/focuses
   the brand link, then snaps back. Animation lives on the
   .brand-sub-letter wrapper so it composes with the path-level scramble +
   wave (which keep running uninterrupted). Each letter's duration varies
   slightly via --i so they desync — looks like an actual signal glitch
   instead of a synchronized shake. step-end timing snaps the keyframes
   for the choppy digital cadence. forwards fill-mode + iteration 1 mean
   the letter ends at translate(0,0) and stays there until hover ends.

   The group-level chromatic-aberration ghost is a separate one-shot
   animation on .brand-sub itself — flickers red/cyan drop-shadows
   alongside the per-letter jumps, then clears. */
.site-nav__brand--svg:hover         .brand-svg .brand-sub-letter,
.site-nav__brand--svg:focus-visible .brand-svg .brand-sub-letter,
.site-footer__brand--svg a:hover         .brand-svg .brand-sub-letter,
.site-footer__brand--svg a:focus-visible .brand-svg .brand-sub-letter {
    animation: brand-sub-glitch-letter 0.22s step-end 1 forwards;
    animation-delay: calc(var(--i, 0) * 0.008s);
}
@keyframes brand-sub-glitch-letter {
    0%   { transform: translate(0, 0); }
    25%  { transform: translate(7px, -3px); }
    50%  { transform: translate(-5px, 4px); }
    75%  { transform: translate(4px, 1px); }
    100% { transform: translate(0, 0); }
}

.site-nav__brand--svg:hover         .brand-svg .brand-sub,
.site-nav__brand--svg:focus-visible .brand-svg .brand-sub,
.site-footer__brand--svg a:hover         .brand-svg .brand-sub,
.site-footer__brand--svg a:focus-visible .brand-svg .brand-sub {
    animation: brand-sub-glitch-chroma 0.28s step-end 1 forwards;
}
@keyframes brand-sub-glitch-chroma {
    0%   { filter: none; }
    25%  { filter: drop-shadow(3px 0 0 oklch(0.65 0.27 25)) drop-shadow(-3px 0 0 oklch(0.78 0.18 200)); }
    50%  { filter: drop-shadow(-3px 0 0 oklch(0.65 0.27 25)) drop-shadow(3px 0 0 oklch(0.78 0.18 200)); }
    75%  { filter: drop-shadow(2px 0 0 oklch(0.65 0.27 25)) drop-shadow(-2px 0 0 oklch(0.78 0.18 200)); }
    100% { filter: none; }
}

/* ========= HOVER SHINE SWEEP ========= */
/* The shine rect lives at x=-326 in SVG coords (off-canvas to the left)
   and is clipped to glyph shapes via #d3-brand-clip in the markup.
   Implemented as a CSS transition (not an animation) on transform:
   on hover, translateX moves it across to the right; on hover-off,
   the transition runs back to the left, so the shine reverses. The
   gradient is wider/softer than a thin highlight so it reads as a
   true gradient sweep. */
.brand-svg .brand-shine {
    pointer-events: none;
    transform: translateX(0);
    transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.site-nav__brand--svg:hover         .brand-svg .brand-shine,
.site-nav__brand--svg:focus-visible .brand-svg .brand-shine,
.site-footer__brand--svg a:hover         .brand-svg .brand-shine,
.site-footer__brand--svg a:focus-visible .brand-svg .brand-shine {
    transform: translateX(652px);
}

/* ========= HOVER MESH BOOST ========= */
/* Speed up the gradient flow and pop saturation/brightness — same idea
   as the original text-mesh hover. Filter transitions in via the
   transition declared on .brand-svg above. */
.site-nav__brand--svg:hover .brand-svg #d3-mesh-flow,
.site-nav__brand--svg:focus-visible .brand-svg #d3-mesh-flow,
.site-footer__brand--svg a:hover .brand-svg #d3-mesh-flow,
.site-footer__brand--svg a:focus-visible .brand-svg #d3-mesh-flow {
    animation-duration: 6s;
}
.site-nav__brand--svg:hover .brand-svg #d3-mesh-flow-rev,
.site-nav__brand--svg:focus-visible .brand-svg #d3-mesh-flow-rev,
.site-footer__brand--svg a:hover .brand-svg #d3-mesh-flow-rev,
.site-footer__brand--svg a:focus-visible .brand-svg #d3-mesh-flow-rev {
    animation-duration: 4s;
}
.site-nav__brand--svg:hover .brand-svg,
.site-nav__brand--svg:focus-visible .brand-svg,
.site-footer__brand--svg a:hover .brand-svg,
.site-footer__brand--svg a:focus-visible .brand-svg {
    filter: saturate(1.18) brightness(1.05);
}

/* ========= REDUCED MOTION ========= */
/* Snap to the final state — full mesh fill, no stroke, subtitle visible
   at a calm opacity. No flowing, no drawing, no scrambling. */
@media (prefers-reduced-motion: reduce) {
    .brand-svg #d3-mesh-flow,
    .brand-svg #d3-mesh-flow-rev,
    .brand-svg .brand-d,
    .brand-svg .brand-3,
    .brand-svg .brand-sub,
    .brand-svg .brand-sub-letter,
    .brand-svg .brand-sub path,
    .brand-svg .brand-shine {
        animation: none !important;
    }
    .brand-svg .brand-d,
    .brand-svg .brand-3 {
        fill-opacity: 1;
        stroke-opacity: 0;
    }
    .brand-svg .brand-sub path {
        opacity: 0.7;
        transform: none;
    }
    .brand-svg .brand-sub,
    .brand-svg .brand-sub-letter {
        filter: none !important;
        transform: none !important;
    }
}

.site-nav__brand--svg {
    /* Override the column flex from .site-nav__brand — the SVG is one
       child, no stacked sub-text. Height drives size; the anchor
       hugs the SVG. */
    display: inline-flex;
    align-items: center;
    height: 3.25rem;                      /* mobile */
    transition: height var(--motion-base) var(--ease-out-expo);
}
@media (min-width: 1024px) {
    .site-nav__brand--svg {
        height: 5.5rem;
    }
}
.site-nav.is-scrolled .site-nav__brand--svg {
    height: 3rem;
}
@media (min-width: 1024px) {
    .site-nav.is-scrolled .site-nav__brand--svg {
        height: 4rem;
    }
}
/* Mobile only: lock the brand-svg height across non-scrolled and
   scrolled states. The logo is already small at mobile sizes and any
   further shrink reads as a flicker at the scroll threshold. Lives
   after the global is-scrolled rule so it wins the cascade. */
@media (max-width: 767px) {
    .site-nav.is-scrolled .site-nav__brand--svg {
        height: 3.25rem;
    }
}

.site-footer__brand--svg {
    height: 4rem;
}
.site-footer__brand--svg a {
    display: inline-flex;
    align-items: center;
    height: 100%;
}

/* Hero — type-led, no canvas, with massive outline-typography bg + scroll parallax + entry stagger.
   Aesthetic: editorial / counter-typography (mono "30" + serif headline). No rainbow. */

.hero {
    position: relative;
    /* Bound the hero: fits the content, capped at the viewport on big screens. */
    min-height: clamp(940px, 100dvh, 1140px);
    display: flex;
    align-items: stretch;
    isolation: isolate;
    margin-top: -1px;
    background: var(--bg);
}

.hero::before {
    /* Soft warm vignette behind the type — clipped to hero only so it can't
       bleed up behind the transparent nav. */
    content: "";
    position: absolute;
    inset: 0;
    background:
        radial-gradient(ellipse 70% 50% at 25% 30%, color-mix(in oklch, var(--accent) 14%, transparent), transparent 60%),
        radial-gradient(ellipse 50% 40% at 80% 75%, color-mix(in oklch, var(--accent) 6%, transparent), transparent 65%);
    pointer-events: none;
    z-index: 0;
}

/* Massive outline typography in the bg — the actual "wow" element.
   z-index: -1 sends ALL of .hero__bg's contents (D3 outline + portrait)
   to the back. Visible because .hero has isolation: isolate creating a
   stacking context, AND .hero's background is painted via ::before
   (which renders above z=-1 children). So the bg+portrait are above the
   page background but below all other hero content. */
.hero__bg {
    /* No z-index — relies on source order (this element appears before
     * .hero__inner in markup) so the portrait paints behind the content
     * naturally. A negative z-index would create a separate stacking
     * context that hides the bg from any descendant's backdrop-filter. */
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.hero__bg-mark {
    position: absolute;
    font-family: "Fraunces", Georgia, serif;
    font-weight: 500;
    line-height: 0.75;
    letter-spacing: -0.05em;
    white-space: nowrap;     /* prevent the huge mono "since 2001" from wrapping into 2 lines */
    -webkit-text-stroke: 1px color-mix(in oklch, var(--fg) 16%, transparent);
    text-stroke: 1px color-mix(in oklch, var(--fg) 16%, transparent);
    color: transparent;
    user-select: none;
}

.hero__bg-mark--1 {
    /* Pulled up far enough to span behind the transparent nav at top of
       viewport (nav is ~188px tall when not scrolled, plus extra so
       outline visually crops into viewport edge). Right offset is positive
       so more of the "3" shows in viewport. */
    top: clamp(-280px, -22vw, -160px);
    right: 3vw;
    font-size: clamp(20rem, 50vw, 56rem);
    font-style: italic;
    transform: rotate(-4deg);
}

/* Tom Dean portrait — vertically centered, anchored to the right edge.
   z-index: 0 inside .hero__bg (which itself is z-index: -1). The whole
   .hero__bg sits below .hero__inner — so .hero__ctas-bg ("since 2001")
   inside .hero__inner renders ABOVE the portrait. */
.hero__portrait {
    position: absolute;
    top: 50%;
    /* Anchored to the .page container's inner right edge minus a 20px nudge
     * (originally aligned exactly to the container; user wanted a bit of
     * extra rightward shift, no more). On mobile this gets overridden to
     * pin flush against the viewport edge — no gutter to spare there. */
    right: calc(max(var(--gutter-page), (100% - var(--max-page)) / 2 + var(--gutter-page)) - 20px);
    translate: 0 -50%;
    height: 92%;
    width: auto;
    max-width: 55%;
    object-fit: contain;
    object-position: right center;
    z-index: 0;
    pointer-events: none;
    user-select: none;
}
/* Theme-swap: light variant default in both themes if no dark image is
 * uploaded; if the dark variant exists, [data-theme="dark"] flips them. */
.hero__portrait--dark {
    display: none;
}
[data-theme="dark"] .hero__portrait--light {
    display: none;
}
[data-theme="dark"] .hero__portrait--dark {
    display: block;
}
/* Mobile: pin portrait flush to the viewport right edge (no gutter,
 * to use every available pixel) and fade out its bottom edge. */
@media (max-width: 1023.98px) {
    .hero__portrait {
        right: 0;
        -webkit-mask-image: linear-gradient(
            to bottom,
            black 0%,
            black 75%,
            transparent 100%
        );
                mask-image: linear-gradient(
            to bottom,
            black 0%,
            black 75%,
            transparent 100%
        );
    }
}
/* "since 2001" mark now lives inside .hero__ctas (see partial) — positioned
   absolutely RELATIVE to the button row so it always centers on the buttons
   regardless of viewport size. */

[data-theme="light"] .hero__bg-mark {
    -webkit-text-stroke-color: color-mix(in oklch, var(--fg) 12%, transparent);
            text-stroke-color: color-mix(in oklch, var(--fg) 12%, transparent);
}
[data-theme="light"] .hero__bg-mark--2 {
    color: color-mix(in oklch, var(--fg) 5%, transparent);
}

.hero__inner {
    /* No z-index here. A positive z-index would establish a stacking
     * context that hides .hero__bg (z-index: -1) from any descendant's
     * backdrop-filter — i.e. the ghost button could no longer "see" the
     * portrait behind it and the blur would render as flat. .hero already
     * has isolation: isolate, so source order + .hero__bg's negative
     * z-index correctly stacks the bg behind .hero__inner content. */
    position: relative;
    display: flex;
    flex-direction: column;
    /* Content at top, just below the visible nav. Bottom padding is small —
       the bg "since 2001" extends below the CTAs.
       Top padding lifted (was 8-10rem) so the headline sits closer to the
       nav and uses less of the hero's vertical space. */
    justify-content: flex-start;
    gap: 1.25rem;
    padding-block: calc(clamp(5rem, 9vh, 7rem) - 20px) clamp(2rem, 4vh, 3rem);
    width: 100%;
    max-width: var(--max-page);
}

.hero__eyebrow {
    color: var(--fg-muted);
}

.hero__title {
    font-family: "Fraunces", Georgia, serif;
    margin: 0;
    line-height: 0.9;
    letter-spacing: -0.04em;
    font-weight: 500;
    font-variation-settings: "opsz" 144, "SOFT" 50, "WONK" 0;
    color: var(--fg);
    text-wrap: balance;
}

.hero__title-row {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.25em 0.4em;
}
.hero__title-row--2 {
    margin-top: 0.05em;
}

.hero__big-num {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: clamp(6rem, 16vw, 16rem);
    font-weight: 200;
    letter-spacing: -0.06em;
    line-height: 0.78;
    color: var(--fg);
}

.hero__title-words {
    display: inline-flex;
    flex-direction: column;
    gap: 0.05em;
    line-height: 0.95;
}

.hero__title-word {
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(2.5rem, 6.5vw, 6rem);
    font-style: italic;
    font-weight: 400;
    color: var(--fg-muted);
}
.hero__title-word--1 {
    color: var(--fg);
    font-style: normal;
    font-weight: 500;
}
.hero__title-word--3,
.hero__title-word--4 {
    font-size: clamp(3rem, 8vw, 8rem);
    font-style: normal;
    font-weight: 500;
    color: var(--fg);
}
.hero__title-word--4 {
    /* Color comes from .text-mesh utility (animated gradient text fill). */
    font-style: italic;
}


.hero__subline {
    font-size: 1.1875rem;
    line-height: 1.55;
    color: var(--fg-muted);
    max-width: 56ch;
    margin: 1rem 0 0;
}
/* Tablet and below: portrait pins to the right of the hero, so the subline
 * has to be narrowed or it runs across the image and becomes unreadable. */
@media (max-width: 1023.98px) {
    .hero__subline {
        max-width: 65%;
    }
}
/* Mobile: shrink the subline so it doesn't dominate the column. */
@media (max-width: 767.98px) {
    .hero__subline {
        font-size: 1.1rem;
    }
}

.hero__ctas {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 1.5rem;
}

/* Big mono "since 2001" tucked behind the CTA row — sized big to halo the
   buttons. Absolute, centered on the cta row, so it always pins to the
   buttons regardless of viewport. Buttons paint over it via DOM document
   order (bg comes first in markup), so no z-index trickery needed and the
   bg stays above the portrait sitting in .hero__bg. */
/* "since 2001" — back in its original spot inside .hero__ctas, anchored to
 * the button row so it tracks button position across viewport widths.
 * Renders ABOVE the portrait (because .hero__inner sits above .hero__bg).
 * Mid-grey fill at 35% opacity reads as solid filled glyphs in BOTH themes:
 *   - Light mode (cream bg): mid-grey is darker → visible
 *   - Dark mode (dark bg): mid-grey is lighter → visible
 *   - Over Tom's dark pants: mid-grey is lighter → 0/1 fill solid in either theme */
.hero__ctas-bg {
    position: absolute;
    top: 60%;
    left: 50%;
    translate: -50% 0;
    pointer-events: none;
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: clamp(6rem, 12vw, 15rem);
    font-weight: 200;
    line-height: 0.75;
    letter-spacing: -0.04em;
    color: oklch(0.62 0 0 / 0.35);
    text-transform: uppercase;
    white-space: nowrap;
    user-select: none;
}
/* Mobile (<768px): smaller and lower so it doesn't overwhelm the headline. */
@media (max-width: 767.98px) {
    .hero__ctas-bg {
        font-size: clamp(3.5rem, 16vw, 6rem);
        top: calc(78% + 30px);
    }
    /* Bottom meta strip hidden on mobile — see the matching media query
     * after the base .hero__meta rule (this earlier block is overridden
     * by the base rule's `display: flex` because of source order). */
    /* Big "D3" outline drops 40px on mobile. */
    .hero__bg-mark--1 {
        top: calc(clamp(-280px, -22vw, -160px) + 40px);
    }
    /* Inner content (headline + CTAs) moves up 45px on mobile. */
    .hero__inner {
        padding-block: calc(clamp(5rem, 9vh, 7rem) - 65px) clamp(2rem, 4vh, 3rem);
    }
    /* Portrait moves up 100px on mobile and shrinks — the new tighter crop
     * was rendering too large at the previous 55% / 92% sizing. */
    .hero__portrait {
        top: calc(50% - 100px);
        max-width: 50%;
        height: 78%;
    }
    /* Drop the dash divider on mobile — too tight to read alongside the text. */
    .hero__meta-divider {
        display: none;
    }
}


.btn--ghost {
    /* Frosted-glass treatment: white fill + backdrop blur. The
     * will-change hint forces the browser to give the element its own
     * compositor layer with backdrop sampling enabled — without it some
     * Chromium builds short-circuit the blur on mobile. */
    background: oklch(1 0 0 / 0.2);
    backdrop-filter: blur(14px) saturate(140%);
    will-change: backdrop-filter;

    color: var(--fg);
    border: 1px solid var(--border);
    transition:
        background var(--motion-fast) var(--ease-default),
        border-color var(--motion-fast) var(--ease-default),
        transform var(--motion-fast) var(--ease-default);
}
.btn--ghost:hover,
.btn--ghost:focus-visible {
    background: oklch(1 0 0 / 0.14);
    /* --fg-subtle is one step softer than --fg-muted — lighter in light
     * mode (less contrast against cream bg), softer in dark mode. */
    border-color: var(--fg-subtle);
    transform: translateY(-1px);
}

/* Mobile-only: more white in the ghost button bg because the portrait sits
 * right behind it, and dark text over Tom's dark suit was unreadable.
 * Lives AFTER the desktop rules so it wins source-order. */
@media (max-width: 767.98px) {
    .btn--ghost {
        background: oklch(1 0 0 / 0.65);
    }
    .btn--ghost:hover,
    .btn--ghost:focus-visible {
        background: oklch(1 0 0 / 0.75);
    }
}

.hero__meta {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-top: 4rem;
    color: var(--fg-subtle);
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.7rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}
.hero__meta-divider {
    flex: 0 0 3rem;
    height: 1px;
    background: var(--border);
}
/* Mobile: hide the bottom meta strip. Lives after the base rule so it wins source order. */
@media (max-width: 767.98px) {
    .hero__meta {
        display: none;
    }
}

/* iPad Mini / small tablet (768-1023px): big D3 outline drops 20px so it
 * sits where Tom wants it relative to the hero content. The "since 2001"
 * mark and the bottom meta strip both shift down 100px. */
@media (min-width: 768px) and (max-width: 1023.98px) {
    .hero__bg-mark--1 {
        top: calc(clamp(-280px, -22vw, -160px) + 45px);
        right: calc(3vw - 5px);
    }
    .hero__ctas-bg {
        top: calc(60% + 100px);
        /* Align "since 2001" to the left edge of the inner content
         * instead of centered on the buttons (the centered layout
         * doesn't read well at tablet width). */
        left: 0;
        translate: 0 0;
    }
    .hero__meta {
        margin-top: calc(4rem + 100px);
    }
}

/* Mobile: the hero's min-height leaves a tall empty band under the content,
 * so pull the next section up into it. */
@media (max-width: 767.98px) {
    .hero {
        margin-bottom: calc(-1 * (8rem + 115px));
    }
}

/* Scroll-driven font-axis pulse on the hero title. */
@supports (animation-timeline: scroll()) {
    .hero__title-word--3 {
        animation: hero-pulse linear both;
        animation-timeline: scroll();
        animation-range: 0 70vh;
    }
    @keyframes hero-pulse {
        from { font-variation-settings: "wght" 500, "SOFT" 50; }
        to   { font-variation-settings: "wght" 800, "SOFT" 100; }
    }

    /* Background outline marks parallax slowly. */
    .hero__bg-mark--1 {
        animation: bg-mark-1-drift linear both;
        animation-timeline: scroll();
        animation-range: 0 100vh;
    }
    @keyframes bg-mark-1-drift {
        from { transform: rotate(-4deg) translateY(0); }
        to   { transform: rotate(-4deg) translateY(-200px); }
    }
}

/* Hero entry stagger — load-time reveal cascade. Children with [data-stagger]
   fade up sequentially. Drop the blur (was on load only, not on scroll —
   inconsistent feel). Just opacity + slight rise. */
.hero [data-stagger] {
    opacity: 0;
    transform: translateY(28px);
    animation: hero-stagger 1100ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
.hero [data-stagger="1"] { animation-delay: 100ms; }
.hero [data-stagger="2"] { animation-delay: 240ms; }
.hero [data-stagger="3"] { animation-delay: 380ms; }
.hero [data-stagger="4"] { animation-delay: 540ms; }
.hero [data-stagger="5"] { animation-delay: 680ms; }
.hero [data-stagger="6"] { animation-delay: 820ms; }
@keyframes hero-stagger {
    from { opacity: 0; transform: translateY(28px); }
    /* End at transform: none (not translateY(0)) so the element doesn't
       retain a stacking context after animation — the .hero__ctas-bg-front
       can then escape and render above the portrait via .hero__inner z=2. */
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .hero [data-stagger],
    .hero__bg-mark {
        animation: none !important;
        opacity: 1 !important;
        transform: none !important;
        filter: none !important;
    }
}

/* iPad Pro / mid-size laptop (1024-1599px): drop "since 2001" and the
 * meta strip 100px on these viewports. */
@media (min-width: 1024px) and (max-width: 1599.98px) {
    .hero__bg-mark--1 {
        top: calc(clamp(-280px, -22vw, -160px) + 50px);
        right: calc(3vw - 10px);
    }
    .hero__ctas-bg {
        top: calc(60% + 100px);
        left: 0;
        translate: 0 0;
    }
    .hero__meta {
        margin-top: calc(4rem + 100px);
    }
}

/* Ultrawide tweak: on screens >=2000px, push the giant "since 2001"
 * background text down 100px to sit lower in the bigger hero. The meta
 * strip nudges down. The big D3 background mark also drops 30px so it
 * doesn't sit too high in the larger viewport. */
@media (min-width: 2000px) {
    .hero__ctas-bg    { top: calc(60% + 100px); }
    .hero__meta       { margin-top: calc(4rem - 40px); }
    .hero__bg-mark--1 {
        top: calc(clamp(-280px, -22vw, -160px) + 60px);
        right: calc(3vw - 15px);
    }
}

/* Services: a grid of tiles. Hovering a tile scrambles its title in letter by letter. */

.bento {
    padding-block: 8rem 6rem;
    border-top: 1px solid var(--border);
}

.bento__header {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    max-width: 52rem;
    margin-bottom: 3rem;
}
.bento__lede {
    font-size: 1.125rem;
    color: var(--fg-muted);
    max-width: 52ch;
    margin-top: 0.5rem;
}

.bento__grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
}

.bento__tile {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding: 1.75rem 1.5rem 1.5rem;
    /* Lit from the upper left: ~5% lighter top-left, ~6% darker bottom-right, in both themes. */
    background:
        linear-gradient(
            155deg,
            color-mix(in oklch, oklch(1 0 0) 5%, var(--bg-elevated)) 0%,
            var(--bg-elevated) 50%,
            color-mix(in oklch, oklch(0 0 0) 6%, var(--bg-elevated)) 100%
        );
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    transition:
        transform var(--motion-fast) var(--ease-default),
        box-shadow var(--motion-fast) var(--ease-default),
        border-color var(--motion-fast) var(--ease-default);
}

.bento__tile:hover {
    transform: translateY(-3px);
    box-shadow:
        0 10px 28px oklch(0.42 0.28 248 / 0.14),
        0 4px 12px oklch(0 0 0 / 0.08);
    /* oklab, not oklch: mixing blue with the warm border in oklch passes through green. */
    border-color: color-mix(in oklab, var(--accent) 30%, var(--border));
}
/* In dark mode the mixed border is barely visible, so use a brighter blue and a glow. */
[data-theme="dark"] .bento__tile:hover {
    border-color: oklch(0.62 0.22 248);
    box-shadow:
        0 10px 32px oklch(0.62 0.22 248 / 0.32),
        0 4px 16px oklch(0.62 0.22 248 / 0.20);
}

.bento__tile--accent {
    background:
        linear-gradient(
            155deg,
            color-mix(in oklab, var(--accent) 32%, var(--bg-elevated)) 0%,
            color-mix(in oklab, var(--accent) 18%, var(--bg-elevated)) 60%,
            color-mix(in oklab, var(--accent) 6%, var(--bg-elevated)) 100%
        );
}

.bento__tile-title {
    font-family: "Fraunces", Georgia, serif;
    font-size: 1.5rem;
    line-height: 1.15;
    font-weight: 500;
    letter-spacing: -0.015em;
}
/* Per-letter spans added by the services script in site.js. */
.bento__tile-title__letter {
    display: inline-block;
    will-change: transform, opacity;
}
.bento__tile:hover .bento__tile-title__letter,
.bento__tile:focus-within .bento__tile-title__letter {
    animation: bento-title-scramble 0.5s cubic-bezier(0.34, 1.4, 0.64, 1) calc(var(--i, 0) * 0.02s) both;
}
@keyframes bento-title-scramble {
    from {
        opacity: 0;
        transform: translate(var(--sx, 0), var(--sy, 0)) rotate(var(--sr, 0deg));
    }
    to {
        opacity: 1;
        transform: translate(0, 0) rotate(0deg);
    }
}

.bento__tile-body {
    font-size: 0.9375rem;
    line-height: 1.55;
    color: var(--fg-muted);
}

@media (min-width: 640px) {
    .bento__grid {
        grid-template-columns: repeat(2, 1fr);
    }
}
@media (min-width: 1024px) {
    .bento__grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

@media (prefers-reduced-motion: reduce) {
    .bento__tile:hover { transform: none; }
    .bento__tile:hover .bento__tile-title__letter,
    .bento__tile:focus-within .bento__tile-title__letter {
        animation: none !important;
    }
}

/* Selected work — asymmetric 3-up grid */

.work-grid {
    padding-block: 6rem;
    border-top: 1px solid var(--border);
}

.work-grid__header {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    max-width: 52rem;
    margin-bottom: 3rem;
}
.work-grid__lede {
    font-size: 1.125rem;
    color: var(--fg-muted);
    max-width: 52ch;
    margin-top: 0.5rem;
}

.work-grid__grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1.5rem;
}

@media (min-width: 768px) {
    .work-grid__grid {
        grid-template-columns: repeat(12, 1fr);
        gap: 1.5rem 1.25rem;
    }
    .work-grid__grid > :nth-child(1) { grid-column: span 7; grid-row: span 2; }
    .work-grid__grid > :nth-child(2) { grid-column: span 5; }
    .work-grid__grid > :nth-child(3) { grid-column: span 5; }
}

@media (min-width: 1280px) {
    .work-grid__grid {
        /* row gap stays modest, column gap widens so the hero card has
           breathing room from the smaller stack on the right. */
        gap: 1.5rem 2.5rem;
    }
    .work-grid__grid > :nth-child(1) { grid-column: span 8; }
    .work-grid__grid > :nth-child(2) { grid-column: span 4; }
    .work-grid__grid > :nth-child(3) { grid-column: span 4; }
}

/* Work archive page (/work) — every project, simpler equal-size grid. */

.work-archive {
    padding-block: 8rem 6rem;
}
.work-archive__header {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    max-width: 56rem;
    margin-bottom: 4rem;
}
.work-archive__lede {
    font-size: 1.125rem;
    color: var(--fg-muted);
    max-width: 56ch;
    margin-top: 0.5rem;
}
.work-archive__grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 2.5rem 2rem;
}
@media (min-width: 720px) {
    .work-archive__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1100px) {
    .work-archive__grid { grid-template-columns: repeat(3, 1fr); }
}

/* Project card — used in home grid + later "next project" loop */

.project-card {
    display: grid;
    grid-template-rows: auto auto;
    gap: 1rem;
    /* On the home grid, the first card spans 2 parent rows. Without
       align-content: start, browsers can stretch the auto rows to fill
       the over-sized cell and the cover→meta gap balloons. start packs
       cover + meta to the top with the natural 1rem gap; any extra
       cell space sits as empty room below the meta. */
    align-content: start;
    color: inherit;
    isolation: isolate;
}

.project-card__cover {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius-lg);
    aspect-ratio: 4 / 3;
    background: var(--bg-elevated);
    /* Match the services bento border treatment — neutral border at
       rest, accent-tinted on hover. */
    border: 1px solid var(--border);
    transition:
        border-color var(--motion-fast) var(--ease-default),
        box-shadow var(--motion-fast) var(--ease-default);
}
.project-card:hover .project-card__cover,
.project-card:focus-visible .project-card__cover {
    border-color: color-mix(in oklab, var(--accent) 30%, var(--border));
    /* Same accent-tinted two-layer shadow as the services bento tiles. */
    box-shadow:
        0 10px 28px oklch(0.42 0.28 248 / 0.14),
        0 4px 12px oklch(0 0 0 / 0.08);
}
/* Dark mode: same fix as the bento hover border — the 30/70 mix
   resolves too dark to see against the dark card bg, so swap in a
   brighter standalone blue. Shadow becomes a brighter blue glow
   instead of the light-mode dark drop, since dark drops vanish on
   dark bg. */
[data-theme="dark"] .project-card:hover .project-card__cover,
[data-theme="dark"] .project-card:focus-visible .project-card__cover {
    border-color: oklch(0.62 0.22 248);
    box-shadow:
        0 10px 32px oklch(0.62 0.22 248 / 0.32),
        0 4px 16px oklch(0.62 0.22 248 / 0.20);
}

.project-card__cover-bg {
    position: absolute;
    inset: 0;
    /* Default placeholder gradient — used when no cover image is uploaded.
     * Once cover_image is set on the project entry, the <img> takes over. */
    background: linear-gradient(135deg, oklch(0.40 0.13 240), oklch(0.55 0.27 320));
    transition: transform var(--motion-slow) var(--ease-default), filter var(--motion-fast) var(--ease-default);
    transform-origin: center;
}

.project-card__cover-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--motion-slow) var(--ease-default), filter var(--motion-fast) var(--ease-default);
    transform-origin: center;
}
.project-card:hover .project-card__cover-img {
    filter: saturate(1.1);
}

.project-card:hover .project-card__cover-bg {
    filter: saturate(1.1);
}

.project-card__cover-mark {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(4rem, 14vw, 9rem);
    font-weight: 600;
    letter-spacing: -0.04em;
    color: oklch(0.99 0 0 / 0.92);
    mix-blend-mode: overlay;
    pointer-events: none;
}

.project-card__year {
    position: absolute;
    top: 1rem;
    right: 1rem;
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: oklch(1 0 0);
    /* Mesh gradient lives on the badge always; a translucent-black
       ::before overlay sits on top of it at rest and fades out on
       hover, revealing the mesh. background-image → background-image
       can't transition, but opacity on the overlay can. */
    background-image: var(--mesh-grad);
    background-size: 200% 100%;
    background-repeat: repeat;
    background-position: 0% 50%;
    animation: mesh-flow 7s linear infinite;
    padding: 0.35rem 0.6rem;
    border-radius: 999px;
    backdrop-filter: blur(6px);
    overflow: hidden;
    isolation: isolate;
}
.project-card__year::before {
    content: '';
    position: absolute;
    inset: 0;
    background: oklch(0 0 0 / 0.45);
    transition: opacity var(--motion-base) var(--ease-default);
    z-index: -1;
}
/* On hover/focus of the parent card, fade the black overlay out so
   the underlying mesh shows through. */
.project-card:hover .project-card__year::before,
.project-card:focus-visible .project-card__year::before {
    opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
    .project-card__year {
        animation: none;
        background-position: 50% 50%;
    }
    .project-card__year::before {
        transition: none;
    }
}

.project-card__meta {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.project-card__title {
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(1.5rem, 2.5vw, 2rem);
    line-height: 1.1;
    font-weight: 500;
    letter-spacing: -0.02em;
    margin: 0;
    /* Mesh-grad sits behind the text via background-clip: text. While the
       title's color is opaque (default), the color covers the gradient.
       On hover the color drops to transparent and the mesh shows through
       the glyph shapes, animated with the mesh-flow keyframes. */
    background-image: var(--mesh-grad);
    background-size: 200% 100%;
    background-repeat: repeat;
    background-position: 0% 50%;
    -webkit-background-clip: text;
    background-clip: text;
    transition: color var(--motion-fast) var(--ease-default);
}

.project-card:hover .project-card__title,
.project-card:focus-visible .project-card__title {
    color: transparent;
    animation: mesh-flow 6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
    .project-card:hover .project-card__title,
    .project-card:focus-visible .project-card__title {
        animation: none;
        background-position: 50% 50%;
    }
}

.project-card__summary {
    font-size: 0.9375rem;
    line-height: 1.55;
    color: var(--fg-muted);
    max-width: 52ch;
    margin: 0;
}

.project-card__tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0.25rem 0 0;
    padding: 0;
    list-style: none;
}
.project-card__tags li {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.6875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--fg-subtle);
    border: 1px solid var(--border);
    padding: 0.3rem 0.6rem;
    border-radius: 999px;
}

/* Project case study: /work/{slug} page layout */

/* Hero — image-only band with side fades ------------------------------- */

.project-page__hero {
    position: relative;
    /* Pull up under the fixed nav so the cover image extends to the top
       of the viewport. The nav-wrap above us reserves --nav-h of flow
       space site-wide; we eat that back here with negative margin. */
    margin-top: calc(-1 * var(--nav-h));
    min-height: calc(clamp(595px, 62dvh + 95px, 815px) + var(--nav-h));
    padding-top: var(--nav-h);
    overflow: hidden;
    isolation: isolate;
    background: var(--bg-elevated);
    /* Column flex with justify-end pushes the title content to the
       bottom of the band. The cover img is position: absolute so it
       sits outside flow and fills the band as bg; the content div is
       the only flex item, getting pushed to the bottom. */
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}

.project-page__cover {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Top-align the cover so the laptop screen stays in frame regardless
       of viewport height. */
    object-position: 50% 0%;
    /* Scale the image up so the laptop fills more of the viewport. The
       extra goes outside the container and gets clipped by hero's
       overflow:hidden. transform-origin: center top keeps the laptop
       top aligned to the band's top after scaling. */
    transform: translateY(20px) scale(1.8);
    transform-origin: center top;
    z-index: -1;
}
.project-page__cover--placeholder {
    background: linear-gradient(135deg, oklch(0.40 0.13 240), oklch(0.55 0.27 320));
}


/* On mobile, switch the cover from cover→contain so the laptop fits
   inside the viewport width without its body getting cropped on the
   sides. Top-aligned so the screen sits high in the band; the bg below
   the contained image is bg-elevated, which the bottom-fade ::after
   transitions into the title-band cleanly. Mask is disabled because
   the image already fits snug to the edges. */
@media (max-width: 767px) {
    /* Tighter min-height so the contained image fills more of the visible
       area instead of floating with empty bg above and below. The content
       area (min-height minus the nav-h padding-top) is sized close to the
       image's natural rendered height at typical mobile widths (320-767),
       so the laptop reads big without overflowing. */
    .project-page__hero {
        min-height: calc(clamp(360px, 50dvh, 480px) + var(--nav-h));
    }
    .project-page__cover {
        object-fit: contain;
        object-position: center top;
        mask-image: none;
        -webkit-mask-image: none;
        /* Reset the +20 translate from the base rule — on mobile the
           image should sit flush at the top of the band. */
        transform: translateY(0) scale(1.8);
    }
}

/* 640-767 wide (large mobile / small tablet) — keep the mobile
   contain + scale(1.8) treatment but lift the image 60px so more of
   the laptop fits in the visible window. Hero gets +100px on min-height
   so the band has more vertical room for the bigger laptop. */
@media (min-width: 640px) and (max-width: 767px) {
    .project-page__hero {
        min-height: calc(clamp(360px, 50dvh, 480px) + var(--nav-h) + 100px);
    }
    .project-page__cover {
        transform: translateY(-60px) scale(1.8);
    }
}

/* Tablet (768-1023) — smaller scale than mobile + nudged up so more of
   the laptop body sits in the visible band and less air at the top.
   Hero is 30px shorter than the base since the laptop scale is smaller
   here and the band looks balanced at less height. */
@media (min-width: 768px) and (max-width: 1023px) {
    .project-page__hero {
        min-height: calc(clamp(595px, 62dvh + 95px, 815px) + var(--nav-h) - 30px);
    }
}
@media (min-width: 768px) {
    .project-page__cover {
        transform: translateY(-70px) scale(1.0);
    }
}

/* Desktop — slightly lower vertical position than tablet so the laptop
   sits a touch more centered in the band on wider viewports. */
@media (min-width: 1024px) {
    .project-page__cover {
        transform: translateY(-20px) scale(1.25);
    }
}

/* 1024 with a SHORT viewport (e.g. 1024x768 / 1280x720 / 1280x800
   desktop monitors) — the band ends up shorter than tall-1024 viewports
   like iPad Pro portrait, so the laptop sits too high. Nudge it down
   15px. iPad Pro 12.9" portrait (1024x1366) and 11" landscape
   (1194x834) skip this rule via the max-height check. */
@media (min-width: 1024px) and (max-width: 1280px) and (max-height: 820px) {
    .project-page__cover {
        transform: translateY(-5px) scale(1.25);
    }
}

/* iPad Pro 12.9" portrait (1024x1366). Tall viewport in the 1024-1280
   range — pull the laptop up 90px from the standard 1024+ position +
   bump hero min-height by 75px so more image is visible. Min-height
   check excludes the short-height desktop rule above. */
@media (min-width: 1024px) and (max-width: 1280px) and (min-height: 1100px) {
    .project-page__hero {
        min-height: calc(clamp(595px, 62dvh + 95px, 815px) + var(--nav-h) + 75px);
    }
    .project-page__cover {
        transform: translateY(-15px) scale(1.0);
    }
}

/* Wide desktop — at 1440+ the laptop sits a bit higher to balance the
   extra vertical air the wider viewport gives the band. */
@media (min-width: 1440px) {
    .project-page__cover {
        transform: translateY(-10px) scale(1.0);
    }
}

/* Extra-wide — at 1600+ the laptop nudges up another 30px since the
   band's vertical space grows. */
@media (min-width: 1600px) {
    .project-page__cover {
        transform: translateY(-40px) scale(1.0);
    }
}

/* Ultra-wide — at 1900+ the hero band gets significantly taller so
   more of the laptop fits naturally instead of relying on the bottom
   fade to mask a hard cut. cover crops less when there's more vertical
   room. */
@media (min-width: 1900px) {
    .project-page__hero {
        min-height: calc(clamp(855px, 72dvh + 95px, 1055px) + var(--nav-h));
    }
    .project-page__cover {
        transform: translateY(-80px) scale(1.0);
    }
}

/* 1920 (Full HD) and up — image nudged 40px down from the 1900-1919
   position. Catches 1920x1080 monitors specifically. */
@media (min-width: 1920px) {
    .project-page__cover {
        transform: translateY(-40px) scale(1.0);
    }
}

/* 4K-ish — at 2000+ the hero ramps up in height and the laptop
   is shifted up so it sits framed high in the band. */
@media (min-width: 2000px) {
    .project-page__hero {
        min-height: calc(clamp(915px, 76dvh + 95px, 1115px) + var(--nav-h));
    }
    .project-page__cover {
        transform: translateY(-130px) scale(1.0);
    }
}


/* Hero content — back link / h1 / client, sits at the bottom of the
   hero band on top of the cover image. The bg gradient acts as the
   readability backdrop: solid var(--bg) at the bottom where text sits,
   fading to transparent upward so the laptop image is visible above.
   Outer is full-viewport-width so the gradient runs edge-to-edge;
   .project-page__hero-inner inside is page-constrained for the text. */
.project-page__hero-content {
    position: relative;
    z-index: 1;
    width: 100%;
    padding-block: 4rem 2.25rem;
    background: linear-gradient(
        to top,
        var(--bg) 0%,
        var(--bg) 50%,
        oklch(from var(--bg) l c h / 0.92) 60%,
        oklch(from var(--bg) l c h / 0.75) 70%,
        oklch(from var(--bg) l c h / 0.5) 80%,
        oklch(from var(--bg) l c h / 0.22) 90%,
        transparent 100%
    );
}
.project-page__hero-inner {
    display: flex;
    flex-direction: column;
    gap: 0.875rem;
}
@media (min-width: 1024px) {
    .project-page__hero-content {
        padding-block: 7rem 2.75rem;
    }
}

.project-page__back {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--fg-muted);
    text-decoration: none;
    align-self: flex-start;
    background-image: var(--mesh-grad);
    background-size: 200% 100%;
    background-repeat: repeat;
    background-position: 0% 50%;
    -webkit-background-clip: text;
            background-clip: text;
    transition: color var(--motion-fast) var(--ease-default);
}
.project-page__back:hover,
.project-page__back:focus-visible {
    color: transparent;
    animation: mesh-flow 4s linear infinite;
}
.project-page__back .arrow {
    display: inline-block;
    transition: transform var(--motion-fast) var(--ease-default);
}
.project-page__back:hover .arrow,
.project-page__back:focus-visible .arrow {
    color: var(--fg-muted);
    transform: translateX(-3px);
}
@media (prefers-reduced-motion: reduce) {
    .project-page__back:hover,
    .project-page__back:focus-visible {
        animation: none;
        color: var(--fg);
    }
    .project-page__back .arrow,
    .project-page__back:hover .arrow,
    .project-page__back:focus-visible .arrow {
        transition: none;
        transform: none;
    }
}

.project-page__title {
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(2.5rem, 7vw, 5.5rem);
    line-height: 0.98;
    font-weight: 500;
    letter-spacing: -0.025em;
    margin: 0;
    text-wrap: balance;
}

.project-page__client {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.875rem;
    letter-spacing: 0.04em;
    color: var(--fg-muted);
    margin: 0;
}

/* Meta strip ------------------------------------------------------------- */

.project-page__meta {
    border-block: 1px solid var(--border);
    background: var(--bg-elevated);
}
.project-page__meta-inner {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1.5rem;
    padding-block: 2.5rem;
}
.project-page__meta-item {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}
.project-page__meta-label {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--fg-subtle);
}
.project-page__meta-value {
    font-size: 1rem;
    color: var(--fg);
}
.project-page__summary {
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(1.125rem, 1.6vw, 1.375rem);
    line-height: 1.45;
    color: var(--fg);
    margin: 0;
    max-width: 56ch;
}

@media (min-width: 768px) {
    .project-page__meta-inner {
        grid-template-columns: repeat(4, 1fr);
        gap: 2.5rem;
        align-items: start;
    }
    .project-page__meta-item--summary {
        grid-column: span 4;
        margin-top: 0.5rem;
        padding-top: 1.5rem;
        border-top: 1px solid var(--border);
    }
}

/* Body — wraps Bard blocks ---------------------------------------------- */

.project-page__body {
    padding-block: 5rem 6rem;
    display: flex;
    flex-direction: column;
    gap: 4rem;
}

/* Next project teaser --------------------------------------------------- */

.project-page__next {
    position: relative;
    display: flex;
    align-items: flex-end;
    min-height: clamp(360px, 50dvh, 520px);
    overflow: hidden;
    isolation: isolate;
    color: inherit;
    text-decoration: none;
    transition: filter var(--motion-fast) var(--ease-default);
    border-top: 1px solid var(--border);
}
.project-page__next:hover { filter: brightness(1.05); }

.project-page__next-cover {
    position: absolute;
    inset: 0;
    z-index: -1;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--motion-slow) var(--ease-default);
}
.project-page__next:hover .project-page__next-cover { transform: scale(1.04); }

.project-page__next::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: linear-gradient(to top,
        oklch(from var(--bg) l c h / 0.85) 0%,
        oklch(from var(--bg) l c h / 0.4) 50%,
        transparent 100%);
}

.project-page__next-inner {
    width: 100%;
    padding-block: 4rem 3rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}
.project-page__next-label {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--fg-muted);
    background-image: var(--mesh-grad);
    background-size: 200% 100%;
    background-repeat: repeat;
    background-position: 0% 50%;
    -webkit-background-clip: text;
            background-clip: text;
    transition: color var(--motion-fast) var(--ease-default);
}
.project-page__next:hover .project-page__next-label,
.project-page__next:focus-visible .project-page__next-label {
    color: transparent;
    animation: mesh-flow 4s linear infinite;
}
.project-page__next-label .arrow {
    display: inline-block;
    transition: transform var(--motion-fast) var(--ease-default);
}
.project-page__next:hover .project-page__next-label .arrow,
.project-page__next:focus-visible .project-page__next-label .arrow {
    color: var(--fg-muted);
    transform: translateX(3px);
}
@media (prefers-reduced-motion: reduce) {
    .project-page__next:hover .project-page__next-label,
    .project-page__next:focus-visible .project-page__next-label {
        animation: none;
        color: var(--fg);
    }
    .project-page__next-label .arrow,
    .project-page__next:hover .project-page__next-label .arrow,
    .project-page__next:focus-visible .project-page__next-label .arrow {
        transition: none;
        transform: none;
    }
}
.project-page__next-title {
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(2rem, 5vw, 3.5rem);
    line-height: 1.05;
    font-weight: 500;
    letter-spacing: -0.02em;
    margin: 0;
}
.project-page__next-year {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--fg-subtle);
}

/* Case-study body blocks: text, pull quote, stat callout. Used inside .project-page__body. */

.block { width: 100%; }

/* text */

.block--text {
    margin-inline: auto;
    max-width: 64rem;
}
.block--text .prose {
    max-width: 64ch;
    margin-inline: auto;
    font-size: 1.0625rem;
    line-height: 1.7;
    color: var(--fg);
}
.block--text .prose > * + * { margin-top: 1.25em; }
.block--text .prose h2 {
    font-family: "Fraunces", Georgia, serif;
    font-size: 1.75rem;
    font-weight: 500;
    letter-spacing: -0.015em;
    margin-top: 2.5em;
}
.block--text .prose h3 {
    font-family: "Fraunces", Georgia, serif;
    font-size: 1.375rem;
    font-weight: 500;
    margin-top: 2em;
}
.block--text .prose a {
    color: var(--fg);
    text-decoration: underline;
    text-decoration-color: var(--accent);
    text-underline-offset: 4px;
}
.block--text .prose blockquote {
    margin-inline: 0;
    padding-inline-start: 1.5rem;
    border-inline-start: 2px solid var(--accent);
    font-style: italic;
    color: var(--fg-muted);
}

/* pull_quote */

.block--pull-quote {
    max-width: 56rem;
    margin-inline: auto;
    text-align: center;
    border: none;
    padding: 0;
}
.block__quote {
    font-family: "Fraunces", Georgia, serif;
    font-style: italic;
    font-size: clamp(1.5rem, 3.5vw, 2.5rem);
    line-height: 1.25;
    letter-spacing: -0.015em;
    color: var(--fg);
    text-wrap: balance;
    margin: 0;
}
.block__attribution {
    display: block;
    margin-top: 1.5rem;
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--fg-subtle);
    font-style: normal;
}

/* stat_callout */

.block--stat {
    text-align: center;
    padding-block: 2rem;
}
.block__stat-row {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
}
.block__stat-number {
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(4rem, 12vw, 9rem);
    line-height: 0.9;
    font-weight: 600;
    letter-spacing: -0.04em;
    color: var(--fg);
    background: var(--mesh-grad, linear-gradient(135deg, var(--accent), var(--accent-2, var(--accent))));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    /* line-height 0.9 keeps the headline tight, but descenders (the y in
       "days", g/p/q in numbers) bleed below the line-box and overlap the
       label below. 0.18em of bottom padding catches the descender tail. */
    padding-bottom: 0.18em;
}
.block__stat-label {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--fg-muted);
}
.block__stat-description {
    margin: 1rem auto 0;
    max-width: 52ch;
    color: var(--fg-muted);
    line-height: 1.55;
}

/* Contact section — Cal.com booking + form */

.contact {
    padding-block: 6rem;
    border-top: 1px solid var(--border);
}

.contact__header {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    max-width: 52rem;
    margin-bottom: 3rem;
}
.contact__lede {
    font-size: 1.125rem;
    color: var(--fg-muted);
    max-width: 56ch;
}

.contact__layout {
    display: grid;
    grid-template-columns: 1fr;
    gap: 3rem;
    align-items: stretch;
}

/* Booking column wrapper — single child below 960px (the card alone),
 * card + tall image above it. The image stretches to match the form's
 * height for a balanced 2-col composition. */
.contact__booking-col {
    display: flex;
    flex-direction: column;
    gap: 3rem;  /* match the column gap on .contact__layout */
}

.contact__booking-image {
    display: block;
    width: 100%;
    flex: 1;
    min-height: 16rem;
    object-fit: cover;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}
/* Theme-swap: light variant shows by default in both themes if no dark
 * variant is uploaded. When [data-theme="dark"] is set on <html>, the
 * dark variant takes over (hide light, show dark). */
.contact__booking-image--dark {
    display: none;
}
[data-theme="dark"] .contact__booking-image--light {
    display: none;
}
[data-theme="dark"] .contact__booking-image--dark {
    display: block;
}

.contact__col-title {
    font-family: "Fraunces", Georgia, serif;
    font-size: 1.5rem;
    font-weight: 500;
    letter-spacing: -0.015em;
    margin: 0 0 0.5rem;
}

.contact__booking,
.contact__form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 2rem;
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
}

.contact__booking-blurb,
.contact__form-blurb {
    color: var(--fg-muted);
    font-size: 0.9375rem;
    margin: 0;
}

.contact__booking-button {
    align-self: flex-start;
    margin-top: 0.5rem;
}

.contact__booking-foot,
.contact__form-foot {
    margin-top: auto;
    color: var(--fg-subtle);
    font-size: 0.875rem;
}
.contact__booking-foot a,
.contact__form-foot a {
    color: var(--fg-muted);
    text-decoration: underline;
    text-decoration-color: var(--border);
    text-underline-offset: 3px;
    transition: text-decoration-color var(--motion-fast) var(--ease-default);
}
.contact__booking-foot a:hover,
.contact__form-foot a:hover {
    text-decoration-color: var(--fg);
}

.contact__field {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    position: relative;
    /* Reserves a fixed slot below the input for the error message so
     * appearing/disappearing errors never push siblings around.
     * The error span is absolutely positioned into this padding zone. */
    padding-bottom: 1.25rem;
}
.contact__field-label {
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--fg-muted);
}
.contact__field input,
.contact__field textarea {
    appearance: none;
    -webkit-appearance: none;
    background: var(--bg);
    color: var(--fg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 0.7rem 0.9rem;
    font-family: inherit;
    font-size: 1rem;
    line-height: 1.4;
    transition:
        border-color var(--motion-fast) var(--ease-default),
        background var(--motion-fast) var(--ease-default);
    width: 100%;
}
.contact__field input:focus,
.contact__field textarea:focus {
    border-color: var(--accent);
}
.contact__field textarea {
    resize: vertical;
    min-height: 8rem;
}

/* Honeypot — visually hidden but kept in DOM. */
.contact__honeypot {
    position: absolute;
    left: -9999px;
    opacity: 0;
    pointer-events: none;
    height: 0;
    overflow: hidden;
}

/* Validation states ---------------------------------------------------- */

/* Error span is absolutely positioned inside the field's reserved
 * padding-bottom slot. It stays in the DOM, held at opacity:0 until
 * [data-shown="true"] flips it in, so it never pushes siblings down. */
.contact__field-error {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    color: oklch(54% 0.24 25);  /* clear, accessible red */
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.75rem;
    letter-spacing: 0.01em;
    line-height: 1.4;
    opacity: 0;
    transform: translateY(-2px);
    transition:
        opacity var(--motion-fast) var(--ease-default),
        transform var(--motion-fast) var(--ease-default);
    pointer-events: none;
}
.contact__field-error[data-shown="true"] {
    opacity: 1;
    transform: translateY(0);
}

.contact__field--error input,
.contact__field--error textarea {
    border-color: oklch(54% 0.24 25);
    background: color-mix(in oklch, oklch(54% 0.24 25) 4%, var(--bg));
}
.contact__field--error input:focus,
.contact__field--error textarea:focus {
    border-color: oklch(54% 0.24 25);
    box-shadow: 0 0 0 3px color-mix(in oklch, oklch(54% 0.24 25) 18%, transparent);
}

.contact__field--valid input,
.contact__field--valid textarea {
    border-color: color-mix(in oklch, oklch(58% 0.22 150) 85%, var(--border));
}

/* Turnstile slot. Usually stays empty: the widget only appears when Cloudflare wants a click. */

.contact__captcha:empty {
    display: none;
}

/* Form-level error + success states -------------------------------------- */

.contact__form-error {
    color: oklch(54% 0.24 25);
    background: color-mix(in oklch, oklch(54% 0.24 25) 8%, var(--bg));
    border: 1px solid color-mix(in oklch, oklch(54% 0.24 25) 50%, var(--border));
    border-radius: var(--radius-md);
    padding: 0.7rem 0.9rem;
    font-size: 0.875rem;
    line-height: 1.45;
}

.contact__form-success {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 1.25rem 1.5rem;
    background: color-mix(in oklch, oklch(58% 0.22 150) 8%, var(--bg-elevated));
    border: 1px solid color-mix(in oklch, oklch(58% 0.22 150) 85%, var(--border));
    border-radius: var(--radius-md);
    animation: contactSuccessIn var(--motion-base) var(--ease-default);
}
.contact__form-success p {
    margin: 0;
    font-family: "Fraunces", Georgia, serif;
    font-size: 1.125rem;
    font-weight: 500;
}
.contact__form-reset {
    background: transparent;
    border: none;
    color: var(--fg-muted);
    font-family: "Geist Mono", ui-monospace, monospace;
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 0;
    cursor: pointer;
    text-decoration: underline;
    text-decoration-color: var(--border);
    text-underline-offset: 4px;
    transition: text-decoration-color var(--motion-fast) var(--ease-default);
}
.contact__form-reset:hover {
    text-decoration-color: var(--fg);
}
@keyframes contactSuccessIn {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* Animated checkmark in the success card. Circle pops in with a gentle
 * overshoot, then the check stroke draws itself. Sequenced to start ~200ms
 * after the card entrance lands. */
.contact__form-success-header {
    display: flex;
    align-items: center;
    gap: 0.85rem;
}
.contact__form-success-header p {
    margin: 0;
}
.contact__form-success-icon {
    width: 2.25rem;
    height: 2.25rem;
    flex-shrink: 0;
    overflow: visible;
}
.contact__form-success-icon-circle {
    fill: oklch(58% 0.22 150);
    transform-box: fill-box;
    transform-origin: center;
    animation: contactCheckPop 320ms 200ms cubic-bezier(0.5, 1.6, 0.4, 1) both;
}
.contact__form-success-icon-check {
    stroke: oklch(98% 0.01 150);
    stroke-dasharray: 40;
    stroke-dashoffset: 40;
    animation: contactCheckDraw 260ms 460ms var(--ease-default) both;
}
@keyframes contactCheckPop {
    from { transform: scale(0); }
    to   { transform: scale(1); }
}
@keyframes contactCheckDraw {
    to { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
    .contact__form-success-icon-circle { animation: none; }
    .contact__form-success-icon-check {
        animation: none;
        stroke-dashoffset: 0;
    }
}

/* Submit button busy state ----------------------------------------------- */

.contact__submit {
    align-self: flex-start;
    margin-top: 0.5rem;
}
.contact__submit[aria-busy="true"] {
    opacity: 0.85;
    cursor: progress;
}
.contact__submit[disabled] {
    opacity: 0.6;
    cursor: not-allowed;
}

@media (min-width: 960px) {
    .contact__layout {
        grid-template-columns: 1fr 1.2fr;
    }
}

/* Mobile: reduce the card padding so the cards don't visually crowd the
 * narrow page gutter. 2rem internal padding on top of the page gutter was
 * eating too much horizontal space and making the cards look oversized. */
@media (max-width: 767.98px) {
    .contact__booking,
    .contact__form {
        padding: 1.25rem;
    }
    /* Validation errors wrap to 2 lines on narrow screens. Switch the
     * error to in-flow positioning on mobile — it expands naturally
     * to fit 1 or 2 lines and sits right below the input. The small
     * layout-shift when an error toggles is acceptable on a single-
     * column phone layout. */
    .contact__field {
        padding-bottom: 0;
    }
    .contact__field-error {
        position: relative;
    }
}

/* Error pages — 404, 500. Big numerical mark, copy, links. */

.error-page {
    min-height: clamp(640px, 88dvh, 1000px);
    display: flex;
    align-items: center;
    padding-block: 6rem;
}

.error-page__inner {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    max-width: 56rem;
}

.error-page__code {
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(8rem, 22vw, 18rem);
    font-weight: 600;
    line-height: 0.85;
    letter-spacing: -0.05em;
    margin-bottom: 0.5rem;
}

.error-page__title {
    font-family: "Fraunces", Georgia, serif;
    font-size: clamp(2rem, 5vw, 3.5rem);
    line-height: 1.05;
    font-weight: 500;
    letter-spacing: -0.02em;
    margin: 0;
    text-wrap: balance;
}

.error-page__lede {
    font-size: 1.125rem;
    color: var(--fg-muted);
    max-width: 56ch;
    line-height: 1.55;
    margin: 0;
}

.error-page__links {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    list-style: none;
    padding: 0;
    margin: 1.5rem 0 0;
}


/* Spectrum gradient utilities — used in 5 specific places per spec */

.text-spectrum {
  background-image: var(--spectrum);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.bg-spectrum {
  background-image: var(--spectrum);
}

.border-spectrum {
  border-image: var(--spectrum) 1;
}

.underline-spectrum {
  background-image: var(--spectrum);
  background-position: 0 100%;
  background-repeat: no-repeat;
  background-size: 100% 2px;
  padding-bottom: 0.18em;
  transition: background-size var(--motion-fast) var(--ease-default);
}

.underline-spectrum:hover,
.underline-spectrum:focus-visible {
  background-size: 100% 6px;
}

/* Animated mesh utilities — flowing gradient that runs across an element's
   background or fills text via background-clip: text. Uses the --mesh-grad
   token defined in tokens.css.

   Seamless infinite loop: background-repeat: repeat tiles the gradient,
   and the keyframe shifts background-position by exactly one tile width
   (-200%, matching background-size: 200%). Combined with the gradient's
   matching first+last color stops, the visual is perfectly periodic. */

.bg-mesh,
.text-mesh {
    background-image: var(--mesh-grad);
    background-size: 200% 100%;
    background-repeat: repeat;
    animation: mesh-flow 18s linear infinite;
    will-change: background-position;
}

.text-mesh {
    background-clip: text;
    -webkit-background-clip: text;
    color: transparent;
    /* Faint shadow keeps glyphs legible against dark and light bg. */
    filter: drop-shadow(0 1px 0 color-mix(in oklch, var(--bg) 60%, transparent));
}

/* Faster + brighter when an interactive element with .bg-mesh is hovered. */
.bg-mesh:hover,
.bg-mesh:focus-visible,
.bg-mesh--fast {
    animation-duration: 7s;
    filter: saturate(1.1) brightness(1.04);
}

@keyframes mesh-flow {
    from { background-position: 0% 50%; }
    to   { background-position: -200% 50%; }
}

@media (prefers-reduced-motion: reduce) {
    .bg-mesh,
    .text-mesh {
        animation: none !important;
        background-position: 50% 50% !important;
    }
}

