/* ============================================
   Design Tokens — From Figma
   ============================================ */
:root {
  /* Brand */
  --brand-orange: #F5941E;
  --brand-sunset: #E85814;
  --brand-rgb: 245, 148, 30;          /* R, G, B for use in rgba() */
  --brand-deep-rgb: 160, 90, 30;      /* warm pillow-shadow variant */
  --brand-light: #F5A23A;             /* button gradient — top */
  --brand-deep:  #D87012;             /* button gradient — bottom */
  --brand-hover-light: #FFB052;       /* button gradient — hover top */
  --brand-hover-deep:  #E27E18;       /* button gradient — hover bottom */

  /* Text */
  --text-primary: #1A1916;
  --text-secondary: #575757;
  --text-tertiary: #888580;
  --text-inverse: #FFFFFF;

  /* Surface */
  --surface-default: #FFFFFF;
  --surface-subtle: #F7F6F4;
  --surface-muted: #CDCDCD;
  --surface-screw: #E1E1E1;
  --surface-screw-detail: #888786;

  /* Decorative */
  --decorative-stripe: #E0E0E0;
  --decorative-ink: #000000;

  /* Border */
  --border-hairline: rgba(0, 0, 0, 0.07);

  /* Footer dots */
  --footer-dot-purple: #6E2D8C;
  --footer-dot-orange: #E85814;
  --footer-dot-teal: #0094A8;
  --footer-dot-blue: #263A94;
  --footer-dot-green: #5AAA32;

  /* Typography */
  /* Display face — historically DM Serif Display, now the same Plus
     Be Vietnam Pro as body text (titles differentiate via weight 800).
     Var name kept so existing rules don't all need renaming. */
  --font-serif: 'Be Vietnam Pro', system-ui, -apple-system, sans-serif;
  --font-sans: 'Be Vietnam Pro', system-ui, -apple-system, sans-serif;

  /* Layout — sections span full viewport; content centered to 1128px.
     Padding scales down on phones for more usable content width. */
  --container-max: 1440px;
  --content-width: 1128px;
  --container-pad: max(20px, calc((100vw - 1128px) / 2));
  /* For elements that need to stay aligned to the 1440px design-frame edge
     on wider viewports (hero shape, blob, etc) */
  --frame-edge-offset: max(0px, calc((100vw - 1440px) / 2));
}
@media (max-width: 600px) {
  :root {
    --container-pad: 16px;
  }
}

/* ============================================
   Page loader (FIRST-VISIT ONLY)
   Simple cinematic: the full logo (lightning-bolt + orange dot) sits
   centered on white for the loader's min-visible window, then the
   whole sheet fades out and the page emerges. No dot expand.
   Intra-session navigations skip this entirely — boot.js adds
   `is-revisit` to <html> so the loader never paints, and skeleton
   placeholders inside the page fill the gap instead.
   ============================================ */
.page-loader {
  position: fixed;
  inset: 0;
  background: #fff;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  transition:
    opacity 0.65s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s 0.65s;
}

/* Logo stack: SVG bolt + an HTML dot overlay positioned exactly where
   the SVG's brand-dot would be. Splitting them lets the dot's
   transform be uncontested (the bolt fades via opacity in its SVG,
   the dot scales via transform in its HTML element) — SVG inline
   transforms via transform-box/transform-origin are unreliable across
   browsers when an animation and a transition both touch transform. */
.page-loader-stack {
  position: relative;
  width: 35px;
  height: 56px;
}
/* SVG carries only the lightning bolt path — the orange dot is now
   the HTML overlay below, so we don't render two dots. */
.page-loader-logo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  transition: opacity 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
.loader-bolt { /* path inside the SVG; nothing to do, color hard-coded */ }

/* Orange brand dot — positioned to coincide pixel-perfectly with where
   the original SVG dot would have been rendered (cx=20.2265, cy=35.6443
   r=4.0074 inside a 25×40 viewBox → at 35×56 render = center (28.31,
   49.9), diameter 11.2).

   NB: no CSS `animation` here. Declaring an animation on the element
   (even one that doesn't touch `transform`) makes Chromium skip the
   transform transition entirely — the dot would snap straight to
   scale(400) without animating. A subtle opacity pulse on the SVG
   parent gives us a similar "still alive" feel instead. */
.loader-dot {
  position: absolute;
  width: 11.2px;
  height: 11.2px;
  border-radius: 50%;
  background: var(--brand-orange, #F5941E);
  left: 22.71px;   /* 28.31 − 11.2/2 */
  top: 44.30px;    /* 49.9 − 11.2/2 */
  transform-origin: center center;
  will-change: transform;
  /* Soft orange halo + a subtle box-shadow pulse to give the dot a
     "warm bulb" feel while the loader sits. The box-shadow animates
     (not transform), so it doesn't interfere with the expand transition
     that boot.js drives via inline styles. */
  animation: loaderHaloPulse 2s ease-in-out infinite;
}
@keyframes loaderHaloPulse {
  0%, 100% {
    box-shadow:
      0 0 8px rgba(var(--brand-rgb), 0.55),
      0 0 18px rgba(var(--brand-rgb), 0.25);
  }
  50% {
    box-shadow:
      0 0 14px rgba(var(--brand-rgb), 0.85),
      0 0 32px rgba(var(--brand-rgb), 0.40);
  }
}

/* Backwards compat: hide old loader marks if the page still has them. */
.page-loader-mark { display: none; }

.page-loader.is-hidden,
html.is-revisit .page-loader {
  opacity: 0;
  visibility: hidden;
}
/* Bolt + dot transform are driven inline by boot.js — only the loader
   sheet's overall fade remains a CSS-class transition. */

/* While loading, hold the page slightly enlarged + invisible. On reveal
   it eases back to scale 1 — crossfades with the loader sheet for a
   smooth "logo dissolves into homepage" handoff. */
.page {
  transform-origin: center top;
  /* Bouncy curve so the page overshoots past scale 1 and settles in —
     the loader's logo zooms out + fades just before this fires. */
  transition:
    opacity 0.55s cubic-bezier(0.16, 1, 0.3, 1),
    transform 0.85s cubic-bezier(0.34, 1.56, 0.64, 1);
}
html.is-loading .page {
  opacity: 0;
  transform: scale(0.94);
}

/* First-visit: pause entry animations until the loader fades */
html.is-loading,
html.is-loading body {
  overflow: hidden;
}
html.is-loading .hero-inner > *,
html.is-loading .reveal {
  animation-play-state: paused !important;
}

@media (prefers-reduced-motion: reduce) {
  .page-loader-mark { animation: none; transition: none; }
  .page-loader { transition: none; }
  .page { transition: none; }
  html.is-loading .page { transform: none; opacity: 1; }
}

/* ============================================
   Skeleton placeholders
   Shown inside dynamic content containers until content-loader
   replaces them with real content.
   ============================================ */
.skel {
  position: relative;
  background: linear-gradient(90deg,
    rgba(0,0,0,0.04) 0%,
    rgba(0,0,0,0.07) 50%,
    rgba(0,0,0,0.04) 100%);
  background-size: 200% 100%;
  border-radius: 12px;
  overflow: hidden;
  animation: skelShimmer 1.4s ease-in-out infinite;
}
@keyframes skelShimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* Project card skeleton — matches .project-card height */
.skel-card {
  height: 382px;
  border-radius: 20px;
}
/* Skill / client pill */
.skel-pill {
  display: inline-block;
  height: 36px;
  width: 110px;
  border-radius: 100px;
}
/* Experience row */
.skel-exp-row {
  height: 60px;
  border-radius: 8px;
}
/* Tool card */
.skel-tool {
  height: 150px;
  border-radius: 16px;
}
/* Fact row */
.skel-fact {
  height: 48px;
  border-radius: 6px;
}

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

/* ============================================
   Page transitions
   Smoother, flowy cross-document transitions.
   ============================================ */
:root {
  --ease-flow: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-soft: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-liquid: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-dwell:  cubic-bezier(0.5, 0, 0.1, 1);
  --ease-bounce: cubic-bezier(0.34, 2.2, 0.64, 1);
}

/* ============================================
   White fade — page-load reveal + navigation curtain

   An html::after overlay starts fully white on every page load and
   fades to transparent (pageFadeIn) — that's the "fade in" reveal.
   When JS adds .is-leaving to <html> on an internal link click, the
   animation switches to pageFadeOut and the overlay fades back to
   white before the browser follows the link.
   ============================================ */
html::after {
  content: '';
  position: fixed;
  inset: 0;
  background: #fff;
  z-index: 9998;
  pointer-events: none;
  animation: pageFadeIn 0.35s ease both;
}
html.is-leaving::after {
  animation: pageFadeOut 0.24s ease both;
}
@keyframes pageFadeIn {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes pageFadeOut {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  html::after,
  html.is-leaving::after { animation: none; opacity: 0; }
}

/* ============================================
   Reset & Base
   ============================================ */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; background: var(--surface-subtle); }
body {
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 27.2px;
  color: var(--text-secondary);
  background: var(--surface-subtle);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ============================================
   Global Animations
   ============================================ */
/* Compositor-only animation (transform + opacity) — never blur or filter,
   those force GPU re-rasterization every frame and cause scroll jank. */
@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translate3d(0, 28px, 0) scale(0.985);
  }
  to {
    opacity: 1;
    /* End at `none` (not an identity matrix) so the retained fill
       leaves no transform behind — a lingering transform creates a
       stacking context that blocks mix-blend-mode on descendants
       (e.g. the hero subtitle) from reaching the layers below. */
    transform: none;
  }
}
@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes shimmerStripes {
  0%   { background-position: 0 0; }
  100% { background-position: 28px 0; }
}

/* Reveal on scroll — uses CSS animation (one-shot) so the delay only
   applies to the entry, never leaking into hover transitions.
   Long 1s curve with deep ease-out for a flowy "rising into focus" feel. */
.reveal {
  opacity: 0;
  transform: translate3d(0, 28px, 0);
  /* will-change only on elements actively animating; cleared after */
}
.reveal.is-visible {
  animation: revealIn 0.7s var(--ease-bounce, cubic-bezier(0.34, 2.2, 0.64, 1)) both;
  will-change: transform, opacity;
}
/* Pure opacity + translate — no scale so the GPU only composites, doesn't
   resample. Longer 1.1s curve gives the "fade up and exhale" feel. */
@keyframes revealIn {
  from {
    opacity: 0;
    transform: translate3d(0, 28px, 0);
  }
  to {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }
}
.reveal.is-visible[data-delay="1"] { animation-delay: 0.08s; }
.reveal.is-visible[data-delay="2"] { animation-delay: 0.16s; }
.reveal.is-visible[data-delay="3"] { animation-delay: 0.24s; }
.reveal.is-visible[data-delay="4"] { animation-delay: 0.32s; }
.reveal.is-visible[data-delay="5"] { animation-delay: 0.40s; }
.reveal.is-visible[data-delay="6"] { animation-delay: 0.48s; }

/* Mobile reveal: same buttery curve, slightly shorter motion path so
   long lists don't feel like they're slowly raining in. Still pure
   opacity + translate so the GPU just composites. */
@media (max-width: 760px) {
  .reveal {
    transform: translate3d(0, 20px, 0);
  }
  .reveal.is-visible {
    animation: revealInMobile 0.65s var(--ease-bounce) both;
  }
  .reveal.is-visible[data-delay="1"] { animation-delay: 0.06s; }
  .reveal.is-visible[data-delay="2"] { animation-delay: 0.12s; }
  .reveal.is-visible[data-delay="3"] { animation-delay: 0.18s; }
  .reveal.is-visible[data-delay="4"] { animation-delay: 0.24s; }
  .reveal.is-visible[data-delay="5"] { animation-delay: 0.30s; }
  .reveal.is-visible[data-delay="6"] { animation-delay: 0.36s; }
}
@keyframes revealInMobile {
  from { opacity: 0; transform: translate3d(0, 20px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; filter: none; }
}
img {
  display: block;
  max-width: 100%;
  /* Casual save protection — disables drag-to-save (Chrome/FF/Edge),
     long-press save menu (iOS Safari) and text-select highlight on
     image alt-text. Determined users can still screenshot, but the
     usual one-click "Save Image As…" routes are closed. */
  -webkit-user-drag: none;
  user-drag: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; background: none; border: 0; cursor: pointer; color: inherit; }

/* ============================================
   Typography Utilities
   ============================================ */
.h-xl { font-family: var(--font-serif); font-weight: 800; font-size: 80px; line-height: 84px; letter-spacing: -2.4px; }
.h-l  { font-family: var(--font-serif); font-weight: 800; font-size: clamp(36px, 6vw, 64px); line-height: 1.05; letter-spacing: -1.5px; }
.h-m  { font-family: var(--font-serif); font-weight: 800; font-size: 32px; line-height: 36px; letter-spacing: -0.5px; }
.h-s  { font-family: var(--font-serif); font-weight: 800; font-size: 24px; line-height: 28px; }
.h-xs { font-family: var(--font-serif); font-weight: 800; font-size: 22px; line-height: 26px; }

.t-l  { font-weight: 700; font-size: 20px; line-height: 24px; }
.t-s  { font-weight: 700; font-size: 16px; line-height: 20px; }
.body-l       { font-size: 16px; line-height: 27.2px; }
.body-m       { font-size: 15px; line-height: 25px; }
.body-s       { font-size: 14px; line-height: 22px; }
.body-xs      { font-size: 13px; line-height: 20px; }
.body-caption { font-size: 12px; line-height: 18px; }
.body-mono    { font-size: 10px; line-height: 14px; letter-spacing: 0.6px; }
.subtitle-label { font-weight: 600; font-size: 11px; line-height: 14px; letter-spacing: 1.5px; }

.text-tertiary { color: var(--text-tertiary); }
.text-secondary { color: var(--text-secondary); }
.text-primary { color: var(--text-primary); }
.text-orange { color: var(--brand-orange); }
.italic-serif { font-family: var(--font-serif); font-style: italic; }

/* ============================================
   Layout
   ============================================ */
.page {
  width: 100%;
  margin: 0 auto;
  background: var(--surface-subtle);
}
.container {
  width: 100%;
  max-width: var(--container-max);
  padding-left: var(--container-pad);
  padding-right: var(--container-pad);
  margin: 0 auto;
}
.section-divider {
  border-bottom: 1px solid var(--border-hairline);
}

/* ============================================
   Nav
   ============================================ */
.nav {
  /* Frosted glass — high-opacity white so the backdrop-filter has
     barely any variable input. (At 0.78 alpha the blur was picking
     up both page snapshots during view transitions and visibly
     shifting in tone.) Still reads as translucent because of the
     saturate(150%) + slight residual transparency. */
  background: rgba(255, 255, 255, 0.92);
  -webkit-backdrop-filter: saturate(150%) blur(14px);
  backdrop-filter: saturate(150%) blur(14px);
  box-shadow:
    inset 0 -1px 0 rgba(255,255,255,0.6),
    0 2px 20px rgba(0,0,0,0.05);
  position: sticky;
  top: 0;
  z-index: 100;
  /* iOS safe area: with viewport-fit=cover the page extends under the
     status bar; this padding stretches the white bar up to the physical
     screen top so content doesn't peek out behind the clock. 0 on
     desktop and devices without insets. */
  padding-top: env(safe-area-inset-top, 0px);
  transition:
    opacity 0.35s var(--ease-soft, ease),
    background 0.35s var(--ease-soft, ease),
    box-shadow 0.35s var(--ease-soft, ease),
    backdrop-filter 0.35s var(--ease-soft, ease),
    -webkit-backdrop-filter 0.35s var(--ease-soft, ease);
}
/* ─────────────────────────────────────────────────────────────────
   Home nav choreography — v2, simpler.

   Layout: on the home page the logo is absolutely positioned inside
   nav-inner. That way its horizontal position is controlled by a
   single `left` value that transitions cleanly between two pixel-
   resolved endpoints — no viewport calc, no flex reflow.

   States:
     .on-home                    → logo at left (default nav), links visible
     .on-home.is-over-hero       → logo centered, links hidden,
                                    background transparent

   All transitions share the same 500ms curve so the swap reads as one
   coordinated motion, not four independent ones.
   ───────────────────────────────────────────────────────────────── */
.nav.on-home .nav-inner {
  position: relative;
  /* Match every other page's nav padding — the hero's own 90px content
     padding is decoupled from this. */
  padding: 14px var(--container-pad);
  justify-content: flex-end; /* links pin to the right; logo is absolute */
}

.nav.on-home .nav-logo {
  position: absolute;
  top: 50%;
  left: var(--container-pad);
  transform: translateY(-50%);
  transition: left 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.nav.on-home.is-over-hero .nav-logo {
  /* Center of logo (25px wide) on center of viewport. */
  left: calc(50% - 12.5px);
}

/* Chrome (bg + shadow + backdrop-filter) fades in/out with the same
   500ms curve — see the base .nav transition list which covers these. */
.nav.is-over-hero {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: none;
}

/* Links: hidden while over the hero, then cascade in on scroll. */
.nav.on-home .nav-links > * {
  transition:
    opacity   0.4s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.nav.on-home.is-over-hero .nav-links > * {
  opacity: 0;
  transform: translateY(-4px);
  pointer-events: none;
}
/* Enter cascade (scrolled state): sweep from right to left so the
   items land TOWARD the logo which just slid back to the left.
   3 items now (Home / Works / About) — theme toggle was removed. */
.nav.on-home:not(.is-over-hero) .nav-links > *:nth-child(1) { transition-delay: 260ms; }
.nav.on-home:not(.is-over-hero) .nav-links > *:nth-child(2) { transition-delay: 180ms; }
.nav.on-home:not(.is-over-hero) .nav-links > *:nth-child(3) { transition-delay: 100ms; }
/* Exit cascade (over-hero state): mirror — sweep from left to right
   so items peel AWAY from the logo which is about to slide to center. */
.nav.on-home.is-over-hero .nav-links > *:nth-child(1) { transition-delay: 0ms; }
.nav.on-home.is-over-hero .nav-links > *:nth-child(2) { transition-delay: 80ms; }
.nav.on-home.is-over-hero .nav-links > *:nth-child(3) { transition-delay: 160ms; }
.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 70px;
  padding: 14px var(--container-pad);
}
.nav-logo { width: 25px; height: 40px; display: block; color: #0E0E0E; }
.nav-logo img, .nav-logo svg { width: 100%; height: 100%; object-fit: contain; }
.nav-links { display: flex; align-items: center; gap: 36px; }
.nav-link {
  position: relative;
  font-size: 16px;
  line-height: 27.2px;
  padding: 10px 20px;
  color: var(--text-tertiary);
  transition: color 0.2s ease;
}
.nav-link::after {
  content: "";
  position: absolute;
  left: 20px;
  right: 20px;
  bottom: 4px;
  height: 2px;
  background: var(--brand-orange);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s cubic-bezier(.2,.7,.2,1);
}
.nav-link:hover { color: var(--text-primary); }
.nav-link:hover::after { transform: scaleX(1); }
.nav-link.is-active { color: var(--brand-orange); }
.nav-link.is-active::after { transform: scaleX(1); }

/* ============================================
   Stripes Background (decorative)
   Uses direct alpha — no mix-blend-mode or opacity which both force
   the browser to composite everything behind on every scroll frame.
   ============================================ */
.stripes-bg {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  /* No background-image anymore — boot.js fills this with individual
     <span class="stripe"> children so each stripe can fall in
     separately from above, staggered left-to-right. */
}
.stripes-bg .stripe {
  position: absolute;
  top: 0;
  width: 4px;
  height: 100%;
  background: rgba(0, 0, 0, 0.022);
}

/* ============================================
   Hero — Home
   ============================================ */
.hero {
  position: relative;
  overflow: hidden;
  background: #fff;
  border-bottom: 1px solid var(--border-hairline);
  /* View-transitions were removed, so `contain: paint` is safe now — it
     isolates the hero into its own compositor layer so the blob
     animations + backdrop-blur don't invalidate paint on the rest of
     the page. Big win for scroll perf below the fold. */
  contain: layout paint style;
}
.hero-blob {
  position: absolute;
  width: 882px;
  height: 882px;
  left: calc(var(--frame-edge-offset) - 241px);
  top: -449px;
  border-radius: 1000px;
  background: radial-gradient(circle at center, rgba(var(--brand-rgb), 0.12) 0%, rgba(var(--brand-rgb), 0) 70%);
  pointer-events: none;
}
.hero-inner {
  position: relative;
  /* No z-index — DOM order already paints the content above the
     decorative layers, and a z-index here would create a stacking
     context that isolates the subtitle's mix-blend-mode from the
     blobs/stripes behind it. */
  min-height: 600px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 90px var(--container-pad);
  text-align: center;
  gap: 36px;
}
.hero-inner > * {
  opacity: 0;
  animation: fadeUp 0.85s var(--ease-bounce, cubic-bezier(0.34, 2.2, 0.64, 1)) forwards;
}
.hero-inner > *:nth-child(1) { animation-delay: 0.10s; }
.hero-inner > *:nth-child(2) { animation-delay: 0.22s; }
.hero-inner > *:nth-child(3) { animation-delay: 0.36s; }
.hero-inner > *:nth-child(4) { animation-delay: 0.52s; }
/* Home subtitle row enters with a plain fade (no slide): a filled
   transform would stick around after the entrance and create a
   stacking context that blocks the subtitle's mix-blend-mode from
   blending with the lava/stripe layers behind it. */
.hero-home .hero-bottom { animation-name: fadeIn; }
@media (max-width: 760px) {
  .hero-inner > * { animation-duration: 0.5s; }
  .hero-inner > *:nth-child(1) { animation-delay: 0.05s; }
  .hero-inner > *:nth-child(2) { animation-delay: 0.12s; }
  .hero-inner > *:nth-child(3) { animation-delay: 0.20s; }
  .hero-inner > *:nth-child(4) { animation-delay: 0.28s; }
}
.hero-eyebrow {
  font-weight: 700;
  font-size: 20px;
  line-height: 24px;
  color: var(--text-tertiary);
}
.hero-title {
  font-family: var(--font-serif);
  font-weight: 800;
  /* Fluid: 36px on small phones, smoothly scales to 80px on desktop */
  font-size: clamp(36px, 7.2vw, 80px);
  line-height: 1.05;
  letter-spacing: -2.4px;
  color: var(--brand-orange);
}
.hero-title em {
  font-style: italic;
}
.hero-desc {
  font-size: 16px;
  line-height: 27.2px;
  color: var(--text-tertiary);
}
.hero-actions {
  display: flex;
  gap: 10px;
  align-items: center;
  justify-content: center;
}
/* Flat pill — vertical white-to-soft-grey gradient body, inset
   highlight ring, warm-tinted pillow drop-shadow, brand-orange
   underglow on hover. No wet-shine pseudo: matches the cleaner
   variant from the Figma source so the rim doesn't read as glow on
   dark backgrounds. */
.btn-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  border-radius: 50px;
  /* Frosted-glass pill: translucent white over a backdrop blur so the
     warm blobs glow through the button body. */
  background:
    linear-gradient(180deg, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0.30) 100%);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  backdrop-filter: blur(14px) saturate(1.2);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.9),
    inset 0 -1px 0 rgba(0,0,0,0.03),
    inset 0 0 0 1px rgba(255,255,255,0.5),
    0 6px 18px rgba(0,0,0,0.10),
    0 22px 36px -10px rgba(0,0,0,0.16);
  color: #2C2623;
  font-size: 16px;
  line-height: 27.2px;
  transition:
    transform   0.45s var(--ease-bounce),
    box-shadow  0.5s var(--ease-liquid),
    color       0.3s var(--ease-soft);
}
.btn-pill:hover {
  transform: translateY(-3px);
  /* Subtle warm-orange underglow on hover. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,1),
    inset 0 -1px 0 rgba(0,0,0,0.03),
    inset 0 0 0 1px rgba(255,255,255,0.7),
    0 10px 24px rgba(var(--brand-deep-rgb), 0.10),
    0 28px 50px -12px rgba(var(--brand-rgb), 0.20);
  color: var(--brand-orange);
}
.btn-pill:active { transform: translateY(-1px) scale(0.98); }

/* ============================================
   Hero — Home (Figma redesign)
   Bottom-left aligned content with two rotated orange radial
   ellipses behind the vertical stripes for a warm shutter effect.
   ============================================ */
/* Pull the hero up behind the sticky nav — that way when the nav is
   transparent-over-hero, the shutter/blobs show through it instead of
   the page background. The safe-area inset is part of the nav's height
   on iOS, so it joins the offset. */
.hero-home {
  margin-top: calc(-70px - env(safe-area-inset-top, 0px));
}
.hero-home .hero-inner {
  /* Decorative hero — text isn't meant to be selected/copied. */
  -webkit-user-select: none;
  user-select: none;
  align-items: flex-start;
  justify-content: flex-end;
  text-align: left;
  gap: 36px;
  /* Fill the first fold exactly — bottom edge lands at viewport bottom. */
  min-height: 100vh;
  /* 90px left/right per the redesign; 160px top clears the nav row
     (plus the iOS status-bar inset when present). */
  padding: calc(160px + env(safe-area-inset-top, 0px)) 90px 90px;
}
@media (max-width: 720px) {
  .hero-home .hero-inner {
    /* Tighten padding on mobile so the title/eyebrow/desc actually get
       room to breathe on a ~380px viewport. */
    padding: calc(120px + env(safe-area-inset-top, 0px)) 24px 60px;
    gap: 24px;
  }
  .hero-home .hero-bottom {
    /* Stack description over buttons — flex-row leaves neither room
       on a narrow screen. */
    flex-direction: column;
    align-items: flex-start;
    gap: 28px;
  }
  .hero-home .hero-desc {
    padding-right: 0;
  }
  /* Buttons sit centered under the description on phones. */
  .hero-home .hero-actions {
    align-self: center;
  }
}
.hero-home .hero-eyebrow {
  font-family: 'Be Vietnam Pro', system-ui, -apple-system, sans-serif;
  font-weight: 700;
  font-size: 20px;
  line-height: 27.2px;
  color: var(--text-tertiary);
  margin: 0;
  /* Optical alignment: the title's huge glyphs carry almost no side
     bearing, so at the same box edge the small text reads as sticking
     out left. 6px inset (2 + 4 after review) trues them up visually. */
  padding-left: 6px;
}
.hero-home .hero-bottom {
  /* Description and buttons share a row: text takes the left, buttons
     hug the right and align to the description's bottom baseline.
     The gap keeps buttons from crowding the last line of copy. */
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 80px;
  width: 100%;
}
/* Tablet range (721–900px): the two fixed-width pills leave the
   description too little room in the row, squishing it into a narrow
   column. Stack instead — full-width description with the buttons
   below it, left-aligned to match the hero content. (Phones ≤720px
   are handled separately above.) */
@media (min-width: 721px) and (max-width: 900px) {
  .hero-home .hero-bottom {
    flex-direction: column;
    align-items: flex-start;
    gap: 28px;
  }
  .hero-home .hero-desc {
    padding-right: 0;
  }
}
.hero-home .hero-actions {
  display: flex;
  gap: 10px;
  flex-shrink: 0;
}
.hero-home .hero-title {
  color: #2C2623;
  font-family: 'Be Vietnam Pro', system-ui, -apple-system, sans-serif;
  font-weight: 800;
  /* Push right up to the 90px padding edge — largest single-line size
     that fills the whole padded width at 1440vw. */
  font-size: clamp(52px, 9.35vw, 135px);
  line-height: 1;
  letter-spacing: -4px;
  white-space: nowrap;
  position: relative;
}
/* Cursor-following glow, clipped to the glyphs. A duplicate of the
   title text (data-text kept in sync by JS) painted with a radial
   gradient that tracks --glow-x/--glow-y, then background-clip:text
   so the light only lives inside the letterforms. */
.hero-home .hero-title::after {
  content: attr(data-text);
  position: absolute;
  /* ── EDIT ME: bevel depth ─────────────────────────────────────────
     The orange fill is nudged down-right by --bevel; the white layer
     underneath (::before) stays at the true position, so it shows
     only as a sliver INSIDE each glyph's top-left edge. Bigger =
     deeper bevel. 0px = no inner highlight. */
  --bevel: 1px;
  top: var(--bevel);
  left: var(--bevel);
  right: calc(-0.2em - var(--bevel));
  bottom: calc(-0.2em - var(--bevel));
  pointer-events: none;
  /* Orange base + the same 42px rod shading as the glass shutter, so
     the lit letters look like they're glowing THROUGH the reeded
     glass. The stripe layer is offset -6px so its grid lines up with
     the background stripes (title sits at 90px = 2x42 + 6). The glow
     circle itself is a mask so both layers fade out together — plateau
     to 72%, resolved by 89%, same falloff as before. */
  background:
    repeating-linear-gradient(
      90deg,
      rgba(255, 255, 255, 0.32) 0px,
      rgba(255, 255, 255, 0.08) 8px,
      rgba(0,   0,   0,   0.25) 36px,
      rgba(168, 168, 168, 0.20) 42px
    ),
    linear-gradient(
      180deg,
      #FFAE3F 10%,
      #FF8900 35%,
      #B95000 100%
    );
  background-position: -6px 0, 0 0;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-mask-image: radial-gradient(
    circle 240px at var(--glow-x, 50%) var(--glow-y, 50%),
    #000 0%, #000 72%, transparent 89%
  );
  mask-image: radial-gradient(
    circle 240px at var(--glow-x, 50%) var(--glow-y, 50%),
    #000 0%, #000 72%, transparent 89%
  );
  color: transparent;
  /* ── EDIT ME: edge depth ── a whisper of deep amber hugging the
     lower-right glyph edges. Keep it faint: this paints OUTSIDE the
     letterform (the fill above is opaque), so strong values read as a
     drop shadow. The real bottom-right INNER shading comes from the
     fill gradient's dark last stop above. */
  text-shadow: 1px 1px 0px rgba(193, 92, 8, 0.2);
  /* Normal (opaque) paint — the bevel layer below must stay hidden
     under the glyph interiors, and screen blending let its white copy
     bleed through and whitewash the letters. The mask edge is kept
     fairly tight so the semi-transparent falloff band over dark text
     stays a narrow, shading-like rim. */
  opacity: 0;
  transition: opacity 0.35s ease;
}
.hero-home .hero-title:hover::after { opacity: 1; }
/* Inner highlight layer: a WHITE copy of the title at the TRUE glyph
   position, painted beneath the orange fill above (which is nudged
   down-right by --bevel). The white therefore shows only as a thin
   band along the INSIDE of each glyph's top-left edge — a real inset
   rim light, nothing sticking out past the letterform.
   ── EDIT ME: the alpha here is the highlight strength. ── */
.hero-home .hero-title::before {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 0;
  right: -0.4em;
  bottom: -0.4em;
  pointer-events: none;
  color: rgba(255, 255, 255, 0.95);
  /* ── EDIT ME: rim softness ── feathers the white sliver's edges.
     0 = crisp cut, 2px+ = misty. */
  filter: blur(1.2px);
  /* Tighter mask than the orange layer above (60/74 vs 72/89): in the
     orange's falloff ring both layers are semi-transparent and the
     white bled through as a haze over dark glyphs. Ending the white
     earlier means it only exists where the orange is fully opaque —
     visible solely as the inset rim. */
  -webkit-mask-image: radial-gradient(
    circle 240px at var(--glow-x, 50%) var(--glow-y, 50%),
    #000 0%, #000 60%, transparent 74%
  );
  mask-image: radial-gradient(
    circle 240px at var(--glow-x, 50%) var(--glow-y, 50%),
    #000 0%, #000 60%, transparent 74%
  );
  opacity: 0;
  transition: opacity 0.35s ease;
}
.hero-home .hero-title:hover::before { opacity: 1; }
@media (hover: none) {
  .hero-home .hero-title::after,
  .hero-home .hero-title::before { display: none; }
}
@media (max-width: 720px) {
  .hero-home .hero-title {
    white-space: normal;
    /* Shrink so long words like "Illustrator" still fit within tight
       viewports, and loosen the -4px tracking that only works at
       display size. */
    font-size: clamp(40px, 12vw, 72px);
    letter-spacing: -2px;
  }
}
.hero-home .hero-desc {
  /* Let the copy stretch across most of the left column — was capped
     at 720px which left too much empty space next to it. */
  max-width: 1000px;
  flex: 1 1 auto;
  color: var(--text-tertiary);
  /* Difference against the lava blobs: the copy inverts against
     whatever drifts underneath, keeping it distinct from the
     background across the whole animation. */
  mix-blend-mode: difference;
  /* Small inset so the copy visually breathes inside its column rather
     than starting flush against the title's edge. */
  padding-right: 24px;
  /* Same 6px optical true-up as the eyebrow. */
  padding-left: 6px;
}
/* ---- Lava-lamp blob field ----------------------------------------
   Ambient warm-glow layer (unfiltered, static) provides the mood.
   .lava-field runs the SVG gooey filter over 7 solid orange blobs so
   overlapping blobs fuse into one mass, and separating blobs look like
   they split off — like paint drops in water. */
.hero-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.lava-ambient {
  position: absolute;
  inset: -20% -10%;
  pointer-events: none;
  background:
    radial-gradient(ellipse 55% 45% at 10% 95%,  rgba(var(--brand-rgb), 0.32) 0%, rgba(var(--brand-rgb), 0) 65%),
    radial-gradient(ellipse 55% 45% at 95% 8%,   rgba(var(--brand-rgb), 0.28) 0%, rgba(var(--brand-rgb), 0) 65%);
  filter: blur(30px);
}
.lava-field {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  filter: url(#lava-goo);
  /* Semi-transparent so blobs read as warm atmosphere, not a solid
     orange block covering the title. */
  opacity: 0.6;
  will-change: filter;
}
.lava-blob {
  position: absolute;
  will-change: transform;
}
/* When the hero is scrolled fully out of view, JS adds .blobs-paused to
   <html>: freeze the blob drift + morph so the goo filter stops
   re-evaluating every frame. Frees the GPU for the project-card reveal
   and cuts idle battery drain. */
html.blobs-paused .lava-blob,
html.blobs-paused .lava-blob::before {
  animation-play-state: paused;
}
/* The visible body lives on a pseudo so DRIFT (translate, on the
   outer element, slow) and MORPH (border-radius + squash-stretch
   scale, on the pseudo, fast) run as independent animations — calm
   travel, violent shape-churn. */
.lava-blob::before {
  content: '';
  position: absolute;
  inset: 0;
  will-change: transform, border-radius;
  animation-delay: var(--morph-delay, 0s);
}
/* Bigger blobs + bigger travel = much more visible drift through the
   shutter. Sizes ~2× previous, translates ~2× previous, speeds bumped
   so the movement is obvious within a couple of seconds. */
/* 5 orange blobs form the warm mass; 2 hot blobs (yellow/red) ride
   inside them so as they cross paths the merged region picks up a
   bright core and a deeper red edge — the fire-through-glass look. */
/* Responsive sizes via clamp(min, vw, max). Positions spread across
   the full hero — anchors pushed back toward the edges/corners so the
   field reads as blobs roaming the whole pane, not a clump in the
   middle. Their large drift keyframes carry them through the center
   on the way past each other. */
/* Each blob carries its own radial gradient — a brighter hot core
   offset from center, falling to a deeper rim — so single blobs read
   as lit-from-within and merged masses inherit multi-hue depth. All
   stops are fully opaque: the goo filter thresholds alpha, so any
   transparency at the rim would eat into the metaball silhouette. */
/* Outer element = slow drift only (translate + gentle uniform scale).
   Sizes are 2x so the field fills the pane; blob-2 anchors the top-left
   corner, which no blob used to reach. */
.lava-blob-1 { width: clamp(340px, 40vw, 720px); height: clamp(340px, 40vw, 720px); left: -12%; bottom: -18%; animation: lava1 26s ease-in-out infinite alternate; }
.lava-blob-2 { width: clamp(240px, 28vw, 520px); height: clamp(240px, 28vw, 520px); left: -12%; top:   -16%;  animation: lava2 21s ease-in-out infinite alternate; }
.lava-blob-3 { width: clamp(300px, 36vw, 640px); height: clamp(300px, 36vw, 640px); left: 24%;  bottom: -24%; animation: lava3 31s ease-in-out infinite alternate; }
.lava-blob-4 { width: clamp(220px, 26vw, 480px); height: clamp(220px, 26vw, 480px); left: 58%;  bottom: -20%; animation: lava4 24s ease-in-out infinite alternate; }
.lava-blob-5 { width: clamp(360px, 42vw, 760px); height: clamp(360px, 42vw, 760px); right: -14%; top:  -20%;  animation: lava5 29s ease-in-out infinite alternate; }
.lava-blob-6 { width: clamp(240px, 28vw, 520px); height: clamp(240px, 28vw, 520px); right: 8%;  top:    28%;  animation: lava6 19s ease-in-out infinite alternate; }
.lava-blob-7 { width: clamp(300px, 34vw, 620px); height: clamp(300px, 34vw, 620px); right: 30%; top:   -26%;  animation: lava7 27s ease-in-out infinite alternate; }

/* Pseudo = gradient body + fast intense morph. */
.lava-blob-1::before { background: radial-gradient(circle at 35% 30%, #FFB84D 0%, #F5941E 52%, #E07A10 100%); border-radius: 62% 38% 55% 45% / 58% 46% 54% 42%; animation: morphA 18s ease-in-out infinite alternate; }
.lava-blob-2::before { background: radial-gradient(circle at 42% 34%, #FFE08A 0%, #FFC24A 55%, #F5A11E 100%); border-radius: 40% 60% 30% 70% / 55% 40% 60% 45%; animation: morphB 15s ease-in-out infinite alternate; }
.lava-blob-3::before { background: radial-gradient(circle at 62% 26%, #FFAE3F 0%, #F5941E 48%, #DE7311 100%); border-radius: 58% 42% 66% 34% / 42% 62% 38% 58%; animation: morphC 21s ease-in-out infinite alternate; }
.lava-blob-4::before { background: radial-gradient(circle at 38% 40%, #F5822E 0%, #E15A15 55%, #C74A0E 100%); border-radius: 34% 66% 44% 56% / 60% 34% 66% 40%; animation: morphA 16s ease-in-out infinite alternate; }
.lava-blob-5::before { background: radial-gradient(circle at 58% 62%, #FFB84D 0%, #F5941E 50%, #E07A10 100%); border-radius: 46% 54% 40% 60% / 52% 40% 60% 48%; animation: morphB 20s ease-in-out infinite alternate; }
.lava-blob-6::before { background: radial-gradient(circle at 36% 58%, #FFEBA0 0%, #FFD466 52%, #FFB83B 100%); border-radius: 68% 32% 60% 40% / 40% 66% 34% 60%; animation: morphC 14s ease-in-out infinite alternate; }
.lava-blob-7::before { background: radial-gradient(circle at 64% 44%, #FFA93A 0%, #F5941E 50%, #E27E12 100%); border-radius: 30% 70% 46% 54% / 64% 42% 58% 36%; animation: morphA 22s ease-in-out infinite alternate; }

/* Calm drift paths (pre-intense-mode speeds and travel). */
@keyframes lava1 {
  0%   { transform: translate(0,      0)     scale(1); }
  30%  { transform: translate(340px, -220px) scale(1.3); }
  60%  { transform: translate(120px, -420px) scale(0.85); }
  100% { transform: translate(440px, -120px) scale(1.2); }
}
@keyframes lava2 {
  /* Tighter orbit than the others — this blob owns the top-left
     corner, so it never strays far from its anchor. */
  0%   { transform: translate(0,      0)     scale(1); }
  35%  { transform: translate(180px,  120px) scale(0.8); }
  70%  { transform: translate(300px, -60px)  scale(1.3); }
  100% { transform: translate(100px, -160px) scale(1); }
}
@keyframes lava3 {
  0%   { transform: translate(0,      0)     scale(1); }
  25%  { transform: translate(-180px,-280px) scale(1.25); }
  55%  { transform: translate(240px, -440px) scale(0.8); }
  100% { transform: translate(-80px,  80px)  scale(1.15); }
}
@keyframes lava4 {
  0%   { transform: translate(0,      0)     scale(1); }
  30%  { transform: translate(-360px, 180px) scale(1.4); }
  65%  { transform: translate(280px,  360px) scale(0.7); }
  100% { transform: translate(-120px,-80px)  scale(1.2); }
}
@keyframes lava5 {
  0%   { transform: translate(0,      0)     scale(1); }
  30%  { transform: translate(-280px, 300px) scale(0.75); }
  60%  { transform: translate(160px,  480px) scale(1.25); }
  100% { transform: translate(-400px, 160px) scale(1.1); }
}
@keyframes lava6 {
  0%   { transform: translate(0,      0)     scale(1); }
  25%  { transform: translate(360px, -160px) scale(1.3); }
  55%  { transform: translate(-240px, 280px) scale(0.65); }
  100% { transform: translate(120px,  400px) scale(1.2); }
}
@keyframes lava7 {
  0%   { transform: translate(0,      0)     scale(1); }
  35%  { transform: translate(-440px,-120px) scale(1.35); }
  70%  { transform: translate(120px, -360px) scale(0.8); }
  100% { transform: translate(-280px, 200px) scale(1.15); }
}

/* Intense morphs — border-radius flung between 22–78% extremes with
   non-uniform squash-and-stretch, on 4.5–7.5s cycles. Three variants
   shared across the 7 blobs; the per-blob duration + randomized
   --morph-delay keep them all out of phase. */
@keyframes morphA {
  0%   { transform: scale(1);          border-radius: 62% 38% 55% 45% / 58% 46% 54% 42%; }
  25%  { transform: scale(1.35, 0.75); border-radius: 26% 74% 24% 76% / 32% 74% 26% 68%; }
  50%  { transform: scale(0.75, 1.25); border-radius: 76% 24% 68% 32% / 52% 72% 28% 48%; }
  75%  { transform: scale(1.2, 1.3);   border-radius: 40% 60% 30% 70% / 66% 30% 70% 34%; }
  100% { transform: scale(0.9, 1.1);   border-radius: 58% 42% 74% 26% / 28% 62% 38% 72%; }
}
@keyframes morphB {
  0%   { transform: scale(1);          border-radius: 40% 60% 30% 70% / 55% 40% 60% 45%; }
  25%  { transform: scale(0.7, 1.3);   border-radius: 74% 26% 66% 34% / 24% 76% 24% 76%; }
  50%  { transform: scale(1.4, 0.85);  border-radius: 24% 76% 46% 54% / 66% 24% 76% 34%; }
  75%  { transform: scale(0.85, 1.2);  border-radius: 62% 38% 26% 74% / 44% 68% 32% 56%; }
  100% { transform: scale(1.25, 0.8);  border-radius: 36% 64% 70% 30% / 58% 30% 70% 42%; }
}
@keyframes morphC {
  0%   { transform: scale(1);          border-radius: 58% 42% 66% 34% / 42% 62% 38% 58%; }
  25%  { transform: scale(1.45, 0.8);  border-radius: 26% 74% 34% 66% / 70% 26% 74% 30%; }
  50%  { transform: scale(0.65, 1.3);  border-radius: 76% 24% 62% 38% / 26% 74% 26% 74%; }
  75%  { transform: scale(1.3, 1.05);  border-radius: 34% 66% 76% 24% / 62% 36% 64% 38%; }
  100% { transform: scale(0.95, 1.35); border-radius: 68% 32% 28% 72% / 46% 70% 30% 54%; }
}
@media (prefers-reduced-motion: reduce) {
  .lava-blob, .lava-blob::before { animation: none; }
}
/* Frosted-glass shutter — matches Figma nodes 568:4743 / 568:4760.
   Backdrop-blur is applied once at the container level (not per stripe)
   so we get the frosted look at ~1 filter cost instead of ~100. Each
   stripe then contributes just the horizontal light→dark→grey gradient
   and the hairline left border that defines the seam. */
.hero-home .stripes-bg {
  /* Frosted-glass base: plain blur, works everywhere (incl. iOS).
     The url() displacement variant is NOT declared here — WebKit
     PARSES `backdrop-filter: url(...)` as valid (so it would win the
     cascade over these) but can't render SVG-referenced backdrop
     filters, leaving no glass at all on iPhone. Chromium gets the
     displacement via the .has-svg-backdrop class that JS sets after
     engine detection (see index.html). */
  -webkit-backdrop-filter: blur(40px) saturate(1.1);
  backdrop-filter: blur(40px) saturate(1.1);
}
/* Chromium only (class set by JS): true reeded refraction — the SVG
   displacement filter warps the backdrop per 42px rod. Additionally
   gated on .at-hero-top (hero pinned to viewport top): once the hero
   scrolls, the filter would sample backdrop pixels above the viewport,
   which Chromium paints as a black band along the cut edge — so
   mid-scroll we drop back to the plain blur above. */
/* Desktop widths only: on narrow viewports the displacement's edge
   artifacts (rainbow fringe along the viewport top) outweigh the
   effect — phones get the same clean plain-blur frosting as iOS. */
@media (min-width: 761px) {
  html.has-svg-backdrop.at-hero-top .hero-home .stripes-bg {
    /* blur() chained AFTER the url() displacement does the frosting
       with proper edge clamping — inside the SVG filter it painted a
       rainbow band along the viewport edges (see filter's comment). */
    backdrop-filter: url(#reeded-glass) blur(4px) saturate(1.1);
  }
}
.hero-home .stripes-bg .stripe {
  width: 42px;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.12) 0%,
    rgba(0,   0,   0,   0.12) 86.009%,
    rgba(168, 168, 168, 0.12) 126.88%
  );
  border-left: 0.5px solid rgba(255, 255, 255, 0.35);
}
.hero-home .stripes-bg .stripe:not(:nth-child(3n+1)) { display: none; }

/* ============================================
   Hero — Works (smaller)
   ============================================ */
.hero-works {
  min-height: 350px;
  padding: 60px var(--container-pad);
  gap: 24px;
  /* Left-aligned against the container padding (base .hero-inner
     centers everything). */
  align-items: flex-start;
  text-align: left;
}
.hero-works .hero-title { /* same */ }

/* ============================================
   Hero — About
   ============================================ */
.hero-about {
  display: flex;
  gap: 80px;
  align-items: center;
  justify-content: center;
  padding: 60px var(--container-pad);
  position: relative;
  overflow: hidden;
  border-bottom: 1px solid var(--border-hairline);
  background: #fff;
}
.hero-about-content {
  flex: 1 0 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: 477px;
  position: relative;
  z-index: 2;
}
/* Entrance cascade — same fadeUp the home/works heroes use on their
   .hero-inner children: card sweeps in first, then the text blocks. */
.hero-about .profile-card,
.hero-about-content > * {
  opacity: 0;
  animation: fadeUp 0.85s var(--ease-bounce, cubic-bezier(0.34, 2.2, 0.64, 1)) forwards;
}
.hero-about .profile-card              { animation-delay: 0.10s; }
.hero-about-content > *:nth-child(1)   { animation-delay: 0.26s; }
.hero-about-content > *:nth-child(2)   { animation-delay: 0.42s; }
@media (prefers-reduced-motion: reduce) {
  .hero-about .profile-card,
  .hero-about-content > * { animation: none; opacity: 1; }
}
.hero-about .h-l { color: var(--brand-orange); }
.hero-about-eyebrow {
  font-weight: 700;
  font-size: 20px;
  line-height: 24px;
  color: var(--text-tertiary);
  margin-bottom: 14px;
}

/* ============================================
   Profile Card (About hero)
   ============================================ */
.profile-card {
  width: 360px;
  border-radius: 26px;          /* gumdrop curvature */
  overflow: hidden;
  background:
    linear-gradient(180deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0.92) 100%);
  /* Neutral resting shadow — same vocabulary as project-card. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,1),
    inset 0 -1px 0 rgba(0,0,0,0.06),
    inset 0 0 0 1px rgba(255,255,255,0.55),
    0 1px 0 rgba(255,255,255,0.95),
    0 6px 16px rgba(0,0,0,0.10),
    0 22px 38px -10px rgba(0,0,0,0.14),
    0 46px 78px -22px rgba(0,0,0,0.18);
  flex-shrink: 0;
  position: relative;
  z-index: 2;
}
/* Wet sheen — strong upper catchlight (linear) + top-left hot spot
   (radial) to suggest internal light through the translucent shell. */
.profile-card::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 45%;
  border-radius: 26px 26px 0 0;
  background:
    linear-gradient(180deg,
      rgba(255,255,255,0.36) 0%,
      rgba(255,255,255,0.10) 55%,
      rgba(255,255,255,0)   100%);
  pointer-events: none;
  z-index: 5;
}
.profile-card::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 60%; height: 50%;
  background:
    radial-gradient(120% 110% at 12% 0%,
      rgba(255,255,255,0.5) 0%,
      rgba(255,255,255,0)  55%);
  pointer-events: none;
  z-index: 6;
  border-radius: 26px 0 0 0;
}
.profile-image {
  position: relative;
  height: 382px;
  overflow: hidden;
}
.profile-image img,
.profile-image .photo-layer {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Two stacked layers cross-fade between photos. The active layer is the
   one currently visible (opacity 1). The other waits with opacity 0. */
.photo-layer {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
}
.photo-layer.is-active { opacity: 1; }
.profile-overlay {
  position: absolute;
  inset: auto 0 0 0;
  height: 92px;
  background: linear-gradient(to top, rgba(0,0,0,0.8), rgba(0,0,0,0));
}
/* Photo caption text swap animation (lives inside .profile-info .name) */
.profile-info .name {
  transition: opacity 0.22s ease, transform 0.35s cubic-bezier(.2,.7,.2,1);
}
.profile-card.is-switching .profile-info .name { opacity: 0; }
.profile-card.is-switching.to-next .profile-info .name { transform: translateX(-8px); }
.profile-card.is-switching.to-prev .profile-info .name { transform: translateX(8px); }
.profile-info {
  position: absolute;
  left: 20px;
  bottom: 20px;
  color: #fff;
  z-index: 2;
}
.profile-info .name {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: 32px;
  line-height: 36px;
  letter-spacing: -0.5px;
}
.profile-info .role {
  font-weight: 600;
  font-size: 11px;
  line-height: 14px;
  letter-spacing: 1.5px;
  color: rgba(255,255,255,0.82);
  margin-top: 6px;
  opacity: 0;
  max-height: 0;
  overflow: hidden;
  transition: opacity 0.22s ease, max-height 0.3s ease, margin-top 0.22s ease;
}
.profile-card.show-role .profile-info .role {
  opacity: 1;
  max-height: 20px;
}
.profile-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 16px;
  /* Vertical "speaker grille" stripes — a quiet iMac G3 nod behind the
     prev/next buttons and reaction bar. 1px line every 5px so it reads
     as texture, not noise. Painted underneath the panel base color so
     the stripes sit a couple % below white. */
  background:
    repeating-linear-gradient(90deg,
      rgba(0,0,0,0.05) 0,
      rgba(0,0,0,0.05) 1px,
      transparent 1px,
      transparent 5px),
    #fff;
  border-top: 1px solid var(--border-hairline);
  position: relative;
  box-shadow: 0 -4px 5px rgba(0,0,0,0.08);
}
/* ============================================
   NavButton — Figma-accurate states
   Default / Hover (scale 1.042, darker orange, icon +22%) / Pressed (1px down, inverted inset)
   Color: Figma uses rgba(var(--brand-rgb), 0.7) over a dark canvas; over our white
   panel that renders too pale, so we use full-opacity brand orange.
   ============================================ */
.nav-btn {
  position: relative;
  width: 72px;
  height: 55px;
  border-radius: 100px;
  /* Bright candy orange — reads as solid Aqua jellybean against the
     light card surface. */
  background: linear-gradient(180deg,
    var(--brand-light) 0%,
    var(--brand-deep) 100%);
  /* Stronger highlight + shadow gives a more pronounced "cushion" feel,
     matching the Figma render rather than the spec's 1px subtle inset. */
  box-shadow:
    2px 3px 6px rgba(0, 0, 0, 0.28),
    inset 1.5px 1.5px 3px rgba(255, 255, 255, 0.55),
    inset -1.5px -1.5px 3px rgba(0, 0, 0, 0.30);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition:
    background 0.22s ease,
    transform 0.22s cubic-bezier(.2,.7,.2,1),
    box-shadow 0.22s ease;
}
.nav-btn:hover {
  /* Hover warms up slightly — deeper bottom, brighter top */
  background: linear-gradient(180deg,
    var(--brand-hover-light) 0%,
    var(--brand-hover-deep) 100%);
  transform: scale(1.042);
  box-shadow:
    2.1px 3.1px 6.2px rgba(0, 0, 0, 0.30),
    inset 1.56px 1.56px 3.12px rgba(255, 255, 255, 0.6),
    inset -1.56px -1.56px 3.12px rgba(0, 0, 0, 0.32);
}
.nav-btn:active {
  /* Pressed: button moves 1px down, inset reversed → looks pushed in */
  transform: translate3d(0, 1px, 0);
  box-shadow:
    2px 3px 6px rgba(0, 0, 0, 0.28),
    inset -1.5px -1.5px 3px rgba(255, 255, 255, 0.7),
    inset 1.5px 1.5px 3px rgba(0, 0, 0, 0.32);
}
/* On hover+press, keep the bigger size but with the pressed inset */
.nav-btn:hover:active {
  transform: scale(1.042) translate3d(0, 1px, 0);
}
.reaction-btn {
  transition: transform 0.25s cubic-bezier(.2,.7,.2,1);
}
.reaction-btn:hover { transform: translateY(-3px) scale(1.1); }
.reaction-btn:active { transform: scale(0.95); }
/* Figma-shipped rounded triangle. Left button rotates 180° (same icon).
   Icon scales up 1.22× on hover, matching Figma's hover variant.
   mix-blend-mode: multiply blends the icon darker into the orange button,
   matching the deep red-orange triangle in Figma. (The SVG's internal
   `style="mix-blend-mode"` doesn't apply when loaded as <img>, so we
   apply it via CSS on the element itself.) */
.nav-btn-icon {
  width: 16px;
  height: 18px;
  display: block;
  /* Arrow SVG is now solid white (no saturation, no orange tint), so
     mix-blend-mode is unnecessary — and removing it keeps the white
     pure on top of whichever brand colour the button is wearing. */
  transition: transform 0.22s cubic-bezier(.2,.7,.2,1);
}
/* No CSS flip — arrow-left.svg is pre-mirrored in the file itself
   (via a `transform="scale(-1 1)"` group), which sidesteps the mobile
   Safari bug where a CSS transform on a mix-blend-mode <img> dropped
   the blend pass. */
.nav-btn:hover .nav-btn-icon { transform: scale(1.22); }
.reaction-bar {
  display: flex;
  gap: 10px;
  align-items: center;
  justify-content: center;
  padding: 0 20px;
  border-radius: 30px;
  /* Recessed groove — looks pressed INTO the surface (Aqua skeuo).
     Inverted gradient (darker top, lighter bottom) + inset shadows
     on the top/left sell the "cast shadow from above" illusion. */
  background:
    linear-gradient(180deg, rgba(200,200,200,0.92) 0%, rgba(225,225,225,0.85) 100%);
  height: 55px;
  width: 160px;
  position: relative;
  box-shadow:
    inset 0 1px 3px rgba(0,0,0,0.09),       /* top inner shadow — subtle depth */
    inset 0 1px 0 rgba(0,0,0,0.06),         /* top edge dark hairline */
    inset 0 -1px 0 rgba(255,255,255,0.5);   /* bottom edge light hairline */
}
.reaction-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  width: 32px;
  color: #343330;
  cursor: pointer;
}
.reaction-btn[disabled] { cursor: default; }
.reaction-btn .ic-wrap {
  position: relative;
  width: 24px;
  height: 24px;
  display: block;
}
.reaction-btn .ic {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transition: opacity 0.25s ease, transform 0.3s cubic-bezier(.2,.7,.2,1);
  transform-origin: center;
}
.reaction-btn .ic-filled { opacity: 0; transform: scale(0.6); }
.reaction-btn.is-reacted .ic-outline { opacity: 0; transform: scale(0.7); }
.reaction-btn.is-reacted .ic-filled  { opacity: 1; transform: scale(1); }

.reaction-btn[data-r="heart"].is-reacted { color: #E85814; }
.reaction-btn[data-r="smile"].is-reacted { color: #5AAA32; }
.reaction-btn[data-r="angry"].is-reacted { color: #D93F3F; }

.reaction-btn .count {
  font-weight: 700;
  font-size: 11px;
  line-height: 14px;
  letter-spacing: 0.6px;
  color: #1A1916;
  transition: color 0.25s ease;
}
.reaction-btn.is-reacted .count { color: currentColor; }

/* Pop bounce on react */
@keyframes reactionPop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.3) translateY(-4px); }
  100% { transform: scale(1) translateY(0); }
}
.reaction-btn.pop .ic-wrap { animation: reactionPop 0.36s cubic-bezier(.2,.7,.2,1); }

/* ============================================
   Photo cycle transition — layered cross-fade
   Two stacked photo layers cross-fade between photos. All state is set via
   inline styles in JS for lock-free spam-proof navigation. Transitions
   below interpolate between whatever current value and the new target,
   so rapid clicks gracefully override in-flight animations.
   ============================================ */
.photo-layer {
  transition:
    opacity 0.4s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}
.profile-info .name,
.profile-info .role {
  transition:
    opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1),
    max-height 0.35s ease;
}

/* ============================================
   Section Header
   ============================================ */
.section {
  padding: 60px var(--container-pad);
  position: relative;
  background: var(--surface-subtle);
  border-bottom: 1px solid var(--border-hairline);
  contain: layout style;
  /* Skip rendering when far off-screen — huge scroll perf win */
  content-visibility: auto;
  contain-intrinsic-size: 1px 600px;
}
.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 24px;
}
.section-header .left {
  font-weight: 700;
  font-size: 20px;
  line-height: 24px;
  color: var(--text-tertiary);
}
.section-header .right {
  font-size: 14px;
  color: var(--text-tertiary);
  transition: color 0.15s;
}
.section-header .right:hover { color: var(--brand-orange); }

/* ============================================
   Projects Grid
   ============================================ */
.projects-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  /* Section is always at least 800px tall (≈ a two-row card layout) so
     the contact CTA below stays put across filters. Cards sit at the
     top of their cells via align-items: start — they don't stretch
     vertically to fill the row's height. Fewer cards = empty space
     below; the cards keep their natural sizes. */
  align-items: start;
  min-height: 800px;
}
@media (max-width: 760px) {
  .projects-grid { min-height: 0; }
}
.projects-grid[hidden] { display: none; }

/* Empty-state — only the empty placeholder has a default height so the
   section doesn't collapse to nothing when a filter has 0 matches.
   Approximately matches a 2-row card view. */
.projects-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  min-height: 800px;
  text-align: center;
  padding: 40px 20px;
  font-family: var(--font-serif);
  font-size: 22px;
  line-height: 1.4;
  color: var(--text-tertiary);
  letter-spacing: 0.02em;
  animation: emptyIn 0.45s var(--ease-bounce) both;
}
.projects-empty-face {
  width: 88px;
  height: 88px;
  color: var(--brand-orange);
  /* The SVG strokes are `currentColor`, so setting color above tints
     the icon to brand orange — same accent as the <em>yet</em> below. */
}
.projects-empty-text { margin: 0; }
@media (max-width: 760px) {
  .projects-empty { min-height: 320px; }
  .projects-empty-face { width: 72px; height: 72px; }
}
.projects-empty em {
  color: var(--brand-orange);
  font-style: italic;
}
.projects-empty[hidden] { display: none; }

/* ============================================
   404 page
   ============================================ */
.hero-404 { min-height: 80vh; }
.notfound-inner {
  text-align: center;
  align-items: center;
}
.notfound-face {
  width: 120px;
  height: 120px;
  color: var(--brand-orange);
  margin-bottom: 8px;
  animation: emptyIn 0.55s var(--ease-bounce) both;
}
.hero-404 .hero-actions { justify-content: center; }
@media (max-width: 760px) {
  .notfound-face { width: 96px; height: 96px; }
}
@keyframes emptyIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0);  }
}

/* ============================================
   Project Cell — with Figma hover prototype
   Hover=OFF → Hover=ON:
     · stripes overlay fades to 0
     · image scales up + becomes brighter
     · title color: #575757 → #000
     · card lifts with bigger shadow
   ============================================ */
/* Filter cross-fade — drive opacity via CSS animations (not transitions).
   The `.reveal` scroll-in animation locks opacity to 1 via
   `animation-fill-mode: both`, and inside that lock plain transitions
   (even on `!important` properties) don't fade reliably. A new
   `animation` declaration REPLACES any prior animation, so we use
   keyframes to do the fade-out and fade-in. */
.project-card.is-fading {
  /* Exit stays smooth (no bounce) so things feel "dismissed" cleanly. */
  animation: cardFadeOut 0.42s var(--ease-liquid) forwards !important;
}
.project-card.is-entering {
  /* Enter has bouncy ease-out-back so cards "pop in" past their
     resting position then settle. */
  animation: cardFadeIn 0.55s var(--ease-bounce) forwards !important;
}
.project-card.is-hidden { display: none !important; }
/* "Fade down" — leaving cards drift downward as they fade out;
   entering cards drift down from above into place. */
@keyframes cardFadeOut {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(14px); }
}
@keyframes cardFadeIn {
  from { opacity: 0; transform: translateY(-14px); }
  to   { opacity: 1; transform: translateY(0); }
}

.project-card {
  position: relative;
  display: flex;
  flex-direction: column;
  border-radius: 26px;          /* gumdrop curvature */
  overflow: hidden;
  background: #fff;
  /* Aqua translucent plastic — neutral resting shadow; hover warms
     the shadow into orange. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,1),
    inset 0 -1px 0 rgba(0,0,0,0.06),
    inset 0 0 0 1px rgba(255,255,255,0.5),
    0 1px 0 rgba(255,255,255,0.95),
    0 4px 12px rgba(0,0,0,0.07),
    0 16px 28px -10px rgba(0,0,0,0.12),
    0 38px 60px -22px rgba(0,0,0,0.16);
  cursor: pointer;
  transition:
    transform  0.55s var(--ease-bounce),
    box-shadow 0.55s var(--ease-liquid),
    opacity    0.32s ease-out;
  contain: layout style;
}
/* Wet sheen — now two-layer for a stronger Aqua catchlight:
   ::before is a strong, narrow upper highlight (the "wet edge")
   ::after is a soft radial hot spot top-left (internal light through
   the translucent shell). */
.project-card::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 42%;
  border-radius: 26px 26px 0 0;
  background:
    linear-gradient(180deg,
      rgba(255,255,255,0.38) 0%,
      rgba(255,255,255,0.10) 50%,
      rgba(255,255,255,0)   100%);
  pointer-events: none;
  z-index: 5;
}
.project-card::after {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 60%; height: 50%;
  background:
    radial-gradient(120% 110% at 12% 0%,
      rgba(255,255,255,0.45) 0%,
      rgba(255,255,255,0)   55%);
  pointer-events: none;
  z-index: 6;
  border-radius: 26px 0 0 0;
}
.project-card:hover {
  transform: translate3d(0, -8px, 0);
  /* Subtle warm-orange underglow only on hover. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,1),
    inset 0 -1px 0 rgba(0,0,0,0.06),
    inset 0 0 0 1px rgba(255,255,255,0.6),
    0 1px 0 rgba(255,255,255,1),
    0 8px 18px rgba(var(--brand-deep-rgb), 0.06),
    0 24px 42px -10px rgba(var(--brand-deep-rgb), 0.10),
    0 50px 80px -22px rgba(var(--brand-rgb), 0.22);
}
.project-pic {
  position: relative;
  height: 284px;
  overflow: hidden;
  border-radius: 20px 20px 0 0;
  background: var(--surface-muted);
}
/* White shine — anchored at TOP-LEFT corner */
.project-pic::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    circle at 0% 0%,
    rgba(255, 255, 255, 0.75) 0%,
    rgba(255, 255, 255, 0.35) 14%,
    transparent 60%
  );
  pointer-events: none;
  z-index: 3;
  mix-blend-mode: screen;
  opacity: 1;
  transition: opacity 0.4s cubic-bezier(.2,.7,.2,1);
}
.project-pic img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1);
  z-index: 1;
  transition: transform 0.5s cubic-bezier(.2,.7,.2,1);
}
/* Hover: fade the shine out so the photograph is fully visible */
.project-card:hover .project-pic::after { opacity: 0; }
.project-card:hover .project-pic img {
  transform: scale(1.06);
}
.project-pic .stripes-overlay {
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    to right,
    rgba(0, 0, 0, 0.035) 0,
    rgba(0, 0, 0, 0.035) 4px,
    transparent 4px,
    transparent 6px
  );
  pointer-events: none;
  opacity: 1;
  z-index: 4;
  transition: opacity 0.4s ease;
}
.project-card:hover .stripes-overlay { opacity: 0; }
.project-pic .label,
.bg-stubby .label,
.bg-ipt-content .ipt,
.bg-ipt-content .duck {
  transition: transform 0.45s cubic-bezier(.2,.7,.2,1);
  position: relative;
  z-index: 1;
}
.project-card:hover .bg-stubby .label,
.project-card:hover .bg-ipt-content .ipt,
.project-card:hover .bg-ipt-content .duck {
  transform: scale(1.06);
}
.project-card .project-tag {
  transition:
    transform 0.45s cubic-bezier(.2,.7,.2,1),
    box-shadow 0.3s ease;
}
.project-card:hover .project-tag {
  transform: translateY(-2px);
  box-shadow: 4px 8px 28px rgba(0,0,0,0.3),
              inset -1px -1px 2px rgba(0,0,0,0.25),
              inset 1px 1px 2px #fff;
}
.project-tag {
  position: absolute;
  left: 20px;
  top: 18px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px 10px 16px;
  border-radius: 100px;
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
  box-shadow: 4px 4px 20px rgba(0,0,0,0.25),
              inset -1px -1px 2px rgba(0,0,0,0.25),
              inset 1px 1px 2px #fff;
  color: #fff;
  font-weight: 600;
  font-size: 13px;
  text-shadow: 2px 2px 10px rgba(0,0,0,0.5);
  z-index: 5;
}
.project-tag .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: rgba(255,255,255,0.9);
  box-shadow: 0 0 6px rgba(255,255,255,0.6);
}
/* Tag color variants */
.tag-uiux       { background: rgba(38,58,148,0.4); }
.tag-graphic    { background: rgba(232,88,20,0.4); }
.tag-editorial  { background: rgba(110,45,140,0.4); }
.tag-illustration { background: rgba(0,148,168,0.4); }
.tag-branding   { background: rgba(90,170,50,0.4); }

.project-name {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 30px 20px 30px 30px;
  background: #fff;
  border-top: 1px solid var(--border-hairline);
  border-radius: 0 0 20px 20px;
  box-shadow: 0 -4px 5px rgba(0,0,0,0.08);
  position: relative;
  /* Fixed-min height so cards are uniform regardless of text length —
     accommodates a 2-line title + 1-line description + padding. */
  min-height: 142px;
}
.project-name .info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}
.project-name .title {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: 24px;
  line-height: 28px;
  color: var(--text-secondary);
  transition: color 0.3s ease;
  /* Clamp to 2 lines max so very long titles don't blow the card
     height; shorter titles still take their natural 1 line. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.project-card:hover .project-name .title { color: #2C2623; }
.project-name .desc {
  font-size: 12px;
  line-height: 18px;
  color: var(--text-tertiary);
  /* Clamp to 1 line so descriptions stay uniform. */
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.screw {
  width: 35.69px;
  height: 35.69px;
  border-radius: 30px;
  /* Pre-multiplied color = surface-screw multiplied against white background.
     Drops the mix-blend-mode (compositing cost on every paint). */
  background: #d5d5d5;
  position: relative;
  flex-shrink: 0;
  box-shadow:
    inset -1px -1px 1px rgba(255,255,255,0.2),
    inset 0.5px 0.5px 1px rgba(0,0,0,0.25);
  /* ease-out-back: snappy + slight overshoot before settling — feels like
     a real screw being driven home */
  transition: transform 0.32s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.screw::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 3.29px;
  height: 21.73px;
  background: var(--surface-screw-detail);
  transform: translate(-50%, -50%) rotate(-45deg);
  border-radius: 1px;
  box-shadow: inset 0 0 2px rgba(0,0,0,0.5);
  /* Slot lights up almost in lockstep with the rotation — tiny 0.05s
     lead so the color/glow ramp is felt during the turn rather than
     as a separate "after" beat. */
  transition:
    transform   0.32s cubic-bezier(0.34, 1.56, 0.64, 1),
    background  0.25s ease-out 0.05s,
    box-shadow  0.25s ease-out 0.05s;
}
.project-card:hover .screw::after {
  transform: translate(-50%, -50%) rotate(-90deg);
  background: var(--brand-orange);
  box-shadow:
    inset 0 0 2px rgba(0,0,0,0.25),
    0 0 6px rgba(var(--brand-rgb), 0.85),    /* tight bright bloom */
    0 0 14px rgba(var(--brand-rgb), 0.55);   /* soft outer halo */
}

/* Project image background variants (matches Figma colorful placeholders) */
.bg-stubby {
  background: linear-gradient(135deg, var(--brand-orange) 0%, var(--brand-sunset) 100%);
  display: flex;
  align-items: center;
  justify-content: center;
}
.bg-stubby .label {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: 96px;
  line-height: 1;
  letter-spacing: -3px;
  color: #fff;
  font-weight: 700;
  text-shadow: 4px 4px 0 rgba(0,0,0,0.15);
  position: relative;
  z-index: 4;
}
.bg-ipt {
  background: linear-gradient(180deg, #FFF6D8 0%, #FFE9A3 100%);
}
.bg-ipt-content {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  z-index: 4;
}
.bg-ipt-content .ipt {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: 88px;
  line-height: 1;
  color: var(--brand-orange);
  font-weight: 700;
}
.bg-ipt-content .duck {
  font-size: 100px;
}

/* ============================================
   Filter Bar
   ============================================ */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: center;
  margin-bottom: 24px;
}
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 18px 10px 16px;
  border-radius: 100px;
  border: 1px solid var(--border-hairline);
  /* Aqua mini-pill: vertical gradient + inner top highlight + pillow shadow. */
  background:
    linear-gradient(180deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0.85) 100%);
  font-weight: 600;
  font-size: 13px;
  color: var(--text-primary);
  cursor: pointer;
  transition: background  0.45s var(--ease-liquid),
              color       0.35s var(--ease-soft),
              transform   0.45s var(--ease-bounce),
              border-color 0.35s var(--ease-soft),
              box-shadow  0.45s var(--ease-liquid);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.85),
    inset 0 -1px 0 rgba(0,0,0,0.02),
    0 2px 6px rgba(0,0,0,0.05);
}
.filter-chip:hover {
  transform: translateY(-2px);
  border-color: rgba(0,0,0,0.18);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.95),
    0 4px 10px rgba(0,0,0,0.08),
    0 12px 22px -10px rgba(var(--brand-rgb), 0.22);
}
.filter-chip .dot {
  width: 8px; height: 8px; border-radius: 50%;
  transition: background 0.3s ease;
}
.filter-chip[data-cat="all"] .dot          { background: #1A1916; }
.filter-chip[data-cat="ui-ux"] .dot        { background: var(--footer-dot-blue); }
.filter-chip[data-cat="graphic"] .dot      { background: var(--footer-dot-orange); }
.filter-chip[data-cat="editorial"] .dot    { background: var(--footer-dot-purple); }
.filter-chip[data-cat="illustration"] .dot { background: var(--footer-dot-green); }
.filter-chip[data-cat="branding"] .dot     { background: var(--footer-dot-teal); }

/* Active state — chip takes the category color, dot + text turn white,
   and casts a soft halo in its own hue so each filter feels "lit" when
   selected. --chip-glow is set per category just below; the box-shadow
   on .is-active reads it. */
.filter-chip[data-cat="all"]          { --chip-glow: rgba(  0,   0,   0, 0.35); }
.filter-chip[data-cat="ui-ux"]        { --chip-glow: rgba( 38,  58, 148, 0.55); }
.filter-chip[data-cat="graphic"]      { --chip-glow: rgba(232,  88,  20, 0.55); }
.filter-chip[data-cat="editorial"]    { --chip-glow: rgba(110,  45, 140, 0.55); }
.filter-chip[data-cat="illustration"] { --chip-glow: rgba( 90, 170,  50, 0.55); }
.filter-chip[data-cat="branding"]     { --chip-glow: rgba(  0, 148, 168, 0.55); }
.filter-chip.is-active {
  color: #fff;
  border-color: transparent;
  box-shadow:
    0 4px 14px -4px rgba(0,0,0,0.28),
    0 0 24px 0 var(--chip-glow);
}
.filter-chip.is-active .dot { background: #fff; }
.filter-chip[data-cat="all"].is-active          { background: #1A1916; }
.filter-chip[data-cat="ui-ux"].is-active        { background: var(--footer-dot-blue); }
.filter-chip[data-cat="graphic"].is-active      { background: var(--footer-dot-orange); }
.filter-chip[data-cat="editorial"].is-active    { background: var(--footer-dot-purple); }
.filter-chip[data-cat="illustration"].is-active { background: var(--footer-dot-green); }
.filter-chip[data-cat="branding"].is-active     { background: var(--footer-dot-teal); }

/* ============================================
   About Rows
   ============================================ */
.about-rows {
  background: var(--surface-subtle);
  border-bottom: 1px solid var(--border-hairline);
}
.about-row {
  display: flex;
  gap: 60px;
  align-items: flex-start;
  padding: 48px var(--container-pad);
  border-bottom: 1px solid var(--border-hairline);
  contain: layout style;
}
.about-row:last-child { border-bottom: 0; }
.about-row .row-label {
  font-weight: 700;
  font-size: 16px;
  line-height: 20px;
  color: var(--text-tertiary);
  width: 180px;
  flex-shrink: 0;
}
.about-row .row-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.about-row .row-content p {
  font-size: 16px;
  line-height: 27.2px;
  color: var(--text-secondary);
}

/* ============================================
   About Tabs (replaces stacked about-rows)
   ============================================ */
.about-tabs {
  background: var(--surface-subtle);
  border-bottom: 1px solid var(--border-hairline);
  padding: 44px 0 0;
}
.about-tab-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  margin: 0 var(--container-pad);
  border-bottom: 1px solid var(--border-hairline);
}
.about-tab {
  position: relative;
  flex: 1 1 0;
  text-align: center;
  background: none;
  border: 0;
  padding: 6px 4px 18px;
  font-weight: 600;
  font-size: 15px;
  line-height: 20px;
  color: var(--text-tertiary);
  cursor: pointer;
  transition: color 0.25s ease;
}
/* Underline indicator — slides in like the nav links */
.about-tab::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--brand-orange);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.35s var(--ease-soft);
}
.about-tab:hover { color: var(--text-primary); }
.about-tab.is-active { color: var(--brand-orange); }
.about-tab.is-active::after { transform: scaleX(1); }
.about-panel {
  padding: 40px var(--container-pad) 56px;
}
.about-panel[hidden] { display: none; }
.about-panel.is-active { animation: aboutPanelIn 0.45s var(--ease-soft) both; }
@keyframes aboutPanelIn {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}
.about-panel .panel-title {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: 26px;
  line-height: 32px;
  letter-spacing: -0.5px;
  color: var(--text-primary);
  margin-bottom: 24px;
}
.about-panel .panel-title-sub { margin: 40px 0 0; }
.about-panel .panel-content {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.about-panel .panel-content > p {
  font-size: 16px;
  line-height: 27.2px;
  color: var(--text-secondary);
  max-width: 820px;
}

/* Skill Pills (Row 02) */
.skill-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.skill-pill {
  padding: 8px 16px;
  border-radius: 100px;
  font-size: 13px;
  line-height: 20px;
  border: 1px solid var(--border-hairline);
  background:
    linear-gradient(180deg, rgba(255,255,255,0.9) 0%, rgba(247,246,244,0.9) 100%);
  color: var(--text-secondary);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.85),
    0 1px 2px rgba(0,0,0,0.03);
  transition: transform 0.4s var(--ease-bounce),
              border-color 0.35s var(--ease-soft),
              background 0.35s var(--ease-soft),
              box-shadow 0.4s var(--ease-liquid);
}
.skill-pill:hover {
  transform: translateY(-2px);
  border-color: rgba(0,0,0,0.18);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.95),
    0 4px 10px rgba(0,0,0,0.06),
    0 8px 18px -8px rgba(var(--brand-rgb), 0.2);
}
.skill-pill.is-primary {
  background: rgba(var(--brand-rgb), 0.08);
  border-color: rgba(var(--brand-rgb), 0.3);
  color: var(--brand-orange);
}
.skill-pill.is-primary:hover {
  background: rgba(var(--brand-rgb), 0.14);
  border-color: rgba(var(--brand-rgb), 0.5);
}
.client-pill {
  transition: transform 0.3s cubic-bezier(.2,.7,.2,1),
              border-color 0.3s ease,
              color 0.2s ease;
}
.client-pill:hover {
  transform: translateY(-2px);
  border-color: rgba(0,0,0,0.18);
  color: var(--text-primary);
}

/* Experience list (Row 03) */
.exp-list { display: flex; flex-direction: column; }
.exp-row {
  display: flex;
  gap: 24px;
  align-items: flex-start;
  padding: 20px 0;
  border-top: 1px solid var(--border-hairline);
}
.exp-row:first-child { border-top: 0; }
.exp-row .year {
  width: 110px;
  font-size: 12px;
  line-height: 18px;
  color: var(--text-tertiary);
  flex-shrink: 0;
}
.exp-row .role {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.exp-row .role-title {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: 22px;
  line-height: 26px;
  color: var(--text-primary);
}
.exp-row .role-place {
  font-size: 14px;
  line-height: 22px;
  color: var(--text-tertiary);
}
.exp-row .badge {
  font-size: 10px;
  line-height: 14px;
  letter-spacing: 0.6px;
  color: var(--text-tertiary);
  white-space: nowrap;
}

/* Tools grid (Row 04) */
.tools-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  width: 100%;
}
.tool-card {
  background:
    linear-gradient(180deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0.92) 100%);
  border: 1px solid var(--border-hairline);
  border-radius: 16px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.85),
    0 4px 14px rgba(0,0,0,0.06),
    0 14px 28px -10px rgba(0,0,0,0.08);
  cursor: default;
  transition: transform 0.55s var(--ease-bounce),
              box-shadow 0.55s var(--ease-liquid),
              border-color 0.35s var(--ease-soft);
}
.tool-card:hover {
  transform: translateY(-4px);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.95),
    0 8px 22px rgba(0,0,0,0.08),
    0 22px 44px -14px rgba(var(--brand-rgb), 0.22);
  border-color: rgba(0,0,0,0.12);
}
.tool-icon {
  transition: transform 0.5s cubic-bezier(.2,.7,.2,1);
}
.tool-card:hover .tool-icon { transform: scale(1.08) rotate(-3deg); }
.tool-icon {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.tool-icon img { width: 100%; height: 100%; object-fit: cover; }
.tool-icon.bg-figma     { background: linear-gradient(135deg, #646464 0%, #000 71.4%); }
.tool-icon.bg-claude    { background: linear-gradient(135deg, #1A1916 0%, #575757 71.4%); }
.tool-icon.bg-photoshop { background: linear-gradient(135deg, #00949C 0%, #1E2A8A 71.4%); }
.tool-icon.bg-illustrator { background: linear-gradient(135deg, #6E2D8C 0%, #C93761 71.4%); }
.tool-icon.bg-indesign  { background: linear-gradient(135deg, #5AAA32 0%, #2F6418 71.4%); }
.tool-name {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: 22px;
  line-height: 26px;
  color: var(--text-primary);
}
.tool-desc {
  font-size: 12px;
  line-height: 18px;
  color: var(--text-tertiary);
}

/* Facts grid (Row 05) */
.facts-grid {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding-top: 12px;
}
.fact-row {
  display: flex;
  gap: 16px;
  align-items: center;
  padding: 14px 0;
  border-bottom: 1px solid var(--border-hairline);
}
.fact-row .key {
  width: 90px;
  font-size: 10px;
  line-height: 14px;
  letter-spacing: 0.6px;
  color: var(--text-tertiary);
  flex-shrink: 0;
}
.fact-row .val {
  flex: 1;
  font-size: 15px;
  line-height: 25px;
  color: var(--text-primary);
}

/* Clients (Row 06) */
.client-list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.client-pill {
  padding: 8px 16px;
  border-radius: 100px;
  font-size: 13px;
  line-height: 20px;
  border: 1px solid var(--border-hairline);
  background: var(--surface-subtle);
  color: var(--text-secondary);
}

/* ============================================
   Project Detail Page
   ============================================ */
.project-hero {
  position: relative;
  overflow: hidden;
  background: #fff;
  border-bottom: 1px solid var(--border-hairline);
}
.project-hero-inner {
  position: relative;
  z-index: 2;
  padding: 80px var(--container-pad) 60px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 48px;
  align-items: center;
  max-width: calc(var(--content-width) + 2 * var(--container-pad));
  margin: 0 auto;
  width: 100%;
}
.project-hero-text {
  display: flex;
  flex-direction: column;
  gap: 20px;
  min-width: 0;
}
.project-hero-image {
  border-radius: 20px;
  overflow: hidden;
  background: var(--surface-muted);
  box-shadow: 0 20px 48px rgba(0,0,0,0.12);
  position: relative;
}
.project-hero-image img {
  width: 100%;
  height: auto;
  display: block;
}
@media (max-width: 900px) {
  .project-hero-inner {
    grid-template-columns: 1fr;
    gap: 28px;
  }
}
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  color: var(--text-tertiary);
  width: fit-content;
  padding: 6px 12px;
  margin-left: -12px;
  border-radius: 8px;
  transition: color 0.2s ease, background 0.2s ease;
}
.back-link:hover {
  color: var(--brand-orange);
  background: rgba(var(--brand-rgb), 0.06);
}
.back-link span { transition: transform 0.25s ease; display: inline-block; }
.back-link:hover span { transform: translateX(-3px); }

.project-tag-static {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  width: fit-content;
  padding: 8px 16px 8px 14px;
  border-radius: 100px;
  font-weight: 600;
  font-size: 13px;
  color: #fff;
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
  box-shadow: 4px 4px 20px rgba(0,0,0,0.18);
}
.project-tag-static .dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: rgba(255,255,255,0.9);
}
.project-tag-static.tag-uiux        { background: var(--footer-dot-blue); }
.project-tag-static.tag-graphic     { background: var(--brand-sunset); }
.project-tag-static.tag-editorial   { background: var(--footer-dot-purple); }
.project-tag-static.tag-illustration { background: var(--footer-dot-green); }
.project-tag-static.tag-branding    { background: var(--footer-dot-teal); }

.project-title {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: clamp(32px, 5.5vw, 56px);
  line-height: 1.05;
  letter-spacing: -1.5px;
  color: var(--text-primary);
}
/* The project page ships placeholder text ("Project title" etc.) that
   is swapped in by JS once /api/content arrives — hide it until then
   so the placeholder never flashes. The renderer adds .project-ready
   to <html> after filling in the real values. visibility (not display)
   so the hero doesn't collapse and reflow when the text appears. */
.project-hero-text .project-tag-static,
.project-hero-text .project-title,
.project-hero-text .project-lead,
.project-hero-text .project-meta {
  visibility: hidden;
}
html.project-ready .project-hero-text .project-tag-static,
html.project-ready .project-hero-text .project-title,
html.project-ready .project-hero-text .project-lead,
html.project-ready .project-hero-text .project-meta {
  visibility: visible;
}
.project-lead {
  font-size: 18px;
  line-height: 1.55;
  color: var(--text-secondary);
  max-width: 60ch;
}
.project-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 40px;
  align-items: flex-start;
  padding-top: 24px;
  margin-top: 8px;
  border-top: 1px solid var(--border-hairline);
}
.project-meta > div {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 100px;
}
.project-meta dt {
  font-size: 11px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--text-tertiary);
  font-weight: 600;
}
.project-meta dd {
  font-size: 15px;
  color: var(--text-primary);
}
.project-meta .meta-link {
  margin-left: auto;
  align-self: center;
}
.project-meta .meta-link a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 18px;
  border-radius: 100px;
  background: var(--brand-orange);
  color: #fff;
  font-weight: 600;
  font-size: 13px;
  transition: background 0.2s ease, transform 0.2s ease;
}
.project-meta .meta-link a:hover {
  background: var(--brand-sunset);
  transform: translateY(-2px);
}

/* (legacy .project-cover-wrap removed — hero image now sits inside .project-hero-inner) */

/* Project body — alternating text + image blocks */
.project-body {
  background: var(--surface-subtle);
  padding: 60px var(--container-pad) 80px;
  border-bottom: 1px solid var(--border-hairline);
  content-visibility: auto;
  contain-intrinsic-size: 1px 800px;
}
.project-body-inner {
  max-width: var(--content-width);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 40px;
}
.proj-block-text {
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: 60px;
  align-items: flex-start;
  padding-top: 16px;
}
.proj-block-text .row-label {
  font-weight: 700;
  font-size: 16px;
  color: var(--text-tertiary);
}
.proj-block-body {
  display: flex;
  flex-direction: column;
  gap: 16px;
  font-size: 17px;
  line-height: 1.7;
  color: var(--text-secondary);
  max-width: 65ch;
}
.proj-block-image {
  margin: 0;
  border-radius: 16px;
  overflow: hidden;
  background: #fff;
  box-shadow: 0 12px 32px rgba(0,0,0,0.08);
}
.proj-block-image img,
.proj-block-image video {
  width: 100%;
  height: auto;
  display: block;
  cursor: zoom-in;
  /* Pre-promote to a GPU layer so the FIRST hover doesn't allocate one
     mid-animation (which causes the visible startup hitch). The empty
     translateZ(0) is the cheapest layer hint. */
  transform: translateZ(0);
  backface-visibility: hidden;
  transition: transform 0.6s cubic-bezier(.2,.7,.2,1);
}
/* Video blocks aren't lightbox-zoomable — plain cursor. */
.proj-block-image video { cursor: default; }
.proj-block-image:hover img,
.proj-block-image:hover video { transform: translateZ(0) scale(1.02); }
.proj-block-image figcaption {
  padding: 12px 20px 16px;
  font-size: 13px;
  color: var(--text-tertiary);
  background: var(--surface-subtle);
  border-top: 1px solid var(--border-hairline);
}

@media (max-width: 900px) {
  .proj-block-text { grid-template-columns: 1fr; gap: 12px; }
}

.project-more {
  padding: 60px var(--container-pad);
  background: var(--surface-subtle);
  border-bottom: 1px solid var(--border-hairline);
}
.project-more .section-header { margin-bottom: 24px; }

@media (max-width: 900px) {
  .project-title { font-size: 40px; }
  .project-about-grid { grid-template-columns: 1fr; gap: 16px; }
  .project-about-grid .row-label { position: static; }
  .project-gallery-grid { grid-template-columns: 1fr; }
  .project-gallery-grid .gallery-item:nth-child(3n+1) { grid-column: span 1; }
  .project-meta .meta-link { margin-left: 0; }
}

/* ============================================
   Now Playing — Spotify embed
   ============================================ */
.now-playing {
  position: relative;
  padding: 56px var(--container-pad) 72px;
  background: var(--surface-default);
  border-top: 1px solid var(--border-hairline);
}
.now-playing-inner {
  max-width: 720px;
  margin: 0 auto;
  text-align: center;
}
.now-playing-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--brand-orange);
  margin: 0 0 6px;
}
.now-playing-title {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: clamp(28px, 4.5vw, 44px);
  line-height: 1.1;
  margin: 0 0 28px;
  color: var(--text-primary);
}
/* iMac G3 candy shell wrapping a Spotify "screen". The translucent
   coloured padding around the iframe IS the iMac casing; the wet
   catchlight at the top reads as the plastic dome, and the bottom
   strip uses the same vertical-line speaker-grille pattern the rest
   of the site uses on the profile-actions row. */
.spotify-frame {
  position: relative;
  padding: 18px 18px 26px;
  border-radius: 28px;
  background: linear-gradient(180deg,
    rgba(var(--brand-rgb), 0.18) 0%,
    rgba(var(--brand-rgb), 0.10) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,1),
    inset 0 -1px 0 rgba(0,0,0,0.05),
    inset 0 0 0 1px rgba(255,255,255,0.55),
    0 8px 22px rgba(0,0,0,0.10),
    0 24px 44px -10px rgba(var(--brand-deep-rgb), 0.18),
    0 50px 80px -22px rgba(0,0,0,0.18);
}
/* Top wet catchlight — confined to the shell's top edge, not over the
   iframe, so the Spotify UI stays crisp. */
.spotify-frame::before {
  content: "";
  position: absolute;
  top: 1px; left: 1px; right: 1px;
  height: 26px;
  border-radius: 27px 27px 6px 6px;
  background: linear-gradient(180deg,
    rgba(255,255,255,0.7)  0%,
    rgba(255,255,255,0.18) 55%,
    rgba(255,255,255,0)    100%);
  pointer-events: none;
}
/* Speaker-grille stripes along the bottom edge — the same fine
   vertical lines used on the profile-actions row. */
.spotify-frame::after {
  content: "";
  position: absolute;
  left: 18px; right: 18px; bottom: 8px;
  height: 10px;
  background: repeating-linear-gradient(90deg,
    rgba(0,0,0,0.12) 0,
    rgba(0,0,0,0.12) 1px,
    transparent      1px,
    transparent      4px);
  border-radius: 5px;
  opacity: 0.5;
  pointer-events: none;
}
.spotify-frame iframe {
  display: block;
  width: 100%;
  border: 0;
  border-radius: 14px;
  position: relative;
  /* Subtle recessed bezel — the iframe reads as a CRT screen set into
     the plastic shell rather than floating on top of it. */
  box-shadow:
    inset 0 1px 2px rgba(0,0,0,0.18),
    0 1px 0 rgba(255,255,255,0.4);
}
@media (max-width: 760px) {
  .now-playing { padding: 40px var(--container-pad) 56px; }
}

/* ============================================
   Contact CTA
   ============================================ */
.contact-cta {
  position: relative;
  overflow: hidden;
  padding: 60px var(--container-pad);
  background: var(--surface-subtle);
  border-top: 1px solid var(--border-hairline);
  box-shadow: 0 -4px 4px rgba(0,0,0,0.05);
  /* Same reason as .hero — drop paint containment so internal stripes
     don't snap to a different sub-pixel layer offset across transitions. */
  contain: layout style;
}
.contact-blob {
  position: absolute;
  bottom: -665px;
  right: calc(var(--frame-edge-offset) - 316px);
  width: 1195px;
  height: 1195px;
  border-radius: 1000px;
  background: radial-gradient(circle at center, rgba(var(--brand-rgb), 0.12) 0%, rgba(var(--brand-rgb), 0) 70%);
  pointer-events: none;
}
.contact-cta-inner { position: relative; z-index: 2; }
.contact-header {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-bottom: 24px;
}
.contact-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-weight: 700;
  font-size: 20px;
  color: var(--text-tertiary);
}
.contact-divider {
  width: 100%;
  height: 1px;
  background: var(--border-hairline);
  border: 0;
}
.contact-title {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: clamp(36px, 7.2vw, 80px);
  line-height: 1.05;
  letter-spacing: -2.4px;
  color: var(--text-primary);
  margin-bottom: 24px;
}
.contact-title em {
  font-style: italic;
  color: var(--brand-orange);
  display: inline-block;
  transition: transform 0.5s cubic-bezier(.2,.7,.2,1);
}
.contact-title:hover em { transform: translateY(-4px) rotate(-2deg); }
.contact-bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}
.contact-bottom .left {
  font-size: 16px;
  line-height: 27.2px;
  color: var(--text-tertiary);
  padding: 10px 0;
}
.contact-bottom .right {
  display: flex;
  align-items: center;
  gap: 10px;
}
.contact-email {
  padding: 10px 20px;
  color: var(--text-tertiary);
  font-size: 16px;
  line-height: 27.2px;
  transition: color 0.15s;
}
.contact-email:hover { color: var(--brand-orange); }
.linkedin-icon { width: 36px; height: 36px; display: block; }

/* ============================================
   Footer
   ============================================ */
.footer {
  background: var(--surface-subtle);
  border-top: 1px solid var(--border-hairline);
  box-shadow: 0 -4px 5px rgba(0,0,0,0.05);
  padding: 33px var(--container-pad) 32px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}
.footer .col {
  font-size: 10px;
  line-height: 14px;
  letter-spacing: 0.6px;
  color: var(--text-tertiary);
}
.footer .col.right { text-align: right; }
.footer-dots {
  display: flex;
  gap: 8px;
  align-items: center;
}
.footer-dot {
  width: 10px;
  height: 10px;
  border-radius: 5px;
  position: relative;
  background-image: linear-gradient(180deg, rgba(0,0,0,0.03) 0%, rgba(0,0,0,0.03) 12.5%, rgba(0,0,0,0) 12.5%, rgba(0,0,0,0) 87.5%);
  /* Decorative — the hover scale is a hint of life; there's no click
     action anymore (palette-swap was removed). */
  transition: transform 0.2s cubic-bezier(.2,.7,.2,1);
}
.footer-dot:hover { transform: scale(1.6); }
.footer-dot.purple { background-color: rgba(110,45,140,0.22); border: 1px solid rgba(110,45,140,0.3); }
.footer-dot.orange { background-color: rgba(232,88,20,0.2); border: 1px solid rgba(232,88,20,0.28); }
.footer-dot.teal   { background-color: rgba(0,148,168,0.2); border: 1px solid rgba(0,148,168,0.28); }
.footer-dot.blue   { background-color: rgba(38,58,148,0.2); border: 1px solid rgba(38,58,148,0.28); }
.footer-dot.green  { background-color: rgba(90,170,50,0.2); border: 1px solid rgba(90,170,50,0.25); }

/* ============================================
   Responsive
   ============================================ */

/* Tablet — small laptops & landscape iPads */
@media (max-width: 1024px) {
  .tools-grid { grid-template-columns: repeat(2, 1fr); }
  .hero-about { gap: 60px; }
  .profile-card { width: 320px; }
}

/* Tablet portrait & large phones */
@media (max-width: 768px) {
  .nav-links { gap: 6px; }
  .nav-link { padding: 10px 14px; font-size: 14px; }
  .hero-inner { min-height: 480px; padding: 70px var(--container-pad); gap: 28px; }
  .hero-works { min-height: 280px; padding: 50px var(--container-pad); }
  /* Project hero on detail page → stack columns */
  .project-hero-inner { grid-template-columns: 1fr; gap: 28px; padding: 56px var(--container-pad) 40px; }
  .project-hero-image { max-width: 480px; margin-left: auto; margin-right: auto; }
  .project-meta { gap: 24px; padding-top: 18px; }
  .project-meta .meta-link { margin-left: 0; }
  /* About hero stacks */
  .hero-about { flex-direction: column; gap: 32px; padding: 40px var(--container-pad); }
  .hero-about-content { flex: 0 1 auto; min-height: 0; gap: 18px; width: 100%; }
  .profile-card { width: 100%; max-width: 380px; align-self: center; }
  /* About rows: label on top of content */
  .about-row { flex-direction: column; gap: 14px; padding: 36px var(--container-pad); }
  .about-row .row-label { width: auto; }
  /* About tabs: horizontal scroll bar, tighter panels */
  .about-tabs { padding-top: 32px; }
  .about-tab-bar {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    gap: 10px;
  }
  .about-tab-bar::-webkit-scrollbar { display: none; }
  .about-tab { flex-shrink: 0; padding-bottom: 14px; font-size: 14px; }
  .about-panel { padding: 30px var(--container-pad) 44px; }
  /* Contact CTA bottom stacks */
  .contact-bottom { flex-direction: column; align-items: flex-start; gap: 16px; }
  .contact-bottom .right { flex-wrap: wrap; }
  /* Footer stacks */
  .footer { flex-direction: column; align-items: flex-start; padding: 24px var(--container-pad); }
  .footer .col.right { text-align: left; }
  /* Section padding tighter */
  .section { padding: 50px var(--container-pad); }
  .project-body { padding: 50px var(--container-pad) 60px; }
  .project-more { padding: 50px var(--container-pad); }
  .contact-cta { padding: 50px var(--container-pad); }
  /* Content block text rows stack */
  .proj-block-text { grid-template-columns: 1fr; gap: 12px; }
  .proj-block-text .row-label { position: static; }
  /* Project gallery 1 col */
  .project-gallery-grid { grid-template-columns: 1fr; }
  .project-gallery-grid .gallery-item:nth-child(3n+1) { grid-column: span 1; }
}

/* Phones */
@media (max-width: 600px) {
  /* Tighter side padding handled by --container-pad: 16px */
  .nav-inner { height: 60px; padding: 10px var(--container-pad); }
  .nav-links { gap: 2px; }
  .nav-link { padding: 8px 10px; font-size: 13px; }
  .nav-link::after { left: 10px; right: 10px; }

  /* Projects single column */
  .projects-grid { grid-template-columns: 1fr; gap: 16px; }
  .project-pic { height: 220px; }
  .project-pic .label { font-size: 64px !important; }
  .bg-ipt-content .ipt { font-size: 64px !important; }
  .bg-ipt-content .duck { font-size: 72px !important; }
  .project-name { padding: 24px 20px; gap: 12px; }
  .project-name .title { font-size: 20px; }

  /* Hero buttons stack full width, spanning to the hero padding. */
  .hero-actions { flex-direction: column; width: 100%; max-width: none; align-self: stretch; gap: 12px; }
  .btn-pill { width: 100%; justify-content: center; padding: 16px; }
  .hero-desc { font-size: 15px; }

  /* Filter bar — horizontal scroll on phones (instead of wrap).
     overflow-x: auto forces overflow-y to also clip, so the chip's
     coloured glow (up to ~24px) was being chopped at the top and
     bottom. Bumping the vertical padding gives the glow room inside
     the bar's box without needing to mess with the overflow rules. */
  .filter-bar {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    justify-content: flex-start;
    margin-left: calc(var(--container-pad) * -1);
    margin-right: calc(var(--container-pad) * -1);
    padding: 22px var(--container-pad) 22px;
    scroll-padding: var(--container-pad);
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .filter-bar::-webkit-scrollbar { display: none; }
  .filter-chip { flex-shrink: 0; }

  /* Project detail hero meta wraps */
  .project-hero-inner { padding: 40px var(--container-pad) 32px; gap: 24px; }
  .project-meta { gap: 18px; }
  .project-meta > div { min-width: 80px; }

  /* About hero & rows tighter */
  .hero-about { gap: 24px; padding: 32px var(--container-pad); }
  .hero-about-eyebrow { font-size: 15px; }
  .about-row { padding: 28px var(--container-pad); gap: 12px; }
  .about-row .row-label { font-size: 14px; }
  .about-row .row-content p { font-size: 15px; line-height: 1.6; }

  /* Section header */
  .section-header { flex-direction: column; align-items: flex-start; gap: 6px; margin-bottom: 18px; }
  .section { padding: 40px var(--container-pad); }
  .project-body { padding: 40px var(--container-pad) 50px; }
  .project-more { padding: 40px var(--container-pad); }
  .contact-cta { padding: 40px var(--container-pad); }
  .contact-bottom { gap: 12px; }
  .contact-email { padding: 8px 0; }

  /* Profile card full width on phone */
  .profile-card { max-width: none; }
  .profile-image { height: 320px; }

  /* Tools 1 col */
  .tools-grid { grid-template-columns: 1fr; }

  /* Experience row condensed */
  .exp-row { gap: 12px; flex-wrap: wrap; padding: 16px 0; }
  .exp-row .year { width: 90px; }
  .exp-row .badge { width: 100%; padding-left: 102px; margin-top: -4px; }

  /* Fact rows */
  .fact-row { gap: 10px; }
  .fact-row .key { width: 70px; font-size: 9px; }
  .fact-row .val { font-size: 14px; line-height: 1.45; }

  /* Skill / client pills slightly smaller */
  .skill-pill, .client-pill { font-size: 12.5px; padding: 6px 12px; }

  /* Body block text reduce */
  .proj-block-body { font-size: 16px; line-height: 1.65; }

  /* Touch targets — make sure tap areas are ≥44px */
  .reaction-btn { min-height: 44px; padding: 6px 0; }
  .filter-chip { min-height: 40px; }
}

/* ============================================
   Mobile performance overrides
   ============================================
   Properties that look great on desktop but cost frames on mid-range
   phones. Removing them on small viewports gives the page transitions
   and scroll the headroom they need. */
@media (max-width: 760px) {
  /* `backdrop-filter` is the most expensive GPU op on mobile (especially
     iOS Safari + mid-range Android). Replace with a slightly darker solid
     so the tags stay readable without the blur cost. */
  .project-tag,
  .project-tag-static {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  /* `mix-blend-mode: screen` forces a separate compositing layer for the
     whole element; on a long page of project cards this adds up. */
  .project-pic::after { mix-blend-mode: normal; opacity: 0.7; }

  /* Hover transitions don't fire on mobile, but the GPU still has to
     prep an image-scale layer when the card scrolls into view. Drop
     the transform-transition costs by reverting on hoverless devices. */
}
@media (hover: none) and (pointer: coarse) {
  .project-card:hover .project-pic img { transform: none; }
  .project-pic img { transition: none; }

  /* ───── Tap micro-feedback ─────
     On touch devices there's no hover state, so a brief 0.96 scale on
     :active gives a satisfying "press" feel without any extra layout
     work. Cheap (transform only) and universally smoothing. */
  .btn-pill,
  .nav-btn,
  .reaction-btn,
  .filter-chip,
  .project-card,
  .nav-link,
  .photo-card,
  .skill-pill,
  .client-pill {
    transition: transform 0.15s var(--ease-soft, ease-out);
  }
  .btn-pill:active,
  .nav-btn:active,
  .reaction-btn:active,
  .filter-chip:active,
  .project-card:active,
  .photo-card:active {
    transform: scale(0.97);
  }
  /* Buttons that already have transform animations (e.g. nav-btn has its
     own translate) shouldn't lose those — scope by selector instead. */
}

/* ============================================
   Image load fade — one-shot
   ============================================
   Images get `.img-fading` on insertion (CSS), then `.img-faded` once
   their natural-height resolves (toggled by page-transitions.js). This
   replaces the abrupt "image pops in" with a 450ms ease.

   IMPORTANT: uses @keyframes (not `transition`) so it doesn't clobber
   other transitions defined on `img` elements — e.g. the `transform`
   transition on `.proj-block-image img` would otherwise be wiped out
   by the `transition` shorthand here. */
img.img-fading       { opacity: 0; }
img.img-fading.img-faded {
  animation: imgFadeIn 0.45s var(--ease-soft, ease-out) both;
}
@keyframes imgFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ============================================
   Lightbox — full-screen image viewer
   ============================================
   Wired up by /lightbox.js. The overlay is appended to <body> on the
   first click on a `.proj-block-image img`. Mounted always-on for
   simplicity; pointer-events: none until `.is-open` so it doesn't eat
   clicks behind it. */
.lightbox-locked, .lightbox-locked body { overflow: hidden; }

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  /* Match the buttery page-transition curve so opening feels like a
     continuation of the same motion language. */
  transition: opacity 0.42s var(--ease-soft);
}
.lightbox.is-open { opacity: 1; pointer-events: auto; }

.lightbox-scrim {
  position: absolute;
  inset: 0;
  background: rgba(15, 15, 12, 0.92);
  cursor: zoom-out;
}

.lightbox-figure {
  position: relative;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-width: 92vw;
  max-height: 92vh;
  z-index: 2;
  /* Tiny scale-in so the image grows into place when the lightbox opens */
  transform: scale(0.96) translateY(8px);
  transition: transform 0.55s var(--ease-flow);
}
.lightbox.is-open .lightbox-figure { transform: scale(1) translateY(0); }

.lightbox-img {
  display: block;
  max-width: 92vw;
  max-height: calc(92vh - 60px); /* leave room for caption */
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 12px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
  background: #111;
  cursor: zoom-out;
  /* Smooth crossfade when navigating between images */
  transition: opacity 0.18s ease-out;
  /* Inherit the no-save protections from img {} but reinforce them
     here so the casual save paths stay closed for the big copy too. */
  -webkit-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none;
  user-select: none;
}

.lightbox-caption {
  text-align: center;
  color: rgba(255, 255, 255, 0.78);
  font-size: 13px;
  line-height: 1.5;
  max-width: 80vw;
  font-style: italic;
}
.lightbox-caption[hidden] { display: none; }

.lightbox-close,
.lightbox-prev,
.lightbox-next {
  position: absolute;
  width: 46px;
  height: 46px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.10);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 3;
  transition: background 0.18s, transform 0.18s, border-color 0.18s;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}
.lightbox-close:hover,
.lightbox-prev:hover,
.lightbox-next:hover {
  background: rgba(255, 255, 255, 0.22);
  border-color: rgba(255, 255, 255, 0.32);
}
.lightbox-close { top: 22px; right: 22px; }
.lightbox-close:hover { transform: scale(1.06); }
.lightbox-prev { left: 22px; top: 50%; transform: translateY(-50%); }
.lightbox-next { right: 22px; top: 50%; transform: translateY(-50%); }
.lightbox-prev:hover { transform: translateY(-50%) scale(1.06); }
.lightbox-next:hover { transform: translateY(-50%) scale(1.06); }
.lightbox-prev[hidden],
.lightbox-next[hidden] { display: none; }

/* Mobile: lose the backdrop blur (expensive) + slightly smaller buttons */
@media (max-width: 600px) {
  .lightbox-close,
  .lightbox-prev,
  .lightbox-next {
    width: 40px;
    height: 40px;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(0, 0, 0, 0.55);
    border-color: rgba(255, 255, 255, 0.12);
  }
  .lightbox-close { top: 14px; right: 14px; }
  .lightbox-prev { left: 10px; }
  .lightbox-next { right: 10px; }
  .lightbox-img { border-radius: 8px; max-height: calc(92vh - 52px); }
  .lightbox-caption { font-size: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  .lightbox,
  .lightbox-figure,
  .lightbox-img { transition: none !important; }
  .lightbox-figure { transform: none !important; }
}

/* Scroll-velocity bounce was removed. Sections stay put on scroll. */

/* When the hero→works snap (or any anchor scroll) lands on the works
   section, stop with the section's top edge at the sticky nav's bottom
   edge (70px + the iOS status-bar inset when present). 2px shy so the
   hero fully clears the nav's intersection line and the nav flips to
   its white linked state instead of hanging in the transparent
   over-hero boundary case. */
#works-section { scroll-margin-top: calc(68px + env(safe-area-inset-top, 0px)); }

