/* =========================================================
   ADITYA VERMA · PORTFOLIO V3
   Aesthetic: warm cream paper · industrial display · cyber accents
   ========================================================= */

:root {
  /* Palette */
  --cream: #F2EEE5;
  --cream-2: #E8E3D5;
  --cream-3: #D8D1BE;
  --ink: #0C0C0A;
  --ink-2: #1B1B17;
  --ink-mute: #5F5A4F;
  --line: #BFB6A1;
  --line-soft: rgba(12, 12, 10, 0.10);
  --line-faint: rgba(12, 12, 10, 0.06);

  --accent: #D8FF38;          /* electric chartreuse */
  --accent-deep: #B4DD18;
  --accent-ink: #1B2200;
  --hot: #FF5A1F;             /* sparingly used coral */
  --glass: rgba(255, 253, 247, 0.55);
  --glass-stroke: rgba(12, 12, 10, 0.08);

  /* Type */
  --f-display: "Anton", "Archivo Black", sans-serif;
  --f-serif: "Instrument Serif", "Fraunces", serif;
  --f-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
  --f-mono: "Geist Mono", ui-monospace, monospace;
  --f-sign: "Dancing Script", "Brush Script MT", cursive;

  /* Spacing scale */
  --gutter: clamp(20px, 4vw, 64px);
  --section-y: clamp(80px, 12vw, 180px);

  /* Fluid type scale — clamp(mobile-safe min, vw, 2K-matching max). The MAX equals
     the value the 2560 design was tuned at, so the wide look is unchanged, while the
     vw middle scales everything proportionally down to phones. Most large type in
     this file already uses inline clamp(); this token covers the small mono labels
     so they scale too instead of looking oversized on smaller / OS-scaled viewports. */
  --fs-label: clamp(10px, 0.72vw, 11px);   /* mono eyebrows / HUD tags */

  /* Responsive breakpoints — vanilla @media mirroring Tailwind sm/md/lg/xl/2xl:
       sm 640px · md 768px · lg 1024px · xl 1280px · 2xl 1536px
     (CSS custom properties can't be used inside @media, so these are the agreed
     numeric thresholds the media queries below use.) */

  /* Easing */
  --ease-out-quart: cubic-bezier(.25,1,.5,1);
  --ease-soft: cubic-bezier(.6,.05,.2,1);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  font-family: var(--f-sans);
  background: var(--cream);
  color: var(--ink);
}

body {
  font-family: var(--f-sans);
  font-weight: 400;
  font-size: clamp(10px, 0.63vw, 16px);
  line-height: 1.45;
  color: var(--ink);
  background: var(--cream);
  overflow-x: clip;
  position: relative;
  letter-spacing: -0.005em;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; background: none; border: none; cursor: pointer; color: inherit; }
ul, ol { list-style: none; padding: 0; margin: 0; }

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

/* ----------- GLOBAL OVERLAYS ----------- */
/* Slow drifting aurora — barely-there color light across the page.
   The wrapper takes scroll parallax (set by GSAP). The ::before
   pseudo holds the gradient + slow drift animation, so the two
   transforms never compete. */
.aurora {
  position: fixed;
  inset: -12vh -12vw;
  pointer-events: none;
  z-index: 0;
  will-change: transform;
}
.aurora::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(900px circle at 22% 30%, rgba(216, 255, 56, 0.06), transparent 65%),
    radial-gradient(720px circle at 78% 78%, rgba(255, 90, 31, 0.035), transparent 70%),
    radial-gradient(620px circle at 60% 12%, rgba(180, 221, 24, 0.04), transparent 70%);
  animation: auroraDrift 28s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes auroraDrift {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); }
  100% { transform: translate3d(4vw, 3vh, 0) rotate(2deg); }
}

/* Film-grain — animated step-shift gives the calm 24fps cinema feel.
   Perf: oversized + animated via translate3d (compositor only, no
   per-frame paint) and a plain normal blend instead of mix-blend-mode
   multiply — the old multiply forced a full-viewport backdrop re-blend
   on every frame of every scroll/animation, site-wide. Over this cream
   palette a low-opacity black grain reads the same either way. */
.grain {
  position: fixed; inset: -40px;   /* oversize so the shift never reveals an edge */
  pointer-events: none;
  z-index: 200;
  opacity: 0.28;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.4' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.55 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-size: 220px 220px;
  animation: grainShift 1.6s steps(6) infinite;
}
@keyframes grainShift {
  0%   { transform: translate3d(  0px,   0px, 0); }
  16%  { transform: translate3d(-14px,   9px, 0); }
  32%  { transform: translate3d( 18px, -11px, 0); }
  50%  { transform: translate3d( -7px, -15px, 0); }
  66%  { transform: translate3d( 12px,   6px, 0); }
  82%  { transform: translate3d(-16px,   3px, 0); }
  100% { transform: translate3d(  0px,   0px, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .grain { animation: none; }
}

/* -------------------------------------------------------
   PERF-LITE — adaptive low-frame-rate fallback.
   The runtime FPS probe (script.js · initPerfProbe) adds
   .perf-lite to <body> when the device can't sustain a
   smooth frame rate. It FREEZES the always-on ambient
   animations — the layers stay visible and blurred, they
   just stop repainting the full viewport every frame, which
   roughly doubles FPS on weak GPUs. The JS-driven character
   reel is unaffected and keeps animating. ?lite=1/0 forces.
   ------------------------------------------------------- */
body.perf-lite .aurora,
body.perf-lite .grain,
body.perf-lite .topo-bg,
body.perf-lite .hero-stage__halo,
body.perf-lite .hero__scroll svg,
body.perf-lite .hero__welcome,
body.perf-lite .hero__welcome--right {
  animation-play-state: paused !important;
}

.topo-bg {
  position: fixed;
  inset: -10vh -10vw -10vh -10vw;
  width: 120vw; height: 120vh;
  z-index: 0;
  pointer-events: none;
  color: rgba(12, 12, 10, 0.22);
  opacity: 0.55;
  /* Parallax handled in JS via translate3d on a child wrapper transform */
  will-change: transform;
}
.topo-bg > g {
  transform-origin: center;
  animation: topoDrift 90s ease-in-out infinite;
}
@keyframes topoDrift {
  0%   { transform: translate3d(0,0,0) rotate(0deg) scale(1); }
  50%  { transform: translate3d(-1.5%, 1%, 0) rotate(0.4deg) scale(1.015); }
  100% { transform: translate3d(0,0,0) rotate(0deg) scale(1); }
}

/* ----------- SCROLL PROGRESS ----------- */
.scroll-progress {
  position: fixed; top: 0; left: 0; right: 0;
  height: 2px; z-index: 250;
  background: var(--line-faint);
}
.scroll-progress span {
  display: block; height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--ink), var(--accent-deep));
  transition: width 80ms linear;
}

/* =========================================================
   PRELOADER — all styles + the noise-animation keyframe are
   inlined in index.html's <head> so they apply on first paint
   regardless of styles.css cache state. Nothing here on purpose.
   ========================================================= */

/* =========================================================
   PRE-LOAD UI — the hero UI starts hidden while the intro
   loader is on screen. JS removes `is-pre-load` from <body>
   at the moment the loader fades, and each element
   transitions to its resting place on a staggered delay so
   the hero "constructs itself" in front of the user
   (nav → eyebrows → title → status → tagline → cards).
   ========================================================= */
body.is-pre-load .nav,
body.is-pre-load .hero__eyebrow,
body.is-pre-load .hero__title-word,
body.is-pre-load .hero__title-italic,
body.is-pre-load .hero__tagline,
body.is-pre-load .hero__status,
body.is-pre-load .hero__card {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  pointer-events: none;
}
/* The nav slides DOWN from above the viewport edge instead
   of up from below. */
body.is-pre-load .nav {
  transform: translate3d(0, -18px, 0);
}
/* While the loader is up, the hero canvas is hidden so the
   only character on screen is the one inside the preview box.
   When is-pre-load drops the canvas crossfades in at the same
   moment the loader's character fades out — and because both
   are positioned identically (height:100%, bottom:-1vh, 16/15),
   the swap is invisible. */
body.is-pre-load .hero-stage__canvas {
  opacity: 0;
}
body:not(.is-pre-load) .hero-stage__canvas {
  opacity: 1;
  transition: opacity 0.6s var(--ease-out-quart);
}

/* Keep the preload progress bar hidden through the greeting +
   curtain lift so the sheet reveals a clean stage; it fades back
   in only once the hero takes over (by then loading is usually
   done, so in practice it never shows). */
body.is-pre-load .hero__loading {
  opacity: 0;
}

body:not(.is-pre-load) .nav,
body:not(.is-pre-load) .hero__eyebrow,
body:not(.is-pre-load) .hero__title-word,
body:not(.is-pre-load) .hero__title-italic,
body:not(.is-pre-load) .hero__tagline,
body:not(.is-pre-load) .hero__status,
body:not(.is-pre-load) .hero__card {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  transition: opacity 0.6s var(--ease-out-quart),
              transform 0.6s var(--ease-out-quart);
}

/* Stagger — each element fires off the moment is-pre-load
   is removed; the delay determines its position in the queue. */
body:not(.is-pre-load) .nav                  { transition-delay: 0.05s; }
body:not(.is-pre-load) .hero__eyebrow--left  { transition-delay: 0.20s; }
body:not(.is-pre-load) .hero__eyebrow--right { transition-delay: 0.25s; }
body:not(.is-pre-load) .hero__title-word     { transition-delay: 0.35s; }
body:not(.is-pre-load) .hero__title-italic   { transition-delay: 0.45s; }
body:not(.is-pre-load) .hero__status         { transition-delay: 0.55s; }
body:not(.is-pre-load) .hero__tagline        { transition-delay: 0.65s; }
body:not(.is-pre-load) .hero__card--next     { transition-delay: 0.75s; }
body:not(.is-pre-load) .hero__card--role     { transition-delay: 0.85s; }

@media (prefers-reduced-motion: reduce) {
  body:not(.is-pre-load) .nav,
  body:not(.is-pre-load) .hero__eyebrow,
  body:not(.is-pre-load) .hero__title-word,
  body:not(.is-pre-load) .hero__title-italic,
  body:not(.is-pre-load) .hero__tagline,
  body:not(.is-pre-load) .hero__status,
  body:not(.is-pre-load) .hero__card {
    transition-duration: 0.001s;
    transition-delay: 0s;
  }
}

/* =========================================================
   NAV
   ========================================================= */
.nav {
  /* Absolute (not fixed) so the whole top bar scrolls up and away with the
     page instead of staying glued to the viewport. */
  position: absolute; top: 0; left: 0; right: 0;
  z-index: 150;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: clamp(14px, 1.6vw, 24px) var(--gutter);
  pointer-events: none;
  font-family: var(--f-sans);
}
.nav > * { pointer-events: auto; }

.nav__brand {
  display: inline-flex; flex-direction: column;
  line-height: 0.88;
  font-family: var(--f-display);
  font-size: clamp(14px, 0.71vw, 18px);
  letter-spacing: 0.04em;
  color: var(--ink);
}
.nav__brand-line1, .nav__brand-line2 {
  display: block;
  font-weight: 400;
}
.nav__brand-line2 { font-weight: 400; }

.nav__monogram {
  justify-self: center;
  width: 56px; height: 32px;
  display: grid; place-items: center;
  color: var(--ink);
  transition: transform .4s var(--ease-out-quart);
}
.nav__monogram svg { width: 100%; height: 100%; }
.nav__monogram:hover { transform: translateY(-1px) scale(1.05); }

.nav__right {
  justify-self: end;
  display: flex; align-items: center; gap: 10px;
}

.nav__cta {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 9px 16px 9px 12px;
  background: var(--accent);
  color: var(--accent-ink);
  font-family: var(--f-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  border-radius: 999px;
  border: 1px solid rgba(0,0,0,0.08);
  transition: transform .35s var(--ease-out-quart), box-shadow .35s var(--ease-out-quart);
  box-shadow:
    0 2px 0 rgba(0,0,0,0.08),
    0 10px 30px -10px rgba(180, 221, 24, 0.6);
}
.nav__cta:hover {
  transform: translateY(-2px);
  box-shadow:
    0 4px 0 rgba(0,0,0,0.10),
    0 18px 40px -10px rgba(180, 221, 24, 0.85);
}
.nav__cta-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--ink);
  box-shadow: 0 0 0 3px rgba(12,12,10,0.06);
}

.nav__menu {
  width: 38px; height: 38px;
  border-radius: 10px;
  border: 1px solid var(--line-soft);
  background: var(--glass);
  backdrop-filter: blur(8px);
  display: grid; place-items: center;
  transition: background .3s var(--ease-out-quart);
}
.nav__menu svg { width: 18px; height: 18px; }
.nav__menu:hover { background: #fff; }

/* =========================================================
   HERO STAGE — outer container provides the scroll runway.
   The inner `__pin` is sticky for the full runway, so the
   character canvas stays fixed in the viewport while the UI
   layer is translated up by JS.
   ========================================================= */
.hero-stage {
  position: relative;
  height: 220vh;                  /* pin (100vh) + 120vh of runway. The
                                     reel auto-scrolls the page in sync
                                     with animFrame, so this runway is
                                     consumed exactly as the animation
                                     plays from HOLD_FRAME → TIMELINE_MAX.
                                     Skills section sits BEHIND the hero
                                     during the last 100vh of this range
                                     (see .warp margin-top), and the hero
                                     pin slides up off-screen naturally
                                     via sticky un-stick — revealing the
                                     skills layer underneath. */
  z-index: 3;                     /* foreground reveal layer — above
                                     .warp (z-1) and .hall (z-2). */
  /* 3D scene depth for the hero pin's lift-away tilt. The JS scrubs
     a small negative rotateX on the pin during the slide-up; this
     perspective makes that rotation read as a real panel receding
     in space rather than a flat skew. perspective-origin stays at
     the default (center) so the viewer feels straight-on. */
  perspective: 1500px;
}
.hero-stage__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  width: 100%;
  overflow: hidden;
  /* Self-contained foreground layer: cream BG belongs INSIDE the
     pin so the light hero background, the character canvas, and the
     hero UI all move as one piece during the slide-up reveal. Without
     this, the pin would be transparent and the warp section behind
     (z-index 1, sticky during the overlap) would show through the
     character canvas's transparent pixels — the bug where the dark
     "LANGUAGES" heading bled through the character. The cream here
     stays locked to the pin: when the pin un-sticks and slides up,
     the cream slides up with it, exposing the skills layer only
     where the pin no longer covers. */
  background: var(--cream);
  /* Pivot the JS-driven lift tilt at the TOP edge so the bottom of
     the panel is what swings back as the pin slides up. */
  transform-origin: 50% 0%;
  /* Soft ambient shadow under the panel — only visible once the pin
     starts sliding up (before that, the shadow sits below the
     viewport's bottom edge). Gives the lifting panel a clear
     separation from the skills layer behind. */
  box-shadow:
    0 28px 56px -14px rgba(0, 0, 0, 0.45),
    0 10px 22px -8px  rgba(0, 0, 0, 0.22);
}

/* Canvas — bottom-anchored. Aspect ratio matches a generous
   source crop (1152×1080 ≈ 16/15) that includes the entire
   character including swaying hair on every frame. The
   `height: min(100vh, …)` form means the canvas scales down
   by WIDTH on narrow viewports so the character is never cut
   off horizontally. `aspect-ratio` keeps the contain-fit
   filling the canvas edge-to-edge — no letterboxing, no
   cropping. */
.hero-stage__canvas {
  position: absolute;
  left: 50%;
  bottom: -1vh;
  bottom: -1svh;
  /* Static fallback transform — JS overrides this with a single
     composed `style.transform` string each animation frame so
     there's only one style mutation per frame (no calc/var
     re-evaluation, no triple setProperty). */
  transform: translate3d(-50%, 0, 0);
  height: min(100vh, calc(100vw * 15 / 16));
  height: min(100svh, calc(100vw * 15 / 16));
  width: auto;
  aspect-ratio: 16 / 15;
  max-width: 100vw;
  max-height: 100vh;
  max-height: 100svh;
  z-index: 2;
  display: block;
  /* Let pointer events fall through to the signature behind it so the
     name's hover (morphing-cursor) effect works — the character canvas
     itself isn't interactive. */
  pointer-events: none;
  /* Soft cast shadow so the figure sits in the scene — lifts off the
     cream and the wordmark behind it. drop-shadow blurs only the SHADOW
     copy, so the character pixels stay sharp. It re-rasterises on frame
     change, not every rAF (renderFrame skips unchanged frames; the
     per-frame transform is composite-only). Kept to ONE layer with a
     modest blur: this is the only shadow that re-rasterises while the
     crawl plays, so a single cheap pass (instead of two big-blur passes)
     is the biggest GPU saving on the page. Key light reads upper-left,
     so the shadow falls down-right. */
  filter: drop-shadow(7px 16px 34px rgba(21, 16, 10, 0.24));
}
/* Weak devices: a single, smaller-blur cast shadow — cheaper to
   re-rasterise as the crawl advances frames. */
body.perf-lite .hero-stage__canvas {
  filter: drop-shadow(8px 15px 26px rgba(19, 15, 9, 0.22));
}

/* Cloned topo contour pattern inside the pinned hero + gallery
   sections. Mirrors the body's fixed .topo-bg color + opacity so
   those opaque-cream pins paint the same faint paper contours the
   rest of the page has. z-index:0 sits above the pin's cream
   background but below the pins' content (hero halo z1 / canvas z2
   / ui z6; gallery canvas z1 / ui z2 / chrome z3). It scrolls with
   the pin, which is fine — the body topo drifts too. */
.pin-topo {
  position: absolute;
  inset: -12vh -12vw;
  z-index: 0;
  /* Very subtle — matches the body's fixed .topo-bg so the contour
     texture reads as the same faint paper across the whole page.
     initTopo() fills the SVG with a full-height field of waves so
     the pattern covers the entire pin edge-to-edge. */
  color: rgba(12, 12, 10, 0.22);
  opacity: 0.5;
  pointer-events: none;
}

/* Soft glow trailing behind the character. Anchored to the
   bottom to match the new character position and slightly
   above so it reads as ambient backlight on the upper body. */
.hero-stage__halo {
  position: absolute;
  left: 50%;
  bottom: 5vh;
  transform: translateX(-50%);
  width: 90vh;
  height: 90vh;
  max-width: 96vw;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    rgba(216, 255, 56, 0.16) 0%,
    rgba(216, 255, 56, 0.05) 50%,
    transparent 78%);
  z-index: 1;
  filter: blur(28px);
  animation: haloPulse 9s ease-in-out infinite;
  pointer-events: none;
}
/* Pause the (blurred, animated) halo while the hero is off-screen — no
   point re-rasterising a 28px blur on a scaling element you can't see.
   Identical on-screen; mirrors the welcomeBreath pause. */
body.hero-hidden .hero-stage__halo { animation-play-state: paused; }

/* =========================================================
   "GOOD PART" HEADLINE — big page-filling statement behind
   the character. z-index 1 keeps it BEHIND the canvas (z2)
   so the figure stands in front of the words. Each word is
   an overflow-clipped box; initGoodPart() injects a reveal
   .goodpart__block per word and wipes it bottom-to-up.
   ========================================================= */
.goodpart {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.02em;
  text-align: center;
  /* Shifted a touch above dead-center so the statement sits high on
     the frame, over the character's head/shoulders. */
  transform: translateY(-6vh);
  font-family: var(--f-display);   /* Anton — the site's heavy condensed display */
  font-weight: 400;                /* Anton is already black at 400 */
  font-size: clamp(36px, 8.7vw, 210px);
  line-height: 0.92;
  letter-spacing: -0.025em;        /* tighter tracking reads bigger + heavier */
  text-transform: uppercase;
  color: var(--ink);
  pointer-events: none;
  user-select: none;
}
/* Three-track grid: equal 1fr side columns flank a fixed center column,
   so the gap is locked to dead-center (= the character) regardless of how
   wide each word group is. A group wider than its 1fr track simply bleeds
   off its outer screen edge (clipped by the pin), never behind the figure. */
.goodpart__line {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: baseline;
  white-space: nowrap;
}
.goodpart__side {
  min-width: 0;            /* let the track stay 1fr; content overflows outward */
  white-space: nowrap;
}
.goodpart__side--l { justify-self: end; text-align: right; }
.goodpart__side--r { justify-self: start; text-align: left; }
/* Premium graduated cast shadow on the giant statement — applied here (not on the
   glyph) because .goodpart__word is overflow:hidden for the reveal wipe and would clip
   a text-shadow. Three stacked drop-shadows on the un-clipped side: crisp contact + soft
   mid + wide ambient, warm-to-cool tint, em-scaled to match the rest of the copy. The
   headline is static once revealed, so the layer rasterises once — no per-frame cost. */
.goodpart__side {
  filter:
    drop-shadow(0 0.01em 0.01em rgba(28, 21, 11, 0.56))
    drop-shadow(0 0.045em 0.06em rgba(24, 18, 10, 0.4))
    drop-shadow(0 0.14em 0.24em rgba(20, 16, 10, 0.24));
}
body.perf-lite .goodpart__side {
  filter: drop-shadow(0 0.055em 0.07em rgba(26, 20, 10, 0.52));
}
/* Nudge STUFF a little toward GOOD (leftward). */
.goodpart__word--stuff { margin-left: -0.1em; }
/* Shift the whole GOOD STUFF group a bit toward the left as a unit. */
.goodpart__side--good { transform: translateX(-0.28em); }
/* Center safe-zone (the middle grid track) sized to the character's
   head/face width at the headline's height. */
.goodpart__gap {
  width: clamp(140px, 19.5vw, 380px);
}
.goodpart__word {
  position: relative;
  display: inline-block;
  overflow: hidden;                /* clips the rising reveal block */
  vertical-align: top;
  padding: 0.06em 0.05em 0.04em;   /* room so the glyphs never clip */
}
.goodpart__text {
  display: inline-block;
  will-change: opacity;
  /* Extra stroke thickens Anton's already-heavy strokes so the words
     read even bolder at this size. */
  -webkit-text-stroke: 0.012em var(--ink);
}
/* "GOOD" stays black like the rest, but sits on a neon-accent highlight
   (a marker swash behind the word). The highlight lives on the text span
   so it fades in with the word — never showing before the reveal. */
.goodpart__word--good .goodpart__text {
  color: var(--ink);
  -webkit-text-fill-color: var(--ink);
  /* Neon accent as a stroked outline box around the word (transparent fill,
     sharp 90° corners) rather than a solid highlight. */
  background: transparent;
  border: 0.055em solid var(--accent);
  border-radius: 0;
  padding: 0.04em 0.12em;
}
/* Injected per word: a glowing NEON block that wipes the word in,
   bottom-to-up (scaleY 0→1 from the bottom, then 1→0 off the top).
   The electric-chartreuse accent + bloom give the reveal its neon flash. */
.goodpart__block {
  position: absolute;
  left: 0; top: 0;
  width: 100%; height: 100%;
  z-index: 2;
  background: var(--accent);
  box-shadow: 0 0 2.2vw rgba(216, 255, 56, 0.65),
              0 0 0.6vw rgba(216, 255, 56, 0.9);
  transform: scaleY(0);
  transform-origin: center bottom;
  pointer-events: none;
}

/* Signature wordmark behind the character. Cursive, tilted like a
   real autograph, z-index 1 so it sits BEHIND the character canvas
   (z2) — the figure overlaps the middle of the name. It "writes on"
   left-to-right (a clip-path sweep along the slanted baseline) once
   the intro loader removes .is-pre-load — the vanilla equivalent of
   the text-effect component's stroke draw-on. */
.hero-sign {
  position: absolute;
  z-index: 1;
  left: 50%;
  top: 48%;
  transform: translate(-50%, -50%) rotate(-8deg);
  transform-origin: center;
  margin: 0;
  font-family: var(--f-sign);
  font-weight: 700;
  /* Sized so the (~5.5em-wide, nowrap) name holds the SAME ~77%-of-viewport
     proportion it has at 2560 across every width: at 2560 the 360px cap ≈ 14vw
     so the 2K look is unchanged, but below ~1900px it now scales down with the
     viewport instead of staying capped and overflowing both edges. */
  font-size: clamp(44px, 14vw, 360px);
  line-height: 1;
  white-space: nowrap;
  letter-spacing: 0.01em;
  color: rgba(12, 12, 10, 0.66);
  /* Hoverable so the morphing-cursor reveal fires; the cursor hides
     over the name and the ink lens becomes the pointer. */
  pointer-events: auto;
  cursor: none;
  user-select: none;
  opacity: 1;   /* motionTick owns the scroll-exit fade */
}
/* Base name layer — carries the write-on clip (JS-driven). Top/bottom/
   left are pushed out negative so the clip only wipes horizontally and
   never cuts ascenders/descenders (the 'y' tail in "Aditya"). */
.hero-sign__base {
  display: inline-block;
  clip-path: inset(-30% 100% -45% -3%);
}
/* Handwriting draw-on: the name as stroked Dancing Script (700) glyph
   paths that self-draw (stroke-dashoffset, staggered per letter, set in
   initHeroSign) then ink in. Sized in em off the 1000-unit viewBox so it
   tracks the h1 font-size; the parent h1 supplies the tilt + centering.
   The base span stays clipped (invisible) underneath purely so the box
   geometry the lens + scroll math rely on is unchanged. */
.hero-sign__draw {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, calc(-50% + var(--draw-dy, -0.02em)));
  z-index: 0;                /* above the (invisible) base, below the lens */
  overflow: visible;
  opacity: 0;                /* JS sets the dash offsets, then flips to 1 */
  pointer-events: none;      /* hover stays on .hero-sign (the h1) */
  /* Premium graduated lift — three warm, em-scaled layers: a crisp dark
     CONTACT edge, a soft MID, and a wide diffuse AMBIENT halo, with a
     subtle warm-to-cool tint shift (28,21,11 → 20,16,10) for depth that
     reads hand-crafted rather than flat. em units track the responsive
     font-size. Static (no animation) so it rasterises once and the extra
     layer costs nothing per frame; the scroll-fade is opacity-only. */
  filter:
    drop-shadow(0 0.012em 0.012em rgba(28, 21, 11, 0.46))
    drop-shadow(0 0.055em 0.075em rgba(24, 18, 10, 0.32))
    drop-shadow(0 0.16em 0.3em rgba(20, 16, 10, 0.2));
}
.hero-sign__stroke {
  fill: rgba(12, 12, 10, 0.66);    /* same ink as the name */
  fill-opacity: 0;                 /* outline only until the stroke is drawn */
  stroke: rgba(12, 12, 10, 0.66);
  stroke-width: 11;                /* pen width in viewBox units (1000 = 1em) */
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: fill-opacity 0.55s ease, stroke-width 0.55s ease;
}
/* Weak devices: one soft layer instead of three so the opacity scroll-fade
   stays light (the name lives in the perf-sensitive hero). */
body.perf-lite .hero-sign__draw {
  filter: drop-shadow(0 0.05em 0.07em rgba(26, 20, 10, 0.44));
}
/* Spotlight lens: a solid ink circle that follows the cursor; inside,
   "software engineer" shows in cream, counter-translated so it stays
   fixed and is revealed through the moving window. */
.hero-sign__lens {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  border-radius: 50%;
  background: var(--ink);
  overflow: hidden;
  pointer-events: none;
  transition:
    width  0.5s cubic-bezier(0.33, 1, 0.68, 1),
    height 0.5s cubic-bezier(0.33, 1, 0.68, 1);
  will-change: transform, width, height;
}
.hero-sign__lens-inner {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  justify-content: flex-start;   /* text starts at the inner's left edge */
  /* Inherit the name's cursive font, size, weight + spacing so the
     reveal matches "Aditya Verma" exactly; only the colour flips to
     cream so it reads on the ink lens. */
  color: var(--cream);
  white-space: nowrap;
  pointer-events: none;
  will-change: transform;
}
/* Starfield inside the dark lens — small cream dots scattered across
   the fixed layer, revealed through the moving spotlight. The dots are
   generated by initHeroSign(); each gently twinkles. */
.hero-sign__lens-stars {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 120vw;
  height: 110vh;
  transform: translate(-50%, -50%);
  display: block;
  pointer-events: none;
}
.hero-sign__lens-text {
  position: relative;   /* paints above the stars */
  z-index: 1;
  flex-shrink: 0;       /* keep its natural width so we can measure it */
  /* The lens text is line-box-centred in the name's box, but the SVG name
     sits ink-centred (higher), so the reveal landed ~0.12em low. Raise it
     to meet the name. (JS only sets transform, so top is free to use.) */
  top: var(--se-dy, -0.12em);
}
@media (prefers-reduced-motion: reduce) {
  .hero-sign { cursor: auto; }
  /* show the static name; sides widened so the shadow blur isn't clipped */
  .hero-sign__base {
    clip-path: inset(-30% -16% -45% -16%);
    text-shadow:
      0 0.018em 0.016em rgba(26, 20, 10, 0.34),
      0 0.06em 0.075em rgba(26, 20, 10, 0.26),
      0 0.13em 0.22em rgba(22, 17, 9, 0.18);
  }
  .hero-sign__draw { display: none; }                        /* no draw-on */
  .hero-sign__lens { display: none; }
}

/* UI layer — translated upward by JS as scroll progresses */
.hero-stage__ui {
  position: absolute;
  inset: 0;
  z-index: 6;
  padding: clamp(110px, 13vh, 180px) var(--gutter) clamp(30px, 4vh, 60px);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(20px, 3vh, 40px);
  pointer-events: none;
  will-change: transform, opacity;
}
.hero-stage__ui > * { pointer-events: auto; }

/* Cinema HUD/captions — fade in once UI is gone */
.hero-stage__cinema {
  position: absolute;
  inset: 0;
  z-index: 5;
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
}

/* Legacy `.hero` kept neutral — children below still use these
   class names but their layout context is now `.hero-stage__ui`. */
.hero {
  position: relative;
}

/* Subtle frame */
.hero__frame {
  position: absolute;
  inset: clamp(82px, 9vh, 110px) var(--gutter) clamp(20px, 3vh, 40px);
  pointer-events: none;
  border: 1px solid var(--line-soft);
  border-radius: 18px;
  z-index: 0;
}
.corner {
  position: absolute;
  width: 14px; height: 14px;
  border: 1.5px solid var(--ink);
}
.corner--tl { top: -1px; left: -1px; border-right: none; border-bottom: none; }
.corner--tr { top: -1px; right: -1px; border-left: none; border-bottom: none; }
.corner--bl { bottom: -1px; left: -1px; border-right: none; border-top: none; }
.corner--br { bottom: -1px; right: -1px; border-left: none; border-top: none; }

/* eyebrow labels */
.hero__eyebrow {
  position: absolute;
  top: clamp(96px, 11vh, 130px);
  display: inline-flex; align-items: center; gap: 12px;
  font-family: var(--f-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.18em;
  color: var(--ink-mute);
  z-index: 5;
}
.hero__eyebrow--left { left: calc(var(--gutter) + 18px); }
.hero__eyebrow--right { right: calc(var(--gutter) + 18px); }
.hero__eyebrow-num { color: var(--ink); font-weight: 500; }
.hero__eyebrow-divider {
  display: inline-block; width: 36px; height: 1px;
  background: var(--ink-mute); opacity: .55;
}

/* Welcome line, split across the bottom corners of the frame. Same
   cursive as the name (--f-sign) but smaller and a lighter weight. */
.hero__welcome {
  position: absolute;
  bottom: clamp(30px, 4.5vh, 52px);
  z-index: 5;
  font-family: var(--f-sign);
  font-weight: 500;
  font-size: clamp(26px, 2.15vw, 54px);
  line-height: 1;
  letter-spacing: 0.01em;
  color: rgba(86, 60, 40, 0.82);   /* warm brownish tone (vs the name's ink) */
  white-space: nowrap;
  /* Hidden until the write-on sweeps it (same clip as the name); the
     sweep is JS-driven (initHeroSign) and starts after the name. */
  clip-path: inset(-30% 100% -45% -3%);
}
.hero__welcome--left  { left: calc(var(--gutter) + 18px); }
.hero__welcome--right { right: calc(var(--gutter) + 18px); }
/* Breathing weight (animated-text port): a SINGLE rendering of each
   line whose strokes fatten and thin via a same-colour horizontal
   smear (faux-bold) animating 0 -> ~0.6px and back. Paint-only — no
   reflow, no per-frame glyph re-raster — and, being one layer rather
   than a stacked bold copy, it cannot produce the double-image
   ghosting. The two corners run slightly out of phase for a gentle
   wave. Paused while the hero is off screen. */
@keyframes welcomeBreath {
  0%   { text-shadow: 0 0 0 transparent; }
  100% { text-shadow: 0.6px 0 0 currentColor, -0.6px 0 0 currentColor; }
}
.hero__welcome--left,
.hero__welcome--right {
  animation: welcomeBreath 2.4s alternate ease-in-out infinite both;
}
.hero__welcome--right { animation-delay: -0.6s; }   /* phase offset -> wave */
body.hero-hidden .hero__welcome--left,
body.hero-hidden .hero__welcome--right { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .hero__welcome--left,
  .hero__welcome--right { animation: none; }
}
@media (max-width: 991px) {
  .hero__welcome { display: none; }
}

/* Massive title */
.hero__title {
  position: relative;
  margin: clamp(30px, 6vh, 60px) 0 0;
  font-family: var(--f-display);
  font-weight: 400;
  color: var(--ink);
  letter-spacing: -0.02em;
  line-height: 0.82;
  z-index: 6;
  pointer-events: none;
}
.hero__title-row {
  display: flex;
  align-items: flex-end;
  gap: clamp(10px, 1vw, 18px);
}
.hero__title-row--top { justify-content: flex-start; }
.hero__title-row--bottom { justify-content: space-between; flex-wrap: wrap; }
.hero__title-word {
  font-size: clamp(64px, 8.6vw, 220px);
  line-height: 0.78;
  display: inline-block;
  position: relative;
  letter-spacing: -0.015em;
}
.hero__title-italic {
  font-family: var(--f-serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(28px, 3.15vw, 80px);
  color: var(--ink-mute);
  line-height: 0.95;
  padding-bottom: clamp(8px, 1.5vh, 22px);
  letter-spacing: -0.005em;
}

/* Halo pulse keyframes — keeps `translateX(-50%)` so the halo
   stays horizontally centered while the scale/opacity pulse. */
@keyframes haloPulse {
  0%, 100% { opacity: .6; transform: translateX(-50%) scale(1); }
  50%      { opacity: 1;   transform: translateX(-50%) scale(1.05); }
}

.hero__loading {
  position: absolute;
  left: 50%; bottom: 6%;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  font-family: var(--f-mono);
  font-size: 10px; letter-spacing: 0.2em;
  color: var(--ink-mute);
  opacity: 1;
  transition: opacity .8s ease;
  pointer-events: none;
}
.hero__loading.is-done { opacity: 0; }
.hero__loading-bar {
  display: block;
  width: 140px; height: 2px;
  background: rgba(12,12,10,0.08);
  overflow: hidden;
}
.hero__loading-bar i {
  display: block; height: 100%; width: 0%;
  background: var(--accent-deep);
  transition: width .25s linear;
}

/* Positioning tagline */
.hero__tagline {
  position: absolute;
  z-index: 6;
  left: calc(var(--gutter) + 18px);
  bottom: clamp(170px, 22vh, 240px);
  margin: 0;
  max-width: 380px;
  display: flex; align-items: flex-start; gap: 14px;
  font-family: var(--f-sans);
  font-size: clamp(10px, 0.55vw, 14px);
  line-height: 1.45;
  color: var(--ink);
  letter-spacing: -0.005em;
  padding: 16px 18px;
  background: rgba(255, 253, 247, 0.92);
  /* No backdrop-filter — this card sits inside .hero-stage__ui
     which animates `transform` every scroll frame. Blurring the
     backdrop on a translating ancestor forces the compositor to
     re-blur the underlying pixels per frame, which is the most
     expensive thing we can do here. Use a higher-opacity solid
     background instead. */
  border: 1px solid var(--glass-stroke);
  border-radius: 14px;
  box-shadow: 0 12px 30px -20px rgba(12,12,10,0.3);
}
.hero__tagline em {
  font-family: var(--f-serif);
  font-style: italic;
  font-weight: 400;
  color: var(--ink);
}
.hero__tagline-glyph {
  flex-shrink: 0;
  width: 24px; height: 24px;
  display: grid; place-items: center;
  border-radius: 6px;
  background: var(--accent);
  color: var(--accent-ink);
}
.hero__tagline-glyph svg { width: 14px; height: 8px; }

/* status pill */
.hero__status {
  position: absolute;
  right: calc(var(--gutter) + 18px);
  top: clamp(180px, 22vh, 280px);
  display: inline-flex; align-items: center; gap: 10px;
  padding: 8px 14px;
  background: rgba(255, 253, 247, 0.86);
  /* no backdrop-filter — see .hero__tagline comment */
  border: 1px solid var(--glass-stroke);
  border-radius: 999px;
  font-family: var(--f-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--ink);
  z-index: 6;
  box-shadow: 0 10px 30px -16px rgba(12,12,10,0.25);
}
.hero__status-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--hot);
  box-shadow: 0 0 0 4px rgba(255,90,31,0.18);
  animation: pulseDot 1.6s ease-in-out infinite;
}
@keyframes pulseDot {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.25); opacity: .85; }
}
.hero__status-text b { font-weight: 500; }

/* bottom info row */
.hero__bottom {
  position: relative;
  z-index: 6;
  display: flex;
  justify-content: center;
  align-items: end;
  margin-top: auto;
}

.hero__card {
  background: rgba(255, 253, 247, 0.88);
  /* no backdrop-filter — see .hero__tagline comment */
  border: 1px solid var(--glass-stroke);
  border-radius: 14px;
  padding: 14px 18px;
  box-shadow:
    0 1px 0 rgba(255,255,255,0.6) inset,
    0 20px 40px -28px rgba(12,12,10,0.35);
  position: relative;
}
.hero__card--next { justify-self: start; max-width: 280px; }
.hero__card--role { justify-self: end; max-width: 320px; }

.hero__card header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 8px;
}
.hero__card-label {
  font-family: var(--f-mono);
  font-size: 10px; letter-spacing: 0.22em;
  color: var(--ink-mute);
}
.hero__card-corner {
  width: 12px; height: 12px;
  border: 1px solid var(--ink-mute);
  border-left: none; border-bottom: none;
}
.hero__card-body { display: flex; align-items: center; gap: 12px; }
.hero__card-glyph {
  width: 38px; height: 38px;
  color: var(--ink);
  flex-shrink: 0;
}
.hero__card-title {
  font-family: var(--f-display);
  font-size: clamp(11px, 0.71vw, 18px);
  letter-spacing: 0.02em;
  line-height: 1;
}
.hero__card-sub {
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  color: var(--ink-mute);
  margin-top: 4px;
}
.hero__card-role {
  display: flex; flex-direction: column;
  font-family: var(--f-display);
  font-size: clamp(22px, 1.2vw, 30px);
  line-height: 0.95;
  letter-spacing: 0.01em;
  text-align: right;
}
.hero__card-role-accent { color: var(--accent-deep); }
.hero__card-role-sub {
  font-family: var(--f-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.14em;
  color: var(--ink-mute);
  margin-top: 8px;
}

.hero__scroll {
  justify-self: center;
  display: inline-flex; flex-direction: column; align-items: center; gap: 8px;
  font-family: var(--f-mono);
  font-size: 10px; letter-spacing: 0.22em;
  color: var(--ink-mute);
  padding: 8px;
}
.hero__scroll svg { width: 14px; height: 22px; animation: scrollBob 1.8s ease-in-out infinite; }
@keyframes scrollBob {
  0%, 100% { transform: translateY(0); opacity: .8; }
  50%      { transform: translateY(4px); opacity: 1; }
}

/* =========================================================
   CINEMA HUD / CAPTIONS — overlay layer inside the pinned
   hero stage. Soft cream vignette gives caption readability
   over the character.
   ========================================================= */
.hero-stage__cinema::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(242,238,229,0.30) 0%, transparent 14%, transparent 66%, rgba(242,238,229,0.70) 100%),
    radial-gradient(120% 80% at 50% 40%, transparent 38%, rgba(242,238,229,0.32) 100%);
  pointer-events: none;
}

.cinema__hud {
  position: absolute;
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  color: var(--ink-mute);
  display: flex; flex-direction: column; gap: 4px;
  padding: 10px 14px;
  border: 1px solid var(--line-soft);
  background: var(--glass);
  backdrop-filter: blur(10px);
  border-radius: 10px;
  transition: opacity .45s ease, visibility .45s ease;
}
/* Hide the FRAME count + PROGRESS bar on the Instagram-style IG-aside page. */
.hero-stage__cinema:has(.cinema__aside--igpost.is-revealed) .cinema__hud--tl,
.hero-stage__cinema:has(.cinema__aside--igpost.is-revealed) .cinema__hud--bl {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.cinema__hud--tl { top: 28px; left: var(--gutter); }
.cinema__hud--tr { top: 28px; right: var(--gutter); text-align: right; align-items: flex-end; }
.cinema__hud--bl { bottom: 28px; left: var(--gutter); min-width: 240px; }
.cinema__hud--br { bottom: 28px; right: var(--gutter); text-align: right; align-items: flex-end; }
.cinema__hud-label { font-size: 9px; color: var(--ink-mute); opacity: 0.7; }
.cinema__hud-num { color: var(--ink); font-weight: 500; }
.cinema__hud-bar {
  display: block; width: 100%; height: 2px;
  background: rgba(12,12,10,0.10);
  margin-top: 4px;
}
.cinema__hud-bar i {
  display: block; height: 100%; width: 0%;
  background: linear-gradient(90deg, var(--ink), var(--accent-deep));
  transition: width 60ms linear;
}

.cinema__crosshair span {
  position: absolute;
  background: rgba(12, 12, 10, 0.18);
}
.cinema__crosshair span:nth-child(1) { top: 50%; left: 8%; width: 28px; height: 1px; }
.cinema__crosshair span:nth-child(2) { top: 50%; right: 8%; width: 28px; height: 1px; }
.cinema__crosshair span:nth-child(3) { left: 50%; top: 16%; width: 1px; height: 28px; }
.cinema__crosshair span:nth-child(4) { left: 50%; bottom: 16%; width: 1px; height: 28px; }

/* Cinema captions removed — the markup was deleted earlier, so these
   rules (incl. a backdrop-filter: blur(6px)) were dead weight. */

/* Side statement — vanilla port of blur-text-animation. Each word
   blur-reveals (blur + translateY + scale + rotateX, staggered) when
   its side becomes active; sits in the half opposite the character so
   it never collides with the figure. Reveal toggled from motionTick. */
.cinema__aside {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  /* Square editorial block — width tuned against the font so the
     statement wraps to a roughly 1:1 shape; centred lines give the
     symmetrical, rectangular look. */
  width: min(37vw, 525px);
  margin: 0;
  text-align: center;
  /* Same design language as the middle captions: Anton display +
     italic-serif emphasis on a lime highlight (.cinema__aside-w--em). */
  font-family: var(--f-display);
  font-weight: 400;
  /* Big, but sized to still fit ~half the screen; text-wrap: balance
     evens the line lengths so the block reads as a clean shape rather
     than a ragged stack with orphan words. */
  font-size: clamp(30px, 3.3vw, 84px);
  line-height: 1.2;        /* tuned with the width so the 6-line block is ~square */
  letter-spacing: -0.012em;
  text-wrap: balance;
  color: var(--ink);
  pointer-events: none;
  perspective: 1000px;           /* gives the per-word rotateX depth */
}
/* Each block centred within its half, pulled a bit off the middle
   (mirror-symmetric gutters), clear of the character opposite. */
.cinema__aside--right { right: clamp(50px, 10vw, 200px); left:  auto; }
.cinema__aside--left  { left:  clamp(50px, 10vw, 200px); right: auto; }
.cinema__aside-w {
  display: inline-block;
  margin-right: 0.3em;
  /* DISAPPEAR state: words blur back out (prompt + uniform, so the
     statement clears cleanly when the character leaves the side).
     NOTE: no permanent will-change / preserve-3d here — those kept a
     GPU filter-backing store alive for every word for the whole page
     (heavy + site-wide). The blur is rasterised per frame only while a
     transition is actually running, so the effect is unchanged. */
  opacity: 0;
  filter: blur(var(--blur, 12px)) brightness(0.6);
  transform: translateY(0.2em) scale(var(--scale, 0.95)) rotateX(-15deg);
  transition: opacity 0.5s, filter 0.5s, transform 0.5s;
  transition-delay: 0s;
  transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.cinema__aside.is-revealed .cinema__aside-w {
  /* APPEAR state: words blur in, staggered per-word (the blur-text
     effect's computed duration/delay come in via initBlurAside). */
  opacity: 1;
  filter: blur(0px) brightness(1);
  transform: translateY(0) scale(1) rotateX(0deg);
  transition-duration: var(--dur, 1s);
  transition-delay: var(--delay, 0s);
}
/* Emphasised words — same italic-serif + lime highlight as the
   middle captions' <em> (see .cinema__caption em). */
.cinema__aside-w--em {
  font-family: var(--f-serif);
  font-style: italic;
  background: linear-gradient(transparent 68%, var(--accent) 68%, var(--accent) 92%, transparent 92%);
  padding: 0 0.06em;
}
@media (prefers-reduced-motion: reduce) {
  .cinema__aside-w { transition-duration: 0.001s; transition-delay: 0s; }
}
@media (max-width: 768px) {
  .cinema__aside { display: none; }   /* no room beside the figure */
}

/* ---------- LEFT aside as a live terminal ----------
   Vanilla port of a shadcn/ui "Terminal" demo, re-skinned to the site:
   the command + prompt user take the chartreuse accent so the dark panel
   still reads as part of the cream hero. The container keeps .cinema__aside
   for positioning, but drops the giant display type. The reveal (blur-in)
   is moved off the per-word spans onto the .term frame itself. */
.cinema__aside--term {
  /* Big enough to own most of the empty band, and re-centred in the gutter
     between the left edge and the character (~29vw) rather than hugging the
     edge. translate(-50%,-50%) centres the panel on that point. */
  width: clamp(380px, 38.3vw, 980px);
  left: 29vw;
  right: auto;
  transform: translate(-50%, -50%);
  font-family: var(--f-mono);
  font-weight: 400;
  font-size: clamp(13px, 0.67vw, 17px);
  line-height: 1.75;
  letter-spacing: 0;
  text-align: left;
  text-wrap: nowrap;
  color: #e8e8e8;
  perspective: none;
}
/* NB: classes are namespaced `hterm*` (not `term*`) on purpose — the project
   already ships a dormant `.term` component lower in this file, and reusing
   those names let its `max-width:560px` etc. leak in. */
.hterm {
  position: relative;
  isolation: isolate;                              /* own stacking ctx for the glow/scanlines */
  width: 100%;                                     /* fill the container, don't shrink to text */
  overflow: hidden;
  border-radius: 16px;
  /* Frosted glass — translucent so the cream paper + aurora bleed through and
     the panel reads as part of the scene rather than a pasted-on box. */
  border: 1px solid rgba(255, 255, 255, 0.10);
  background: rgba(17, 18, 20, 0.52);
  -webkit-backdrop-filter: blur(18px) saturate(1.2);
  backdrop-filter: blur(18px) saturate(1.2);
  box-shadow:
    0 44px 90px -30px rgba(0, 0, 0, 0.55),         /* deep drop */
    0 14px 34px -18px rgba(0, 0, 0, 0.45),
    0 0 70px -22px rgba(216, 255, 56, 0.18),       /* faint chartreuse halo */
    inset 0 1px 0 rgba(255, 255, 255, 0.10);       /* top-edge highlight */
  /* DISAPPEAR state — blurred + dimmed, mirrors the word-aside reveal. */
  opacity: 0;
  filter: blur(16px) brightness(0.6);
  transform: translateY(0.5em) scale(0.96);
  transition: opacity .7s, filter .7s,
              transform .7s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.cinema__aside--term.is-revealed .hterm {
  opacity: 1;
  filter: blur(0) brightness(1);
  transform: none;
}
/* Corner accent glow — sits BEHIND the content (z-index 0). */
.hterm::before {
  content: "";
  position: absolute;
  z-index: 0;
  top: -25%;
  right: -12%;
  width: 58%;
  height: 70%;
  pointer-events: none;
  background: radial-gradient(closest-side, rgba(216, 255, 56, 0.12), transparent 75%);
  filter: blur(12px);
}
/* Hairline scanlines — sits ABOVE the content (z-index 2), very subtle. */
.hterm::after {
  content: "";
  position: absolute;
  z-index: 2;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.022) 0 1px,
    transparent 1px 3px
  );
  mix-blend-mode: overlay;
  opacity: 0.6;
}
.hterm__bar,
.hterm__body { position: relative; z-index: 1; }   /* above the glow */
.hterm__bar {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 13px 16px;
  background: rgba(40, 41, 46, 0.45);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.hterm__dot {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  flex: none;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.18);
}
.hterm__dot--r { background: #ef4444; }
.hterm__dot--y { background: #eab308; }
.hterm__dot--g { background: #22c55e; }
.hterm__title {
  flex: 1;
  text-align: center;
  font-size: clamp(11px, 0.51vw, 13px);
  color: #b3b3b3;
  letter-spacing: 0.02em;
}
.hterm__spacer { width: 52px; flex: none; }
.hterm__body {
  height: clamp(340px, 50vh, 560px);
  padding: 24px 28px;
  overflow-y: auto;
  scrollbar-width: none;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);      /* keeps text crisp over the glass */
}
.hterm__body::-webkit-scrollbar { width: 0; height: 0; }
.hterm__line, .hterm__out { white-space: pre-wrap; word-break: break-word; }
.hterm__out { color: #bdbdbd; }
/* prompt */
.hterm__p-user   { color: var(--accent); }
.hterm__p-host   { color: #8a93a0; }
.hterm__p-sep    { color: #16a34a; }
.hterm__p-tilde  { color: #38bdf8; }
.hterm__p-dollar { color: #8a8a8a; }
/* bash syntax (command + path/flag/string/etc.) */
.hterm__t-command  { color: var(--accent); }
.hterm__t-flag     { color: #56c0f5; }
.hterm__t-string   { color: #fcd34d; }
.hterm__t-number   { color: #c084fc; }
.hterm__t-operator { color: #f87171; }
.hterm__t-path     { color: #7adfee; }
.hterm__t-variable { color: #f472b6; }
.hterm__t-comment  { color: #8a8a8a; }
.hterm__t-default  { color: #e8e8e8; }
/* caret */
.hterm__cursor {
  display: inline-block;
  width: 9px;
  height: 1.05em;
  margin-left: 3px;
  background: var(--accent);
  box-shadow: 0 0 8px rgba(216, 255, 56, 0.6);     /* glowing caret */
  vertical-align: text-bottom;
}
.hterm__cursor--blink { animation: htermBlink 1.06s steps(1) infinite; }
@keyframes htermBlink { 50% { opacity: 0; } }
/* Weak devices: drop the (expensive) backdrop blur, fall back to a more
   opaque solid so it stays cheap. See perf-lite gating elsewhere. */
body.perf-lite .hterm {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  background: rgba(17, 18, 20, 0.86);
}

/* ---------- RIGHT aside as a 3D testimonial marquee ----------
   Vanilla port of a "3D testimonials" component: columns of cards scroll
   vertically (two copies per track, translateY(-50%) loops seamlessly) in
   alternating directions, tilted into 3D, filling the right half opposite the
   character. Non-interactive. The blur-reveal sits on .testi3d. */
.cinema__aside--testi {
  top: 0;
  bottom: 0;
  right: 0;
  left: auto;
  width: 50vw;
  height: 100vh;
  max-width: none;
  transform: none;
  perspective: none;
  pointer-events: none;
}
.testi3d {
  position: absolute;
  inset: 0;
  overflow: hidden;
  perspective: 1200px;
  opacity: 0;
  filter: blur(16px) brightness(0.85);
  transition: opacity .8s, filter .8s;
}
.cinema__aside--testi.is-revealed .testi3d {
  opacity: 1;
  filter: blur(0) brightness(1);
  /* purely decorative - let the mouse pass through (no hover interaction) */
}
.testi3d__scene {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  flex-direction: row;
  gap: clamp(12px, 1vw, 20px);
  transform-style: preserve-3d;
  transform: translate(-50%, -50%) rotateX(16deg) rotateY(-16deg) rotateZ(14deg) scale(1.18);
}
.tcol {
  flex: none;
  width: clamp(150px, 11vw, 208px);
  height: 150vh;
  overflow: hidden;
}
.tcol__track {
  display: flex;
  flex-direction: column;
  will-change: transform;
}
.tcol__track--up   { animation: tmarquee 34s linear infinite; }
.tcol__track--down { animation: tmarquee 34s linear infinite reverse; }
@keyframes tmarquee { from { transform: translateY(0); } to { transform: translateY(-50%); } }

.tcard {
  margin: 0 0 clamp(12px, 1vw, 18px);
  padding: clamp(13px, 1vw, 18px);
  background: rgba(255, 255, 255, 0.72);
  border: 1px solid rgba(12, 12, 10, 0.08);
  border-radius: 14px;
  box-shadow: 0 16px 34px -22px rgba(0, 0, 0, 0.30);
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
}
.tcard__head { display: flex; align-items: center; gap: 10px; }
.tcard__avatar {
  flex: none;
  width: 38px; height: 38px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--f-sans);
  font-weight: 700;
  font-size: clamp(10px, 0.63vw, 16px);
  color: #1b1b17;
}
.tcard__id { display: flex; flex-direction: column; min-width: 0; }
.tcard__name {
  font-family: var(--f-sans);
  font-weight: 600;
  font-size: clamp(10px, 0.51vw, 13px);
  color: var(--ink);
  display: flex; align-items: center; gap: 5px;
  white-space: nowrap;
}
.tcard__flag { font-size: clamp(10px, 0.47vw, 12px); }
.tcard__handle { font-family: var(--f-mono); font-size: 11px; color: var(--ink-mute); }
.tcard__body {
  margin: 11px 0 0;
  font-family: var(--f-sans);
  font-size: clamp(10px, 0.51vw, 13px);
  line-height: 1.45;
  color: var(--ink-2);
}
.testi3d__fade { position: absolute; pointer-events: none; z-index: 4; }
.testi3d__fade--t { top: 0;    left: 0; right: 0; height: 24%; background: linear-gradient(to bottom, var(--cream), transparent); }
.testi3d__fade--b { bottom: 0; left: 0; right: 0; height: 24%; background: linear-gradient(to top,    var(--cream), transparent); }
.testi3d__fade--r { top: 0; bottom: 0; right: 0; width: 16%;  background: linear-gradient(to left,    var(--cream), transparent); }
@media (prefers-reduced-motion: reduce) {
  .tcol__track { animation: none; }
}
body.perf-lite .tcol__track { animation: none; }
body.perf-lite .tcard { -webkit-backdrop-filter: none; backdrop-filter: none; background: rgba(255,255,255,0.85); }


/* ---------- LEFT element: Instagram post (image carousel) ----------
   Left-edge. The IMAGE keeps its size (media = container width, unchanged); the
   surrounding UI (avatar, handle, location, action icons, counts) is large and
   airy for a minimal feel. Transparent header/footer; slide transition between
   images; the heart is an animated like toggle. */
.cinema__aside--igpost {
  left: calc(var(--gutter) + 15px); /* post aligns with the FRAME count / PROGRESS readout text */
  right: auto;
  top: 50%;
  transform: translateY(-50%);
  width: calc(62vw - var(--gutter) - 15px); /* right edge runs a bit PAST the character so the bio tucks behind it */
  max-width: none;
  display: flex;
  align-items: center;          /* vertically centre the bio against the post */
  perspective: none;
  pointer-events: none;
  color: var(--ink);
  opacity: 0;
  filter: blur(16px) brightness(0.95);
  transition: opacity .8s, filter .8s;
}
.cinema__aside--igpost.is-revealed {
  opacity: 1;
  filter: blur(0) brightness(1);
  pointer-events: auto;
}
/* md and below (≤768): the IG section is a wide horizontal layout (tilted panel +
   Anton bio side-by-side) that can't fit a phone/small-tablet — the bio ran off the
   right edge. Hide it here, matching the other cinema asides (which already hide at
   ≤768). The character reel still plays; only the desktop-only aside is dropped.
   This rule sits AFTER `.cinema__aside--igpost{display:flex}` so it wins the cascade. */
@media (max-width: 768px) {
  .cinema__aside--igpost { display: none; }
}
/* On the IG page, lift the character canvas above the cinema overlay so the bio text
   tucks BEHIND the character (canvas made pointer-transparent so the post/dock/stories
   stay clickable through its transparent areas). Scoped via :has so other reel states,
   and the intro, keep the canvas at its normal z-index. */
.hero-stage__pin:has(.cinema__aside--igpost.is-revealed) .hero-stage__canvas {
  z-index: 6;
  pointer-events: none;
}
/* 3D perspective for the Instagram section. The dark panel + IG content sit on ONE flat,
   uniformly-tilted plane: .igstage flattens its children (default transform-style: flat),
   so the panel and the UI composite first and then tilt together as a single unit. Nothing
   is split into separate 3D layers, so nothing can dip behind the panel (no dark wedge).
   The bio text lives outside this, so it never tilts. */
.igperspective { flex: none; perspective: 4000px; }   /* far perspective: keeps the panel's 12deg lean
   but flattens depth-exaggeration so the horizontal stories/dock rows don't skew (circles read level) */
.igstage {
  position: relative;
  transform: rotateY(12deg);   /* tilt on the Y axis only — no rotateX (top/bottom edges stay level) */
  transform-origin: center;
  will-change: transform;
}
/* The IG UI gets its OWN local perspective so ONLY it tilts toward the cursor on hover. This
   3D scene is self-contained (flattened before it meets the panel), so the UI composites ON
   TOP of the panel in 2D and can never dip behind it (no dark wedge). The whole thing still
   rides the panel's fixed rotateY via .igstage. .iguicard reads the live --rx/--ry from JS. */
/* The UI slides in/out with the SAME animation as the panel (identical translateX, easing
   and duration with no delay), so the whole IG section appears/disappears as one. The slide
   lives here on the outer UI wrapper, kept separate from the live cursor tilt on .iguicard
   so the transitioned slide and the per-frame tilt don't conflict. */
.iguifx {
  perspective: 1000px;
  transform: translateX(-120%);   /* fully clears the left edge at rest (its box is narrower
                                     than the panel, so it needs a bit more than the panel's
                                     -104% to start equally off-screen) */
  transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1);
}
.cinema__aside--igpost.is-revealed .iguifx { transform: translateX(0); }
.iguicard {
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transform-origin: center;
  will-change: transform;
}

/* Dark side panel behind the Instagram section. Anchored to the viewport's LEFT edge
   (negative left cancels the aside's left offset), full viewport height, and wide
   enough to back the stories/post/dock but STOP before the bio text. It slides in
   from off-screen-left whenever the aside reveals. The IG content (.igcol) rides on
   top with a gentler, slightly-delayed slide so it reads as one incoming panel. */
.igpanel {
  position: absolute;
  top: 50%;
  /* overshoot 50px past the viewport's left edge so the rotateY tilt (which nudges the panel
     ~2px right) never exposes a cream sliver at x=0; the extra width keeps the right edge put */
  left: calc(-1 * (var(--gutter) + 15px) - 50px);
  width: calc(var(--gutter) + 15px + 50px + min(29vw, 38vh) + clamp(28px, 3.5vw, 100px));
  height: 100vh;
  z-index: 0;
  border-radius: 0 28px 28px 0;
  background:
    radial-gradient(125% 78% at 16% 10%, rgba(78, 80, 96, 0.34), transparent 58%),
    linear-gradient(158deg, #1a1a20 0%, #101013 56%, #0a0a0d 100%);
  box-shadow:
    36px 0 90px -42px rgba(0, 0, 0, 0.65),
    inset -1px 0 0 rgba(255, 255, 255, 0.05),
    inset 0 1px 0 rgba(255, 255, 255, 0.05);
  transform: translateY(-50%) translateX(-104%);
  transition: transform 1s cubic-bezier(0.16, 1, 0.3, 1);
  pointer-events: none;
}
.cinema__aside--igpost.is-revealed .igpanel { transform: translateY(-50%) translateX(0); }
.igpost { flex: none; width: min(29vw, 38vh); font-family: var(--f-sans); color: var(--ink); }
/* Big, bold handwritten bio: a single block centred in the gap between the post and
   the character (auto margins), sized to stand roughly as tall as the post. */
.igbio {
  flex: 1;
  margin: 0 0 0 clamp(30px, 7vw, 170px);   /* sit in the gap; the lines run right, ends behind the character */
  font-family: var(--f-display);            /* Anton — heavy condensed display, like the skills headings */
  font-weight: 400;                         /* Anton ships one heavy weight */
  font-size: clamp(28px, 3vw, 76px);      /* scaled down to fit the longer paragraph */
  line-height: 0.98;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  text-align: left;
  color: var(--ink);
  /* Premium graduated cast shadow — crisp contact + soft mid + wide diffuse ambient,
     warm-to-cool tint (28,21,11 → 20,16,10) for hand-crafted depth. em units track the
     responsive size. Text-shadow paints with the glyph, so three layers stays cheap. */
  text-shadow:
    0 0.012em 0.012em rgba(28, 21, 11, 0.58),
    0 0.05em 0.07em rgba(24, 18, 10, 0.42),
    0 0.15em 0.26em rgba(20, 16, 10, 0.26);
  min-height: 9.8em;                        /* reserve the typed lines so it doesn't jump while typing */
}
/* Each character fades in/out via opacity (layout-neutral, so wrapping stays fixed). All lines
   reveal their chars in parallel on appear, and un-write in parallel on disappear (driven by
   initIgBio) — the disappear is the mirror of the appear. */
.igbio__ch { opacity: 0; transition: opacity .4s ease; }
.igbio__ch.is-on { opacity: 1; }
/* A few scattered words (tagged by initIgBio) get a neon-accent marker. The
   bg lives on each char span so it fades in/out with the write animation; the
   vertical box-shadow gives the bar a little marker height without bleeding
   over neighbouring glyphs. */
.igbio__ch--hl {
  color: var(--ink);
  background: var(--accent);
  box-shadow: 0 0.06em 0 var(--accent), 0 -0.04em 0 var(--accent);
  text-shadow: none;                        /* keep glyphs crisp on the neon bars — no halo */
}
/* Weak devices: drop to a single soft layer. */
body.perf-lite .igbio {
  text-shadow: 0 0.06em 0.08em rgba(26, 20, 10, 0.54);
}
.igpost__header { display: flex; align-items: center; gap: 16px; padding: 0 0 18px; }
.igpost__avatar {
  width: 58px; height: 58px; border-radius: 50%; overflow: hidden; flex: none;
  background: var(--cream-2);
  box-shadow: 0 0 0 1px rgba(12, 12, 10, 0.13), 0 0 0 3px var(--cream), 0 0 0 5px rgba(12, 12, 10, 0.07);
}
.igpost__avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* text-align:left is REQUIRED — these inherit `text-align:center` from .cinema__aside, which
   centred each line in its full-width box; the short "adivrmaa" then started ~38px right of the
   box edge while the long location filled it, so the two lines looked mis-indented. Left-aligning
   makes both start at the same box edge. */
.igpost__id { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; gap: 2px; text-align: left; }
.igpost__tag { font-weight: 700; font-size: clamp(17px, 0.83vw, 21px); color: var(--ink); letter-spacing: -0.01em; }
.igpost__loc { font-size: clamp(12px, 0.59vw, 15px); color: var(--ink-2); }
.igpost__more { width: 29px; height: 29px; margin-left: auto; fill: var(--ink); }
.igpost__media {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: 12px;
  cursor: pointer;
  background: var(--cream-2);
  box-shadow: 0 26px 54px -34px rgba(0, 0, 0, 0.32), 0 8px 18px -14px rgba(0, 0, 0, 0.2);
}
.igpost__slide {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  scale: 1.08;                 /* every image sits a little zoomed-in by default */
  will-change: transform, scale;
  transition: transform .55s cubic-bezier(0.45, 0.05, 0.2, 1), scale .5s ease;
}
/* Zoom in a touch more on hover (standalone `scale` composes with the slide translateX). */
.igpost__media:hover .igpost__slide { scale: 1.14; }
.igpost__skeleton { position: absolute; inset: 0; z-index: 3; display: none; align-items: center; justify-content: center; background: var(--cream-2); }
.igpost__media.is-loading .igpost__skeleton { display: flex; }
.igpost__loader { width: 48px; height: 48px; border-radius: 50%; border: 3px solid rgba(12, 12, 10, 0.15); border-top-color: rgba(12, 12, 10, 0.42); animation: igspin 0.9s linear infinite; }
@keyframes igspin { to { transform: rotate(360deg); } }
.igpost__dots { display: flex; justify-content: center; gap: 9px; padding: 16px 0 14px; }
.igpost__dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(12, 12, 10, 0.22); cursor: pointer; transition: background .25s, transform .25s; }
.igpost__dot.is-active { background: var(--ink); transform: scale(1.25); }
.igpost__actions { display: flex; align-items: center; gap: 18px; padding: 0; }
.igpost__act { display: flex; align-items: center; justify-content: center; background: none; border: 0; padding: 3px; cursor: pointer; color: var(--ink); transition: transform .15s ease, color .2s ease; }
.igpost__act:hover { transform: scale(1.12); }
.igpost__act:active { transform: scale(0.9); }
.igpost__act--save { margin-left: auto; }
.igpost__icon { width: 34px; height: 34px; fill: currentColor; color: var(--ink); }
.igpost__count { font-weight: 600; font-size: clamp(15px, 0.75vw, 19px); color: var(--ink); margin-right: 14px; margin-left: 2px; }
.igpost__like { color: var(--ink); }
.igpost__heart { width: 34px; height: 34px; fill: none; stroke: currentColor; stroke-width: 2; transition: fill .2s ease, stroke .2s ease; }
.igpost__like.is-liked { color: #ed4956; }
.igpost__like.is-liked .igpost__heart { fill: #ed4956; stroke: #ed4956; }
.igpost__like.is-pop .igpost__heart { animation: heartPop 0.32s ease-in-out; }
@keyframes heartPop { 0% { transform: scale(1); } 45% { transform: scale(1.32); } 100% { transform: scale(1); } }

/* ---------- Instagram "stories" row (vanilla port of the typewriter-testimonial) ----------
   A row of gradient-ringed circular avatars sitting above the post; hovering one scales
   it up and types out a caption in a little speech-bubble tooltip. */
.igcol {
  flex: none; display: flex; flex-direction: column;
  position: relative; z-index: 1;            /* ride above the dark .igpanel */
}
.igstories { display: flex; justify-content: space-between; gap: clamp(6px, 0.8vw, 14px); padding: 0 0 clamp(14px, 1.8vh, 24px); }
.igstory { position: relative; display: flex; flex-direction: column; align-items: center; gap: 10px; cursor: pointer; }
.igstory__ring {
  width: clamp(50px, 3.3vw, 84px); height: clamp(50px, 3.3vw, 84px);
  border-radius: 50%; padding: 3px; flex: none;
  background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888);
  display: grid; place-items: center;
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);   /* square cell. minmax(0,..) (NOT plain 1fr,
                                 which is minmax(AUTO,1fr)) lets the track shrink to the ring height
                                 instead of being forced open by a tall portrait cover's min-content —
                                 so the avatar stays 78x78 and object-fit crops it to a circle */
  overflow: hidden;
  /* The panel's rotateY compresses circles horizontally (~4%), rendering them as ovals.
     Pre-stretch each ring on X by the inverse so it reads as a PERFECT circle on screen.
     Uses the standalone `scale` property so it composes with the hover-grow `transform`. */
  scale: 1.048 1;
  transition: transform .28s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.igstory:hover .igstory__ring { transform: scale(1.07); }
.igstory__avatar {
  width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block;
  border: 3px solid var(--cream); background: var(--cream-2);
}
.igstory__label {
  font-family: var(--f-sans); font-size: clamp(11px, 0.55vw, 14px); color: var(--ink-2);
  max-width: clamp(58px, 5vw, 92px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; letter-spacing: -0.01em;
}
.igstory__tip {
  position: absolute; top: calc(100% + 16px); left: 50%;
  width: clamp(196px, 16vw, 240px);
  transform: translateX(-50%) translateY(-8px);
  background: #fff; color: #111; border-radius: 14px; padding: 14px 16px;
  box-shadow: 0 26px 64px -26px rgba(0, 0, 0, 0.5), 0 8px 22px -16px rgba(0, 0, 0, 0.3);
  opacity: 0; visibility: hidden; pointer-events: none; z-index: 6;
  transition: opacity .35s ease, transform .35s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.igstory.is-active .igstory__tip { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
.igstory__tip-body { font-family: var(--f-sans); font-size: clamp(13px, 0.59vw, 15px); line-height: 1.5; min-height: 3em; }
.igstory__tip-text { white-space: pre-wrap; }
.igstory__caret { display: inline-block; margin-left: 1px; font-weight: 400; animation: igblink 1s steps(1) infinite; }
@keyframes igblink { 50% { opacity: 0; } }
.igstory__dots { position: absolute; left: 50%; transform: translateX(-50%); top: -15px; display: flex; flex-direction: column-reverse; align-items: center; gap: 2px; }
.igstory__dots i { display: block; border-radius: 50%; background: #fff; box-shadow: 0 4px 10px -4px rgba(0, 0, 0, 0.4); }
.igstory__dots i:nth-child(1) { width: 10px; height: 10px; }
.igstory__dots i:nth-child(2) { width: 6px; height: 6px; }
.igstory__dots i:nth-child(3) { width: 3px; height: 3px; }

/* ---------- Instagram bottom-nav "dock" (vanilla port of the dock-two component) ----------
   A floating frosted pill of nav icons centred at the bottom of the page; only shown on
   the IG-aside page. Gentle continuous float + hover lift with a label tooltip. */
/* width = post width + a small symmetric overhang (var). Percentage width keeps it from
   growing .igcol, so the stories/post/bio alignment is unaffected; negative left margin
   re-centres the slightly-wider bar over the post. */
.igdock {
  --igdock-grow: clamp(14px, 1.7vw, 36px);
  width: calc(100% + var(--igdock-grow));
  margin: clamp(12px, 1.4vh, 24px) 0 0 calc(var(--igdock-grow) / -2);
  pointer-events: auto;
}
.igdock__bar {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%;
  padding: clamp(8px, 0.7vw, 13px) clamp(14px, 1.2vw, 24px);
  border-radius: 18px;
  background: rgba(248, 245, 238, 0.72);
  backdrop-filter: blur(18px) saturate(1.3);
  border: 1px solid rgba(12, 12, 10, 0.10);
  box-shadow: 0 18px 44px -28px rgba(0, 0, 0, 0.4), 0 6px 16px -14px rgba(0, 0, 0, 0.25);
}
.igdock__btn {
  position: relative; display: grid; place-items: center;
  width: clamp(40px, 2.9vw, 52px); height: clamp(40px, 2.9vw, 52px);
  border: 0; background: none; border-radius: 14px; cursor: pointer; color: var(--ink);
  transition: transform .2s cubic-bezier(0.22, 0.61, 0.36, 1), background .2s ease;
}
.igdock__btn:hover { transform: translateY(-3px) scale(1.1); background: rgba(12, 12, 10, 0.06); }
.igdock__btn:active { transform: scale(0.94); }
.igdock__btn svg { width: clamp(20px, 1.6vw, 26px); height: clamp(20px, 1.6vw, 26px); fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.igdock__btn--me img { width: clamp(26px, 1.95vw, 34px); height: clamp(26px, 1.95vw, 34px); border-radius: 50%; object-fit: cover; box-shadow: 0 0 0 2px var(--ink); }
.igdock__btn::after {
  content: attr(data-label);
  position: absolute; bottom: calc(100% + 9px); left: 50%;
  transform: translateX(-50%) translateY(5px);
  padding: 4px 9px; border-radius: 7px;
  font-family: var(--f-mono); font-size: 11px; letter-spacing: 0.04em;
  background: var(--ink); color: var(--cream); white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity .2s ease, transform .2s ease;
}
.igdock__btn:hover::after { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ---------- Instagram DARK MODE (content sitting on the dark .igpanel) ----------
   Scoped to .igcol so the cream-page bio text to the right is untouched. Flips the
   IG card / stories / dock to light-on-dark to match the dark side panel. */
.igcol .igpost { color: #f3f1ea; }
.igcol .igpost__tag { color: #f6f4ee; }
.igcol .igpost__loc { color: rgba(243, 241, 234, 0.62); }
.igcol .igpost__more { fill: #f6f4ee; }
.igcol .igpost__avatar {
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2), 0 0 0 3px #15151a, 0 0 0 5px rgba(255, 255, 255, 0.08);
}
.igcol .igpost__dot { background: rgba(255, 255, 255, 0.28); }
.igcol .igpost__dot.is-active { background: #f6f4ee; }
.igcol .igpost__act,
.igcol .igpost__icon,
.igcol .igpost__like { color: #f3f1ea; }
.igcol .igpost__count { color: #f3f1ea; }
.igcol .igstory__avatar { border-color: #15151a; }
.igcol .igstory__label { color: rgba(243, 241, 234, 0.72); }
.igcol .igdock__bar {
  background: rgba(30, 30, 35, 0.55);
  border-color: rgba(255, 255, 255, 0.1);
  box-shadow: 0 18px 44px -28px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.igcol .igdock__btn { color: #f3f1ea; }
.igcol .igdock__btn:hover { background: rgba(255, 255, 255, 0.1); }
.igcol .igdock__btn--me img { box-shadow: 0 0 0 2px #f3f1ea; }
.igcol .igdock__btn::after { background: #f3f1ea; color: #15151a; }

/* ---------- "Calling card" dictionary-style intro ----------
   Upper-left of the character: a bold headword + phonetic, a numbered
   definition (the intro), and a hand-drawn arrow curving toward the figure.
   Lives in the cinema overlay so it fades in with the HUD. */
.callcard {
  position: absolute;
  top: clamp(96px, 13vh, 168px);
  left: clamp(36px, 5vw, 96px);
  width: clamp(280px, 18vw, 460px);
  margin: 0;
  color: var(--ink);
  pointer-events: none;
  /* Coherent appear/disappear for the whole card (text + arrow together):
     hidden until .is-in, toggled by initCallcard only once you're on the page. */
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.55s ease, transform 0.6s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.callcard.is-in {
  opacity: 1;
  transform: translateY(0);
}
.callcard__def {
  position: relative;                   /* positioning context for the overlaid live text */
  margin: 0;
  /* Fun, hand-written script — pairs with the hand-drawn arrow. */
  font-family: var(--f-sign);
  font-weight: 600;
  font-size: clamp(21px, 1.2vw, 30px);
  line-height: 1.38;
  color: var(--ink);
  /* Premium graduated lift to echo the signature — three warm, em-scaled
     layers (crisp contact + soft mid + diffuse ambient) with a warm-to-cool
     tint. Kept tighter than the big wordmark so the script intro copy stays
     crisp and legible while gaining depth. */
  text-shadow:
    0 0.028em 0.028em rgba(28, 21, 11, 0.52),
    0 0.09em 0.12em rgba(24, 18, 10, 0.36),
    0 0.22em 0.34em rgba(20, 16, 10, 0.22);
}
/* Weak devices: one soft layer — the typed text re-renders each char as it
   types, so keep the shadow cheap. */
body.perf-lite .callcard__def {
  text-shadow: 0 0.07em 0.1em rgba(26, 20, 10, 0.52);
}
/* Sizer reserves the FULL wrapped height up front (so the arrow never jumps);
   the live/typed text is overlaid on it and wraps identically. */
.callcard__sizer { visibility: hidden; }
.callcard__live { position: absolute; top: 0; left: 0; right: 0; }
/* Blinking caret for the typing effect (smooth fade pulse, like the source). */
.callcard__caret {
  display: inline-block;
  width: 3px;
  height: 1em;
  margin-left: 3px;
  border-radius: 1px;
  background: currentColor;
  vertical-align: -0.12em;
  animation: callcardBlink 0.8s ease-in-out infinite alternate;
}
@keyframes callcardBlink { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .callcard__caret { animation: none; }
  .callcard { transition: none; }
}
.callcard__arrow {
  display: block;
  width: clamp(110px, 9vw, 150px);
  height: auto;
  margin: 0.4em 0 0 auto;              /* pushed right, under the text, pointing at the figure */
  margin-right: clamp(20px, 6vw, 110px);
  color: var(--ink);
  opacity: 0.85;
}
/* Arrow draw-on: paths use pathLength="1", so dashoffset 1→0 strokes them in.
   Hidden until .is-drawn (toggled per entry by initCallcard), arrowhead last. */
.callcard__arrow path { stroke-dasharray: 1; stroke-dashoffset: 1; }
.callcard.is-drawn .callcard__arrow path { animation: callcardDraw 0.55s ease forwards; }
.callcard.is-drawn .callcard__arrow path:last-child { animation-delay: 0.4s; }
@keyframes callcardDraw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
  .callcard__arrow path { stroke-dashoffset: 0; animation: none; }   /* always visible */
}
@media (max-width: 900px) {
  .callcard { display: none; }         /* no room beside the figure on small screens */
}

/* =========================================================
   SECTION HEAD (shared)
   ========================================================= */
.section-head {
  display: grid;
  grid-template-columns: 80px 1fr;
  column-gap: clamp(20px, 4vw, 60px);
  row-gap: 12px;
  align-items: end;
  margin-bottom: clamp(50px, 8vh, 90px);
  padding: 0 var(--gutter);
}
.section-head__num {
  font-family: var(--f-mono);
  font-size: clamp(10px, 0.47vw, 12px);
  letter-spacing: 0.22em;
  color: var(--ink-mute);
  padding-bottom: 10px;
  border-bottom: 1px solid var(--ink);
}
.section-head__title {
  margin: 0;
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(40px, 3.75vw, 96px);
  line-height: 0.92;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.section-head__title .italic {
  font-family: var(--f-serif);
  font-style: italic;
  font-weight: 400;
  letter-spacing: -0.015em;
  color: var(--ink);
}
.section-head__lede {
  grid-column: 2 / -1;
  max-width: 56ch;
  margin: 0;
  font-family: var(--f-sans);
  font-size: clamp(15px, 0.71vw, 18px);
  line-height: 1.55;
  color: var(--ink-mute);
  letter-spacing: -0.005em;
}

/* =========================================================
   PROJECTS — INFINITE GALLERY HALL
   Three.js 3D wall of "paintings" (project cards). The outer
   section provides vertical scroll runway; the inner pin is
   sticky 100vh and hosts the canvas + UI overlay. ScrollTrigger
   converts that vertical scroll into the horizontal camera
   target inside the Three.js scene.
   ========================================================= */
.hall {
  position: relative;
  /* Scroll runway for the gallery. The inner __pin is sticky for
     (height - 100vh); the first 200vh of that range is the 3D fly-in
     intro (camera flies forward through the section first-frame collage
     onto the live project wall), and the remaining ~280vh is the camera
     pan through all projects.

     margin-top: -300vh overlaps the last 300vh of the (extended) skills
     section so the gallery's fly-in plays while the real skills pin is STILL
     stuck behind it — the transparent gallery layer shows that live scene as
     the intro's background. z-index 2 keeps the gallery in front of the warp
     (z-index 1). The gallery layer stays transparent (--clear) throughout the
     intro so the skills scene reads straight through it. */
  height: 580vh;
  margin-top: -100vh;     /* overlaps the warp's 200vh tail so the gallery pin
                            expands to full screen while the warp pin is STILL stuck
                            behind it. The expand (intro ST) starts exactly at the
                            warp scrub end — when the dedicated gallery frame is at
                            the front — and finishes as the warp pin releases. */
  z-index: 2;
  color: var(--ink);
  /* NOTE: no `perspective`/`transform`/`filter` here — those would make the
     intro's `position: fixed` pin resolve against THIS section instead of the
     viewport (so it would still "rise"). The 3D depth now lives in the gallery's
     own THREE camera fly-in, not a CSS tilt. */
}
.hall__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  width: 100%;
  overflow: hidden;
  /* Dark by default so the pin reads as "still in skills" while it slides into
     place during the section overlap (dark-on-dark over the skills scene = no
     cream seam) AND through the 3D fly-in intro (the gallery's own THREE scene
     renders the collage emerging from this dark field). Once the camera has
     flown in and the live wall fills the frame, JS adds .hall__pin--clear →
     transparent, so the gallery shows the SAME global background (cream + fixed
     topo + grain) as every other section. */
  background-color: var(--ink);
  transform-origin: 50% 0%;
}
/* Arrived: transparent so the global cream + topo shows through. */
.hall__pin--clear { background-color: transparent; }

/* The gallery shown as a FLOATING FRAME inside the warp (before it expands to
   full screen). Cream page background + a soft drop shadow + rounded corners so
   the scaled-down pin reads as a card floating on the dark warp scene. The shadow
   scales with the pin's transform — natural for a card growing toward the viewer.
   At full screen JS swaps this for --clear (transparent → global cream + topo). */
.hall__pin--frame {
  background-color: var(--cream);
  box-shadow: 0 28px 90px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(12, 12, 10, 0.10);
  border-radius: 6px;
}

/* Top/bottom HUD lines for the 3D fly-in — mirrors .warp__hud (gray mono labels
   + thin gray lines) so the intro matches the skills section's chrome with no
   green. JS fades opacity with the fly-in. */
.hall__intro-hud {
  position: absolute;
  inset: clamp(18px, 2.2vw, 32px);
  z-index: 5;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  font-family: var(--f-mono);
  font-size: 10px;
  color: rgba(242, 238, 229, 0.5);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0;
}
.hall__intro-hud > div { display: flex; align-items: center; gap: 16px; }
.hall__intro-hud i { flex: 1; height: 1px; background: rgba(242, 238, 229, 0.18); display: block; }

/* During the intro ONLY: pin the gallery to the viewport (fixed) instead of
   letting the sticky pin slide up from the bottom of the section. This is what
   kills the "panel sliding in" — the gallery is full-screen the entire fly-in,
   so the collage emerges across the whole screen (continuous with the skills
   flythrough behind it), not on a rising rectangle. Swapped back to sticky the
   instant the section's own top reaches the viewport top (same position → no
   jump), so the camera-pan + exit gate behave exactly as before. */
.hall__pin--fixed { position: fixed; top: 0; left: 0; z-index: 2; }

/* While the 3D fly-in intro plays (camera flying toward the project wall), the
   gallery chrome/text stay hidden so only the emerging collage + wall read;
   they fade in once the wall has filled the frame and you've "arrived". */
.hall__pin.is-entering .hall__chrome,
.hall__pin.is-entering .hall__ui { opacity: 0; transition: opacity 0.45s ease; }

/* Ambient accent glow — two soft chartreuse pools, one behind the text panel
   (left) and one behind the painting (right). Sits below the canvas (z 0 vs
   z 1) so it reads as a halo: it bleeds around the painting's edges and washes
   gently behind the text. Kept very low-alpha so it's barely-there premium
   atmosphere on cream; the dark-mode variant lifts it just enough to glow
   like neon without shouting. Static (no animation) so perf-lite is unaffected. */
.hall__glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(42% 48% at 23% 50%, rgba(216, 255, 56, 0.085), transparent 70%),
    radial-gradient(44% 56% at 69% 50%, rgba(216, 255, 56, 0.07),  transparent 72%);
}
html.is-dark .hall__glow {
  background:
    radial-gradient(44% 50% at 23% 50%, rgba(216, 255, 56, 0.11), transparent 70%),
    radial-gradient(48% 58% at 69% 50%, rgba(216, 255, 56, 0.19), transparent 72%);
}
.hall__canvas {
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* Floating link-preview card shown when hovering a video painting (vanilla
   port of the Aceternity LinkPreview). The .lp-follow wrapper trails the
   cursor (JS damped-spring); the .lp-card runs the slow incoming animation.
   pointer-events:none so it never blocks the canvas hit-testing. */
.lp-follow {
  position: fixed;
  left: 0; top: 0;
  z-index: 9000;
  pointer-events: none;
  will-change: transform;
}
.lp-card {
  /* Anchored bottom-centre on the follow point (sits just above the cursor). */
  position: absolute;
  left: 0; bottom: 0;
  display: block;
  padding: 5px;
  background: #ffffff;
  border: 1px solid rgba(0, 0, 0, 0.06);
  border-radius: 11px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.12);
  opacity: 0;
  /* Slow incoming animation: a big rise + scale-up with a soft overshoot. */
  transform: translate(-50%, 42px) scale(0.7);
  transform-origin: center bottom;
  transition: opacity 0.8s ease, transform 1.1s cubic-bezier(0.16, 0.84, 0.24, 1.08);
  pointer-events: none;
  will-change: transform, opacity;
}
.lp-card.is-on {
  opacity: 1;
  transform: translate(-50%, 0) scale(1);
}

/* Image frame holds the shimmer skeleton + the screenshot. */
.lp-card__frame {
  position: relative;
  width: 196px;
  height: 122px;
  border-radius: 8px;
  overflow: hidden;
  background: #e9e6dd;
}
.lp-card__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.12);
  transition: opacity 0.9s ease, transform 1.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.lp-card.is-loaded .lp-card__img {
  opacity: 1;
  transform: scale(1);
}

/* Shimmer skeleton while the screenshot is being generated/fetched. */
.lp-card__skeleton {
  position: absolute;
  inset: 0;
  background-color: #e4dfd3;
  background-image: linear-gradient(
    100deg,
    rgba(255, 255, 255, 0) 20%,
    rgba(255, 255, 255, 0.65) 50%,
    rgba(255, 255, 255, 0) 80%
  );
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: lp-shimmer 1.5s ease-in-out infinite;
  opacity: 1;
  transition: opacity 0.6s ease;
}
.lp-card.is-loaded .lp-card__skeleton {
  opacity: 0;
}
@keyframes lp-shimmer {
  0%   { background-position: -120% 0; }
  100% { background-position: 220% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .lp-card { transition: opacity 0.2s ease; transform: translate(-50%, 0) scale(1); }
  .lp-card__img { transition: opacity 0.3s ease; transform: none; }
  .lp-card__skeleton { animation: none; }
}
.hall__canvas canvas {
  display: block;
  width: 100% !important;
  height: 100% !important;
}

/* Top-left logo + bottom-left scroll hint (chrome) */
.hall__chrome {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.hall__logo {
  position: absolute;
  top: clamp(20px, 3vw, 40px);
  left: clamp(20px, 3vw, 50px);
  font-family: var(--f-mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.85;
  /* Two-layer lift on the mono HUD chrome so every gallery label carries the
     shadow (em-scaled off the small font, warm-to-cool tint). */
  text-shadow:
    0 0.05em 0.09em rgba(26, 20, 10, 0.34),
    0 0.14em 0.24em rgba(20, 16, 10, 0.2);
}
.hall__hint {
  position: absolute;
  bottom: clamp(20px, 3vw, 40px);
  left: clamp(20px, 3vw, 50px);
  font-family: var(--f-mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(0,0,0,0.45);
  text-shadow:
    0 0.05em 0.09em rgba(26, 20, 10, 0.34),
    0 0.14em 0.24em rgba(20, 16, 10, 0.2);
}
.hall__hint span::before {
  content: "↓  ";
  display: inline-block;
  margin-right: 4px;
  transform: translateY(1px);
}

/* UI overlay — only one slide is `.is-active` at a time */
.hall__ui {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}
.hall-slide {
  position: absolute;
  /* Vertically CENTER the text block in the viewport (was top: 22%, which
     anchored it high so a tall block ran off the bottom on shorter screens).
     Centering keeps it balanced and fitting at any viewport height; the reveal
     rise (18px) is folded into the same transform. */
  top: 50%;
  /* Pushed further right so the panel clears the previous painting's
     outline / shadow that bleeds in from the left of the 3D wall
     view. ~15vw lands the text past where the off-camera painting
     edge sits on typical viewports. Mobile (below) overrides this. */
  left: clamp(40px, 15vw, 380px);
  width: clamp(280px, 16.4vw, 420px);
  opacity: 0;
  transform: translate(0, calc(-50% + 18px));
  transition: opacity 0.7s ease, transform 0.7s cubic-bezier(.22,1,.36,1);
  /* Only the .is-active slide captures pointer events — the others
     are stacked invisibly at the same position (later DOM order = on
     top), and without this they'd steal every hover from the visible
     slide, so the decrypt effect would only fire on the last
     project's words no matter which project was actually showing. */
  pointer-events: none;
}
.hall-slide.is-active {
  opacity: 1;
  transform: translate(0, -50%);
  pointer-events: auto;
}
.hall-slide__num {
  display: inline-block;
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(0,0,0,0.45);
  border-bottom: 1px solid rgba(0,0,0,0.15);
  padding-bottom: 6px;
  margin-bottom: clamp(15px, 0.94vw, 24px);
  text-shadow:
    0 0.05em 0.09em rgba(26, 20, 10, 0.34),
    0 0.14em 0.24em rgba(20, 16, 10, 0.2);
}
.hall-slide__title {
  font-family: var(--f-serif, "Instrument Serif", serif);
  font-weight: 400;
  font-size: clamp(40px, 2.5vw, 64px);
  line-height: 1.05;
  letter-spacing: -0.01em;
  margin: 0 0 clamp(13px, 0.86vw, 22px);
  color: var(--ink);
}
.hall-slide__title i {
  font-style: italic;
  color: var(--ink-mute, #555);
}
.hall-slide__title-sub {
  display: block;
  margin-top: clamp(9px, 0.55vw, 14px);
  font-family: var(--f-serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(24px, 1.5vw, 38px);
  line-height: 1.05;
  color: var(--ink-mute);
  /* Lighter two-layer lift — thin serif italic, so kept soft to stay legible. */
  text-shadow:
    0 0.02em 0.03em rgba(28, 21, 11, 0.38),
    0 0.08em 0.15em rgba(20, 16, 10, 0.24);
}
.hall-slide__title-main {
  display: flex;
  align-items: center;
  gap: clamp(15px, 1.35vw, 22px);
  perspective: 900px;            /* enables the gentle 3D tilt on hover */
  /* Premium graduated lift on the project wordmark — crisp contact + soft
     mid + wide ambient, warm-to-cool tint, em-scaled. Matches the landing's
     shadow language so the gallery reads as one system. (The logo chip keeps
     its own box-shadow; text-shadow only touches the glyphs.) */
  text-shadow:
    0 0.012em 0.012em rgba(28, 21, 11, 0.52),
    0 0.05em 0.07em rgba(24, 18, 10, 0.36),
    0 0.14em 0.24em rgba(20, 16, 10, 0.24);
}

/* ------------------------------------------------------------------
   PROJECT LOGO — reusable premium 3D icon chip.
   The source art tiles already carry baked gloss / bevel / gradient, so
   this wrapper adds only *complementary* depth, never competing with it:
     • a symmetric layered shadow stack (tight contact → soft wide cast)
       that lifts the tile cleanly off the paper;
     • a soft top-left specular sheen for glassy shine;
     • a bevel ring (light top edge / dark bottom edge) for separation —
       this is what keeps the cream-faced Splitz tile from dissolving
       into the cream page.
   The <img> is object-fit:contain inside a fixed square, so every logo is
   framed + sized identically regardless of source dimensions, with no
   stretching or clipping. Titles stay the focus; the chip only supports.
   ------------------------------------------------------------------ */
.project-logo {
  position: relative;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: clamp(50px, 5vw, 74px);
  aspect-ratio: 1 / 1;
  border-radius: 23%;            /* squircle, matches the tile silhouette */
  box-shadow:
    0 1px 1px rgba(28, 24, 16, 0.10),
    0 3px 6px rgba(28, 24, 16, 0.10),
    0 9px 18px rgba(28, 24, 16, 0.13),
    0 20px 34px rgba(28, 24, 16, 0.10);
  /* baseline nudge: optically centre on the title cap height (the scramble
     block also contains its underline below the text). */
  transform: translateY(-6px);
  transform-style: preserve-3d;
  transition:
    transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.5s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}
.project-logo__img {
  width: 100%;
  height: 100%;
  object-fit: contain;          /* never stretch / distort; preserve aspect */
  border-radius: inherit;
  display: block;
  /* deepen the embossed mark a touch without muddying the art */
  filter: drop-shadow(0 1.5px 2px rgba(0, 0, 0, 0.14));
}
/* specular sheen + bevel ring, painted ABOVE the art (::after = on top) */
.project-logo::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    152deg,
    rgba(255, 255, 255, 0.50) 0%,
    rgba(255, 255, 255, 0.10) 22%,
    rgba(255, 255, 255, 0) 45%
  );
  box-shadow:
    inset 0 1.5px 1px rgba(255, 255, 255, 0.55),
    inset 0 -2px 3px rgba(0, 0, 0, 0.16),
    inset 0 0 0 1px rgba(255, 255, 255, 0.06);
  opacity: 0.9;
  transition: opacity 0.5s ease;
}
/* polished hover — lift higher, a gentle dimensional tilt, deeper shadow */
.hall-slide.is-active .project-logo:hover {
  transform: translateY(-9px) scale(1.05) rotateX(7deg) rotateY(-9deg);
  box-shadow:
    0 3px 5px rgba(28, 24, 16, 0.12),
    0 8px 16px rgba(28, 24, 16, 0.13),
    0 18px 30px rgba(28, 24, 16, 0.15),
    0 34px 54px rgba(28, 24, 16, 0.12);
}
.hall-slide.is-active .project-logo:hover::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .project-logo { transition: none; }
  .hall-slide.is-active .project-logo:hover { transform: translateY(-6px); }
}

/* ------------------------------------------------------------------
   TEXT SCRAMBLE — kinetic hover-decode title.
   Vanilla port of the react text-scramble component. The text
   resolves left → right on hover, each still-scrambling character
   tinted accent + scaled 1.1. An underline slides in from the
   left and a subtle accent glow fades in behind the row; both
   retract on mouseleave.
   ------------------------------------------------------------------ */
.text-scramble {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  cursor: pointer;
  user-select: none;
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(44px, 3vw, 76px);
  letter-spacing: -0.005em;
  text-transform: uppercase;
  line-height: 0.92;
  color: var(--ink);
  outline: none;
}
.text-scramble__row {
  position: relative;
  display: inline-flex;
}
.text-scramble__word { display: inline-flex; }
.text-scramble__char {
  display: inline-block;
  color: var(--ink);
  transition: color 150ms ease, transform 150ms ease;
  will-change: transform;
}
.text-scramble__char.is-scrambling {
  color: var(--accent-deep);
  transform: scale(1.1);
}
.text-scramble__underline {
  position: relative;
  display: block;
  height: 1.5px;
  width: 100%;
  margin-top: 12px;
  overflow: hidden;
}
.text-scramble__underline-base {
  position: absolute;
  inset: 0;
  background: rgba(12, 12, 10, 0.14);
}
.text-scramble__underline-active {
  position: absolute;
  inset: 0;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 500ms cubic-bezier(0.22, 1, 0.36, 1);
}
.text-scramble:hover .text-scramble__underline-active,
.text-scramble:focus-visible .text-scramble__underline-active {
  transform: scaleX(1);
}
.text-scramble__glow {
  position: absolute;
  inset: -18px;
  border-radius: 14px;
  background: rgba(180, 221, 24, 0.10);
  opacity: 0;
  transition: opacity 300ms ease;
  pointer-events: none;
  z-index: -1;
}
.text-scramble:hover .text-scramble__glow,
.text-scramble:focus-visible .text-scramble__glow {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .text-scramble__char,
  .text-scramble__underline-active,
  .text-scramble__glow { transition: none; }
}
.hall-slide__desc {
  font-family: var(--f-sans);
  font-weight: 300;
  font-size: clamp(15px, 0.71vw, 18px);
  line-height: 1.7;
  color: rgba(0,0,0,0.62);
  margin: 0 0 clamp(20px, 1.4vw, 36px);
  /* Two-layer lift — more apparent now, still tuned to stay legible at this
     size / light weight (crisp contact + soft drop). */
  text-shadow:
    0 0.025em 0.03em rgba(28, 21, 11, 0.28),
    0 0.07em 0.13em rgba(20, 16, 10, 0.17);
}

/* ------------------------------------------------------------------
   HYPER-TEXT DECRYPTION EFFECT (vanilla port of the React component)
   JS (initHyperText) wraps every word in the description in a
   .hyper-word span; words listed in `data-hl-words` get .is-hl
   (interactive). Hovering an interactive word:
     - adds .is-hyper-hover to the parent → dims every other word
     - adds .is-hovered to the word → scales, dark pill, scrambles
     - the JS scrambles the chars then resolves to the original word.
   Pure CSS transitions handle the visuals; the scramble itself is
   purely text-content updates from JS.
   ------------------------------------------------------------------ */
.hyper-word {
  position: relative;
  display: inline-block;
  white-space: nowrap;
  transition:
    opacity 0.3s ease,
    filter  0.3s ease,
    transform 0.35s cubic-bezier(.22,1,.36,1),
    color   0.2s ease;
}
.hyper-word.is-hl {
  cursor: pointer;
  font-family: var(--f-mono);
  font-weight: 600;
  /* Dark forest green — strong contrast on cream while reading as
     a natural, premium accent. On hover the colour flips to cream
     against the dark pill (rule below), so this only paints the
     idle / "ready to decrypt" state. */
  color: #15803d;
  letter-spacing: 0.005em;
}
/* Dark pill behind the hovered word — hidden by default. */
.hyper-word.is-hl::before {
  content: '';
  position: absolute;
  inset: -3px -7px;
  border-radius: 8px;
  background: var(--ink);
  opacity: 0;
  transform: scale(0.85);
  transition: opacity 0.25s ease, transform 0.25s cubic-bezier(.22,1,.36,1);
  z-index: -1;
  pointer-events: none;
  /* Soft green glow under the dark pill — same hue family as the
     idle colour so the hover state reads as the natural "lit-up"
     version of the same accent. */
  box-shadow:
    0 10px 22px -6px rgba(21, 128, 61, 0.38),
    0 6px 10px -4px rgba(0, 0, 0, 0.20);
}
/* Tiny green dot at the top-right corner of the hovered word —
   slightly brighter shade so it pops against the dark pill. */
.hyper-word.is-hl::after {
  content: '';
  position: absolute;
  top: -4px;
  right: -4px;
  width: 5px;
  height: 5px;
  background: #22c55e;
  border-radius: 50%;
  opacity: 0;
  transform: scale(0);
  transition: opacity 0.2s ease, transform 0.3s cubic-bezier(.34,1.56,.64,1);
  pointer-events: none;
}

/* When any interactive word is hovered, dim every word in the
   paragraph (so the focus is on the one being decrypted). */
.is-hyper-hover .hyper-word {
  opacity: 0.3;
  filter: blur(2px);
}
/* The hovered word itself: scale + lift + dark pill + cream text. */
.is-hyper-hover .hyper-word.is-hovered {
  opacity: 1;
  filter: none;
  transform: scale(1.08) translateY(-3px);
  color: var(--cream);
  z-index: 20;
}
.is-hyper-hover .hyper-word.is-hovered::before {
  opacity: 1;
  transform: scale(1);
}
.is-hyper-hover .hyper-word.is-hovered::after {
  opacity: 1;
  transform: scale(1);
}

@media (prefers-reduced-motion: reduce) {
  .hyper-word,
  .hyper-word::before,
  .hyper-word::after {
    transition: none !important;
  }
}
.hall-slide__meta {
  display: grid;
  grid-template-columns: 70px 1fr;
  row-gap: 10px;
  column-gap: 14px;
  border-top: 1px solid rgba(0,0,0,0.12);
  padding-top: clamp(11px, 0.7vw, 18px);
}
.hall-slide__meta-label {
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(0,0,0,0.45);
  align-self: center;
  text-shadow:
    0 0.05em 0.09em rgba(26, 20, 10, 0.34),
    0 0.14em 0.24em rgba(20, 16, 10, 0.2);
}
.hall-slide__meta-value {
  font-family: var(--f-serif, "Instrument Serif", serif);
  font-style: italic;
  font-size: clamp(14px, 0.67vw, 17px);
  color: var(--ink);
  /* Two-layer lift to match the copy above. */
  text-shadow:
    0 0.025em 0.03em rgba(28, 21, 11, 0.32),
    0 0.08em 0.14em rgba(20, 16, 10, 0.19);
}
/* GitHub / repo link — inherits the serif-italic meta value look, with an
   underline + green accent on hover (matching the body's hyper-word accent). */
.hall-slide__meta-link {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid rgba(0, 0, 0, 0.28);
  white-space: nowrap;
  transition: color 0.22s ease, border-color 0.22s ease;
}
.hall-slide__meta-link:hover {
  color: #15803d;
  border-bottom-color: #15803d;
}

@media (max-width: 768px) {
  .hall { height: 580vh; }
  .hall-slide {
    top: auto;
    bottom: 18%;
    left: 18px;
    right: 18px;
    width: auto;
    max-width: none;
    transform: translateY(18px);           /* mobile: bottom-anchored, not centered */
  }
  .hall-slide.is-active { transform: translateY(0); }
  .hall-slide__title { font-size: clamp(34px, 9vw, 48px); }
  .project-logo { width: clamp(42px, 11vw, 52px); transform: translateY(-4px); }
  .hall-slide.is-active .project-logo:hover { transform: translateY(-4px); }
  .hall-slide__title-main { gap: 13px; }
  .hall__logo, .hall__hint { font-size: 10px; }
}

@media (prefers-reduced-motion: reduce) {
  .hall-slide {
    transition: opacity 0.2s linear;
    transform: translateY(-50%);   /* stay vertically centered without the rise */
  }
}

/* =========================================================
   SEAM (inter-section divider)
   ========================================================= */
.seam {
  position: relative;
  z-index: 1;
  display: flex; align-items: center; justify-content: center;
  gap: 14px;
  padding: 40px var(--gutter) 20px;
}
.seam__line {
  flex: 1; max-width: 320px;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--line) 30%, var(--line) 70%, transparent);
}
.seam__chip {
  width: 28px; height: 28px;
  border-radius: 50%;
  border: 1px solid var(--ink);
  display: grid; place-items: center;
  font-family: var(--f-mono);
  color: var(--ink);
  background: var(--cream);
}

/* =========================================================
   HIGHLIGHTS / STATS
   ========================================================= */
.highlights {
  position: relative;
  padding: var(--section-y) 0;
  z-index: 1;
}
.highlights__grid {
  padding: 0 var(--gutter);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 18px;
}
.stat-card {
  position: relative;
  padding: 28px 24px 24px;
  background: var(--glass);
  backdrop-filter: blur(8px);
  border: 1px solid var(--glass-stroke);
  border-radius: 18px;
  display: flex; flex-direction: column; gap: 8px;
  overflow: hidden;
  transition: transform .5s var(--ease-out-quart), box-shadow .5s var(--ease-out-quart);
}
.stat-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 30px 60px -30px rgba(12,12,10,0.25);
}
.stat-card--accent { background: var(--ink); color: var(--cream); border-color: var(--ink); }
.stat-card--accent .stat-card__label,
.stat-card--accent .stat-card__unit { color: rgba(242, 238, 229, 0.6); }
.stat-card--accent .stat-card__num { color: var(--accent); }
.stat-card--accent .stat-card__bar { background: rgba(255,255,255,0.1); }
.stat-card--accent .stat-card__bar i { background: var(--accent); }

.stat-card__label {
  font-family: var(--f-mono); font-size: 10px;
  letter-spacing: 0.22em; color: var(--ink-mute);
}
.stat-card__num {
  font-family: var(--f-display);
  font-size: clamp(48px, 3vw, 76px);
  line-height: 0.9;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin-top: 8px;
}
.stat-card__unit {
  font-family: var(--f-sans);
  font-size: clamp(10px, 0.51vw, 13px);
  color: var(--ink-mute);
  margin-top: 4px;
  line-height: 1.4;
}
.stat-card__bar {
  margin-top: auto;
  height: 2px;
  background: rgba(12,12,10,0.10);
  display: block; overflow: hidden;
}
.stat-card__bar i {
  display: block; height: 100%; width: 0%;
  background: var(--ink);
  transition: width 1.4s var(--ease-out-quart);
}
.stat-card.in-view .stat-card__bar i { width: 100%; }

/* ticker */
.ticker {
  margin-top: clamp(60px, 8vh, 100px);
  border-top: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
  padding: 22px 0;
  overflow: hidden;
  background: transparent;
}
.ticker__track {
  display: flex; gap: 30px;
  white-space: nowrap;
  font-family: var(--f-display);
  font-size: clamp(22px, 1.3vw, 32px);
  letter-spacing: 0.04em;
  color: var(--ink);
  animation: tickerScroll 32s linear infinite;
}
.ticker__track span:nth-child(even) {
  color: var(--ink-mute);
}
@keyframes tickerScroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* =========================================================
   SKILLS — HYPER-SCROLL 3D WARP
   Pinned full-viewport "deep dive" between the cream hero and
   the rest of the page. Camera flies through 5 category
   headings + 15 skill cards in 3D space, with dark cyber
   chrome (HUD, scanlines, vignette) over the portfolio's
   chartreuse accent.
   ========================================================= */
.warp {
  position: relative;
  /* Layout: 100vh hero-reveal zone + 600vh camera scrub (skill cards + a SHORT
     frame field + the live gallery pin emerging and EXPANDING to full screen — all
     on the camera scrub, so the pin stays STUCK throughout) + 100vh tail. Height
     800vh = 100 + 600 + 100. margin-top: -100vh overlaps the last 100vh of the hero
     so the warp pin engages before the hero pin unsticks. */
  height: 800vh;
  margin-top: -100vh;
  color: var(--cream);
  z-index: 1;          /* back layer — hero (z-3) and gallery (z-2) overlay us */
  /* Dark BG on the SECTION (not just the pin). CSS `filter: blur()`
     on .warp__pin extends past the pin's edges and fades to
     transparent — without this, the body's cream BG would show
     through that fade as bright white/cream stripes on the left
     and right edges of the blurred dark scene during the hero
     reveal. With the section itself painted ink, the fade picks
     up the same dark colour and the edges stay clean. */
  background: var(--ink);
}
.warp__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  width: 100%;
  overflow: hidden;
  /* Flat ink — no radial "glow" pool (it read as a green wash through the
     gallery's transparent fly-in layer; removing it keeps the skills scene and
     the fly-in an exact, even dark). */
  background: var(--ink);
  cursor: crosshair;
}

/* HUD chrome */
.warp__hud {
  position: absolute;
  inset: clamp(18px, 2.2vw, 32px);
  z-index: 20;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  font-family: var(--f-mono);
  font-size: 10px;
  color: rgba(242, 238, 229, 0.5);
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.warp__hud-top, .warp__hud-bottom {
  display: flex; align-items: center; gap: 16px;
}
.warp__hud strong { color: var(--accent); font-weight: 500; }
.warp__hud-line {
  flex: 1;
  height: 1px;
  background: rgba(242, 238, 229, 0.18);
  position: relative;
}
.warp__hud-line::after {
  content: '';
  position: absolute;
  right: 0; top: -2px;
  width: 5px; height: 5px;
  background: var(--accent);
}

/* 3D viewport */
.warp__viewport {
  position: absolute;
  inset: 0;
  perspective: 1000px;
  overflow: hidden;
  z-index: 1;
}
.warp__world {
  position: absolute;
  top: 50%; left: 50%;
  transform-style: preserve-3d;
  will-change: transform;
}
.warp-item {
  position: absolute;
  left: 0; top: 0;
  backface-visibility: hidden;
  transform-origin: center center;
  display: flex; align-items: center; justify-content: center;
  will-change: transform, opacity;
}

/* Category heading text — massive stroke type that POPS as
   it flies past. Inner faint chartreuse fill + bright stroke
   + chartreuse glow lifts the headings off the dark scene. */
.warp-heading {
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(96px, 9.4vw, 240px);
  color: rgba(242, 238, 229, 0.92);
  text-transform: uppercase;
  white-space: nowrap;
  transform: translate(-50%, -50%);
  pointer-events: none;
  letter-spacing: -0.03em;
  line-height: 0.82;
}

/* "Project Gallery" divider title in the warp flythrough — same heading font /
   size as the category headings (FRAMEWORKS, etc.), but a two-word title, so it
   stacks to two centred lines. It's slightly THICKER than the base headings via
   -webkit-text-stroke (Anton ships one weight, so stroke is how you add heft —
   same colour, so it just fattens each glyph), the two lines are pushed further
   apart with a looser line-height, and the tracking is opened a touch (base is
   -0.03em) so the thicker glyphs don't read as cramped. */
.warp-heading--pg {
  white-space: normal;       /* override base nowrap so the two words stack */
  text-align: center;        /* centre the two stacked lines */
  line-height: 1.0;          /* more air between PROJECT and GALLERY (base is 0.82) */
  letter-spacing: -0.01em;   /* a touch more character spacing than the base -0.03em */
  -webkit-text-stroke: 0.022em currentColor;  /* thicken the glyphs (no heavier Anton weight) */
}

/* Project-gallery first-frame "cards" that fly in behind the last skill card
   (same warp flythrough). Image fills a 16:9 frame with the same dark chrome. */
.warp-frame {
  width: clamp(300px, 28vw, 430px);
  aspect-ratio: 16 / 9;
  background-size: cover;
  background-position: center;
  border: 1px solid rgba(242, 238, 229, 0.12);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 20px 50px rgba(0, 0, 0, 0.55);
  transform: translate(-50%, -50%);
  pointer-events: none;
}

/* Skill card */
.warp-card {
  width: clamp(260px, 24vw, 340px);
  height: clamp(360px, 32vw, 460px);
  background: rgba(18, 18, 14, 0.55);
  border: 1px solid rgba(242, 238, 229, 0.10);
  padding: clamp(18px, 1.8vw, 28px);
  display: flex; flex-direction: column;
  justify-content: space-between;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.6),
    0 20px 50px rgba(0, 0, 0, 0.5);
  transform: translate(-50%, -50%);
  transition: border-color .35s ease, background .35s ease, box-shadow .35s ease;
  position: relative;
}
.warp-card::before,
.warp-card::after {
  content: '';
  position: absolute;
  width: 12px; height: 12px;
  border: 1px solid var(--cream);
  transition: 0.35s;
}
.warp-card::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
.warp-card::after { bottom: -1px; right: -1px; border-left: none; border-top: none; }
@media (hover: hover) {
  .warp-card:hover {
    border-color: var(--accent);
    background: rgba(28, 28, 22, 0.78);
    box-shadow: 0 0 40px rgba(216, 255, 56, 0.18);
    z-index: 100;
  }
  .warp-card:hover::before,
  .warp-card:hover::after {
    width: 100%; height: 100%;
    border-color: var(--accent);
  }
}
.warp-card__head {
  border-bottom: 1px solid rgba(242, 238, 229, 0.10);
  padding-bottom: 14px;
  display: flex; justify-content: space-between; align-items: center;
}
.warp-card__cat {
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.22em;
  color: var(--accent);
}
.warp-card__dot {
  width: 10px; height: 10px;
  background: var(--accent);
  display: block;
}
.warp-card h3 {
  margin: clamp(16px, 2vw, 28px) 0 0;
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(28px, 1.6vw, 40px);
  line-height: 0.92;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  color: var(--cream);
}
.warp-card__sub {
  margin-top: 12px;
  font-family: var(--f-serif);
  font-style: italic;
  font-size: clamp(10px, 0.59vw, 15px);
  color: rgba(242, 238, 229, 0.6);
  line-height: 1.35;
}
.warp-card__meta {
  margin-top: auto;
  display: flex; justify-content: space-between;
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  color: rgba(242, 238, 229, 0.45);
  padding-top: 14px;
  border-top: 1px solid rgba(242, 238, 229, 0.06);
}
.warp-card__num {
  position: absolute;
  bottom: 18px; right: 18px;
  font-family: var(--f-display);
  font-size: clamp(50px, 3vw, 76px);
  line-height: 1;
  color: rgba(242, 238, 229, 0.06);
  font-weight: 400;
  pointer-events: none;
}

/* Particle dot */
.warp-star {
  position: absolute;
  width: 2px; height: 2px;
  border-radius: 50%;
  background: rgba(242, 238, 229, 0.8);
  transform: translate(-50%, -50%);
  /* No will-change here: ~1300 stars × will-change = ~1300 permanent GPU
     layers (huge VRAM). Instead the compositor promotes only the visible,
     animating ones on demand (paintItems stops touching faded-out stars).
     No box-shadow either: a blurred glow would inflate every star's layer
     texture far past its pixel size — flat round dots keep it cheap. */
}

/* Scanlines + vignette overlays inside pin */
.warp__scanlines {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,0) 50%,
    rgba(0,0,0,0.18) 50%,
    rgba(0,0,0,0.18) 100%);
  background-size: 100% 4px;
  pointer-events: none;
  z-index: 15;
  mix-blend-mode: overlay;
  opacity: 0.5;
}
.warp__vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle, transparent 35%, #000 130%);
  z-index: 14;
  pointer-events: none;
}

/* =========================================================
   EXPERIENCE
   ========================================================= */
.experience {
  position: relative;
  padding: var(--section-y) 0;
  z-index: 1;
  /* Skip layout/paint while off-screen. `auto` intrinsic size remembers
     the real height after first render, so there's no scroll jump. */
  content-visibility: auto;
  contain-intrinsic-size: auto 900px;
}
.experience__list {
  padding: 0 var(--gutter);
  display: flex; flex-direction: column;
  gap: 24px;
}
.exp-card {
  display: grid;
  grid-template-columns: 40px 1fr;
  gap: 16px;
  padding: 32px 32px 32px 24px;
  background: var(--glass);
  backdrop-filter: blur(8px);
  border: 1px solid var(--glass-stroke);
  border-radius: 22px;
  transition: transform .5s var(--ease-out-quart), box-shadow .5s var(--ease-out-quart);
}
.exp-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 30px 60px -30px rgba(12,12,10,0.25);
}
.exp-card__rail {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  padding-top: 8px;
}
.exp-card__rail-dot {
  width: 14px; height: 14px;
  border-radius: 50%;
  background: var(--ink);
  box-shadow: 0 0 0 3px rgba(12,12,10,0.10);
  position: relative;
}
.exp-card__rail-line {
  flex: 1;
  width: 1px;
  background: linear-gradient(to bottom, var(--ink), transparent);
  margin-top: 4px;
}
.exp-card__head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 20px; flex-wrap: wrap;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--line-soft);
  margin-bottom: 18px;
}
.exp-card__company {
  display: flex; align-items: center; gap: 14px;
}
.exp-card__logo {
  width: 48px; height: 48px;
  display: grid; place-items: center;
  background: var(--ink); color: var(--accent);
  border-radius: 12px;
  font-family: var(--f-display);
  font-size: clamp(11px, 0.71vw, 18px);
  letter-spacing: 0.02em;
}
.exp-card__company h3 {
  margin: 0;
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(22px, 1.15vw, 28px);
  letter-spacing: 0.01em;
  color: var(--ink);
  line-height: 1;
}
.exp-card__loc {
  display: block;
  font-family: var(--f-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  color: var(--ink-mute);
  margin-top: 6px;
}
.exp-card__meta {
  display: flex; flex-direction: column; gap: 4px; text-align: right;
}
.exp-card__role {
  font-family: var(--f-mono);
  font-size: 11px; letter-spacing: 0.18em;
  color: var(--ink);
}
.exp-card__dates {
  font-family: var(--f-mono);
  font-size: 11px; letter-spacing: 0.18em;
  color: var(--ink-mute);
}
.exp-card__bullets {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14px 28px;
}
.exp-card__bullets li {
  position: relative;
  padding-left: 18px;
  font-family: var(--f-sans);
  font-size: clamp(10px, 0.57vw, 14.5px);
  line-height: 1.5;
  color: var(--ink);
}
.exp-card__bullets li::before {
  content: "";
  position: absolute;
  left: 0; top: 9px;
  width: 8px; height: 1.5px;
  background: var(--accent-deep);
}
.exp-card__bullets li b { font-weight: 600; color: var(--ink); }

/* =========================================================
   EXPERIENCE — TERMINAL (left) + TIMELINE (right)
   ========================================================= */
.xp {
  position: relative;
  padding: var(--section-y) 0;
  z-index: 1;
}
.xp__head {
  max-width: 1180px;
  margin: 0 auto clamp(40px, 6vh, 72px);
  padding: 0 var(--gutter);
}
.xp__eyebrow {
  display: inline-block;
  font-family: var(--f-mono);
  font-size: clamp(10px, 0.47vw, 12px);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.xp__lede {
  margin: 14px 0 0;
  max-width: 62ch;
  font-family: var(--f-sans);
  font-size: clamp(15.5px, 0.71vw, 18px);
  line-height: 1.55;
  color: var(--ink-mute);
}
/* Centered content block (terminal + timeline) so it reads balanced
   instead of pinned to the edges. */
.xp__grid {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: grid;
  grid-template-columns: minmax(0, 520px) minmax(0, 1fr);
  gap: clamp(32px, 5vw, 72px);
  align-items: start;
}

/* ---- Terminal ---- */
.term {
  width: 100%;
  max-width: 560px;
  font-family: var(--f-mono);
  font-size: clamp(10px, 0.53vw, 13.5px);
}
.term__win {
  overflow: hidden;
  border-radius: 14px;
  border: 1px solid #262626;
  background: #171717;
  box-shadow: 0 40px 90px -45px rgba(12, 12, 10, 0.55);
}
.term__bar {
  display: flex; align-items: center; gap: 8px;
  background: #262626;
  padding: 11px 14px;
}
.term__dot { width: 12px; height: 12px; border-radius: 50%; flex: none; }
.term__dot--r { background: #ef4444; }
.term__dot--y { background: #eab308; }
.term__dot--g { background: #22c55e; }
.term__title { flex: 1; text-align: center; font-size: clamp(10px, 0.47vw, 12px); color: #a3a3a3; }
.term__bar-spacer { width: 52px; flex: none; }
.term__screen {
  height: 320px;
  overflow-y: auto;
  padding: 16px;
  line-height: 1.65;
  white-space: pre-wrap;
  word-break: break-word;
  color: #d4d4d4;
  scrollbar-width: none;
}
.term__screen::-webkit-scrollbar { width: 0; height: 0; }
.term__line { min-height: 1.65em; }
/* prompt segments */
.term__u { color: #0ea5e9; }
.term__c { color: #059669; }
.term__tilde { color: #38bdf8; }
.term__d { color: #737373; }
.term__out { color: #a3a3a3; }
/* bash token colors */
.t-command { color: #34d399; }
.t-flag { color: #38bdf8; }
.t-string { color: #fcd34d; }
.t-number { color: #c084fc; }
.t-operator { color: #f87171; }
.t-path { color: #67e8f9; }
.t-variable { color: #f472b6; }
.t-comment { color: #737373; }
.t-default { color: #d4d4d4; }
.term__cursor {
  display: inline-block;
  width: 8px; height: 1em;
  background: #d4d4d4;
  vertical-align: text-bottom;
  margin-left: 1px;
  animation: termBlink 1.06s steps(1, end) infinite;
}
@keyframes termBlink { 50% { opacity: 0; } }
body.perf-lite .term__cursor { animation: none; }

/* ---- Timeline ---- */
.tl {
  position: relative;
  display: flex; flex-direction: column;
  gap: clamp(34px, 4.5vw, 56px);
  padding-left: 30px;
}
.tl::before {
  content: "";
  position: absolute;
  left: 6px; top: 6px; bottom: 6px;
  width: 1px;
  background: linear-gradient(to bottom, var(--ink), var(--line-soft));
}
.tl__item { position: relative; }
.tl__dot {
  position: absolute;
  left: -30px; top: 4px;
  width: 13px; height: 13px;
  border-radius: 50%;
  background: var(--ink);
  box-shadow: 0 0 0 3px var(--cream), 0 0 0 4px var(--line-soft);
}
/* Current / in-flight role — accent node. */
.tl__dot--live {
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--cream), 0 0 0 4px var(--accent-deep);
}
.tl__when {
  font-family: var(--f-mono);
  font-size: clamp(10px, 0.49vw, 12.5px); letter-spacing: 0.18em;
  color: var(--ink-mute);
}
.tl__role {
  margin: 9px 0 7px;
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(26px, 1.45vw, 37px);
  letter-spacing: 0.01em;
  line-height: 1;
  color: var(--ink);
}
.tl__org {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.tl__logo {
  width: 36px; height: 36px; flex: none;
  display: grid; place-items: center;
  background: var(--ink); color: var(--accent);
  border-radius: 10px;
  font-family: var(--f-display);
  font-size: clamp(10px, 0.55vw, 14px); letter-spacing: 0.02em;
}
.tl__org-name {
  font-family: var(--f-sans);
  font-size: clamp(16.5px, 0.79vw, 20px);
  font-weight: 500;
  color: var(--ink);
}
.tl__loc {
  width: 100%;
  font-family: var(--f-mono);
  font-size: 11.5px; letter-spacing: 0.14em;
  color: var(--ink-mute);
}
.tl__points { display: flex; flex-direction: column; gap: 10px; }
.tl__points li {
  position: relative;
  padding-left: 20px;
  font-family: var(--f-sans);
  font-size: clamp(10px, 0.63vw, 16px);
  line-height: 1.55;
  color: var(--ink-2);
}
.tl__points li::before {
  content: "";
  position: absolute;
  left: 0; top: 9px;
  width: 8px; height: 1.5px;
  background: var(--accent-deep);
}
.tl__points li b { font-weight: 600; color: var(--ink); }

@media (max-width: 900px) {
  .xp__grid { grid-template-columns: 1fr; gap: 48px; }
  .xp__left { position: static; }
  .term { max-width: none; }
}

/* ---- Dark-mode veil + experience-section dark theme ----
   The veil's opacity is scroll-driven, so the page fades light → dark as the
   experience section scrolls in and back to light as it leaves. The section's
   own content is styled light-on-dark to read against the veil. */
/* Timeline scroll-in animation (armed by JS · initTimeline): the rail draws
   downward and each entry + node staggers in. */
.tl--anim::before {
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform 1s var(--ease-out-quart);
}
.tl--anim.is-active::before { transform: scaleY(1); }
.tl__item:nth-child(1) { --i: 0; }
.tl__item:nth-child(2) { --i: 1; }
.tl__item:nth-child(3) { --i: 2; }
.tl--anim .tl__item {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.7s var(--ease-out-quart), transform 0.7s var(--ease-out-quart);
  transition-delay: calc(var(--i, 0) * 0.14s);
}
.tl--anim.is-active .tl__item { opacity: 1; transform: none; }
.tl--anim .tl__dot {
  transform: scale(0);
  transition: transform 0.5s var(--ease-out-quart);
  transition-delay: calc(var(--i, 0) * 0.14s + 0.15s);
}
.tl--anim.is-active .tl__dot { transform: scale(1); }
@media (prefers-reduced-motion: reduce) {
  .tl--anim .tl__item, .tl--anim .tl__dot, .tl--anim::before {
    transition: none; opacity: 1; transform: none;
  }
}

/* =========================================================
   PROJECTS
   ========================================================= */
.projects {
  position: relative;
  padding: var(--section-y) 0;
  z-index: 1;
}
.projects__list {
  padding: 0 var(--gutter);
  display: flex; flex-direction: column;
  gap: 14px;
}
.proj {
  position: relative;
  border: 1px solid var(--glass-stroke);
  border-radius: 24px;
  background: var(--glass);
  backdrop-filter: blur(8px);
  overflow: hidden;
  transition: transform .5s var(--ease-out-quart), border-color .3s ease, background .5s ease;
}
.proj:hover {
  transform: translateY(-3px);
  border-color: var(--ink);
  background: rgba(255,255,255,0.7);
}
.proj__inner {
  display: grid;
  grid-template-columns: 100px 1fr 200px;
  gap: 30px;
  padding: 32px;
  align-items: center;
}
.proj__index {
  display: flex; flex-direction: column; gap: 8px;
  font-family: var(--f-display);
  font-size: clamp(30px, 1.88vw, 48px);
  line-height: 1;
  color: var(--ink);
}
.proj__status {
  font-family: var(--f-mono);
  font-size: 9px;
  letter-spacing: 0.2em;
  color: var(--ink-mute);
  padding: 4px 8px;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  align-self: flex-start;
  width: max-content;
}
.proj__status--live {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: var(--accent-deep);
  animation: livePulse 2s ease-in-out infinite;
}
@keyframes livePulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(216, 255, 56, 0.5); }
  50%      { box-shadow: 0 0 0 6px rgba(216, 255, 56, 0); }
}
.proj__body { display: flex; flex-direction: column; gap: 10px; }
.proj__meta {
  display: flex; flex-wrap: wrap; gap: 8px;
  font-family: var(--f-mono);
  font-size: 10px; letter-spacing: 0.18em;
  color: var(--ink-mute);
}
.proj__title {
  margin: 0;
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(28px, 1.72vw, 44px);
  letter-spacing: 0.005em;
  line-height: 1;
  color: var(--ink);
}
.proj__title .italic {
  font-family: var(--f-serif);
  font-style: italic;
  font-weight: 400;
  font-size: 0.7em;
  color: var(--ink-mute);
  letter-spacing: -0.005em;
}
.proj__desc {
  margin: 4px 0 0;
  font-family: var(--f-sans);
  font-size: clamp(10px, 0.57vw, 14.5px);
  line-height: 1.55;
  color: var(--ink-mute);
  max-width: 70ch;
}
.proj__desc b { color: var(--ink); font-weight: 600; }
.proj__tags {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin-top: 4px;
}
.proj__tags li {
  padding: 4px 8px;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--ink);
}
.proj__cta {
  display: flex; align-items: center; gap: 12px;
  justify-content: flex-end;
  font-family: var(--f-mono);
  font-size: 11px;
  letter-spacing: 0.2em;
  color: var(--ink);
}
.proj__cta svg {
  width: 32px; height: 12px;
  transition: transform .4s var(--ease-out-quart);
}
.proj:hover .proj__cta svg { transform: translateX(6px); }

.proj--featured { background: var(--ink); border-color: var(--ink); }
.proj--featured .proj__index,
.proj--featured .proj__title,
.proj--featured .proj__cta { color: var(--cream); }
.proj--featured .proj__title .italic { color: rgba(242, 238, 229, 0.5); }
.proj--featured .proj__desc { color: rgba(242, 238, 229, 0.65); }
.proj--featured .proj__desc b { color: var(--cream); }
.proj--featured .proj__meta { color: rgba(242, 238, 229, 0.5); }
.proj--featured .proj__tags li { border-color: rgba(242, 238, 229, 0.18); color: var(--cream); }
.proj--featured:hover { background: #0c0c0a; border-color: var(--accent); }

/* =========================================================
   CONTACT
   ========================================================= */
.contact {
  position: relative;
  padding: var(--section-y) var(--gutter) 0;
  z-index: 1;
  content-visibility: auto;
  contain-intrinsic-size: auto 700px;
}
.contact__inner {
  max-width: 640px;
  margin: 0 auto;
  padding-bottom: clamp(80px, 12vh, 160px);
  position: relative;
}
/* Desktop: the weave canvas is viewport-centred but the knot reads centre /
   right-of-centre. Left-anchor the copy with a left margin that mirrors the weave's
   right-side gap, so the text and the knot look equidistant from the two edges, and
   cap the width so the lede never runs into the knot. Below 1024px the copy is simply
   centred (the faint multiply-blend weave sits behind it). */
@media (min-width: 1024px) {
  .contact__inner {
    /* Copy stays in the LEFT ~third→half; its right edge stops before the knot's
       ~50vw left edge (gap absorbs the knot's rotation). */
    max-width: min(37vw, 960px);
    margin: 0;
    /* Total left margin = section --gutter padding + this ≈ 10vw, mirroring the
       knot's ~10vw right-side gap so text + knot read equidistant from the edges. */
    margin-left: max(0px, calc(10vw - var(--gutter)));
  }
}
/* Woven torus-knot backdrop. Fixed + centred in the viewport so the knot is
   always fully visible and never clipped by the section edges. Fades in with
   the section; click-through; kept under the copy (z-index 0). */
.contact__weave {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1.3s ease;
  mix-blend-mode: multiply;   /* graphite threads settle into the cream */
}
.contact__weave.is-live { opacity: 1; }
/* Keep all the real content above the weave. */
.contact__meta,
.contact__headline,
.contact__lede,
.contact__email,
.contact__socials,
.footer { position: relative; z-index: 1; }
.contact__meta {
  display: flex; gap: 12px; align-items: center;
  margin-bottom: 30px;
  font-family: var(--f-mono);
  font-size: 11px; letter-spacing: 0.22em;
  color: var(--ink-mute);
  /* Light two-layer lift on the mono eyebrow (matches the landing's shadow
     language — warm-to-cool, em-scaled). */
  text-shadow:
    0 0.04em 0.08em rgba(26, 20, 10, 0.28),
    0 0.1em 0.18em rgba(20, 16, 10, 0.16);
}
.contact__meta .contact__num {
  padding: 4px 10px;
  border: 1px solid var(--ink);
  border-radius: 4px;
  color: var(--ink);
}

.contact__headline {
  margin: 0 0 30px;
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(60px, 7.85vw, 200px);
  line-height: 0.86;
  letter-spacing: -0.02em;
  color: var(--ink);
  /* Premium graduated cast shadow — crisp contact + soft mid + wide diffuse
     ambient, warm-to-cool tint, em-scaled to the huge type. Inherits to the
     italic "something" + "good." for one cohesive lift off the cream. */
  text-shadow:
    0 0.01em 0.01em rgba(28, 21, 11, 0.42),
    0 0.045em 0.06em rgba(24, 18, 10, 0.28),
    0 0.13em 0.22em rgba(20, 16, 10, 0.18);
  display: flex; flex-direction: column;
  gap: 0;
}
.contact__headline .italic {
  font-family: var(--f-serif);
  font-style: italic;
  font-weight: 400;
  font-size: 0.65em;
  color: var(--ink-mute);
  padding-left: 0.1em;
}
.contact__headline-accent {
  position: relative;
  display: inline-block;
  width: max-content;
}
.contact__underline {
  position: absolute;
  bottom: -0.1em; left: 0;
  width: 100%; height: 0.12em;
  color: var(--accent-deep);
  pointer-events: none;
}

.contact__lede {
  max-width: 56ch;
  margin: 0 0 40px;
  font-family: var(--f-sans);
  font-size: clamp(16px, 0.75vw, 19px);
  line-height: 1.55;
  color: var(--ink-mute);
  /* Soft two-layer lift, tuned to stay legible at body size. */
  text-shadow:
    0 0.02em 0.03em rgba(28, 21, 11, 0.24),
    0 0.06em 0.12em rgba(20, 16, 10, 0.14);
}

.contact__email {
  display: inline-flex; align-items: center; gap: 24px;
  padding: 22px 28px;
  background: var(--ink);
  color: var(--cream);
  border-radius: 999px;
  transition: transform .4s var(--ease-out-quart), background .3s ease;
  margin-bottom: 50px;
  /* Premium layered lift on the raised pill — tight contact through wide
     ambient so it floats convincingly off the cream. */
  box-shadow:
    0 2px 4px rgba(12, 10, 6, 0.28),
    0 8px 16px rgba(12, 10, 6, 0.22),
    0 22px 44px -12px rgba(12, 10, 6, 0.4),
    0 40px 70px -30px rgba(12, 10, 6, 0.55);
}
.contact__email:hover { transform: translateY(-3px); background: #000; }
.contact__email-label {
  font-family: var(--f-mono);
  font-size: 11px; letter-spacing: 0.22em;
  color: var(--accent);
}
.contact__email-addr {
  font-family: var(--f-display);
  font-size: clamp(20px, 1.15vw, 28px);
  letter-spacing: 0.02em;
}
.contact__email svg { width: 32px; height: 16px; }

.contact__socials {
  display: flex; flex-wrap: wrap; gap: 8px 24px;
}
.contact__socials a {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 0;
  font-family: var(--f-mono);
  font-size: clamp(10px, 0.47vw, 12px);
  letter-spacing: 0.16em;
  color: var(--ink);
  border-bottom: 1px solid var(--line-soft);
  transition: color .25s ease, border-color .25s ease;
  text-shadow:
    0 0.04em 0.08em rgba(26, 20, 10, 0.28),
    0 0.1em 0.18em rgba(20, 16, 10, 0.16);
}
.contact__socials a:hover { color: var(--accent-deep); border-color: var(--accent-deep); }

/* footer */
.footer {
  padding: 30px 0 30px;
  border-top: 1px solid var(--line-soft);
  display: grid; grid-template-columns: 1fr 1fr 1fr;
  align-items: center;
  content-visibility: auto;
  contain-intrinsic-size: auto 120px;
  font-family: var(--f-mono);
  font-size: 10px; letter-spacing: 0.22em;
  color: var(--ink-mute);
  text-shadow:
    0 0.04em 0.08em rgba(26, 20, 10, 0.26),
    0 0.1em 0.18em rgba(20, 16, 10, 0.15);
}
.footer__col { display: flex; align-items: center; gap: 8px; }
.footer__col--center { justify-content: center; }
.footer__col--right { justify-content: flex-end; }
.footer__dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent-deep);
  box-shadow: 0 0 0 3px rgba(180, 221, 24, 0.2);
}

/* =========================================================
   REVEAL ANIMATIONS
   ========================================================= */
.reveal {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity .9s var(--ease-out-quart), transform .9s var(--ease-out-quart);
}
.reveal.in-view {
  opacity: 1;
  transform: translateY(0);
}

/* Pure-CSS hero entrance (no JS dependency, reliable everywhere) */
.reveal-stagger {
  opacity: 0;
  transform: translateY(0.7em);
  animation: heroIn 1.1s var(--ease-out-quart) forwards;
}
.hero__title-row--top .reveal-stagger { animation-delay: 120ms; }
.hero__title-row--bottom .hero__title-italic.reveal-stagger { animation-delay: 260ms; }
.hero__title-row--bottom .hero__title-word.reveal-stagger { animation-delay: 380ms; }
@keyframes heroIn {
  to { opacity: 1; transform: translateY(0); }
}

/* =========================================================
   RESPONSIVE
   ========================================================= */
@media (max-width: 1100px) {
  .highlights__grid { grid-template-columns: repeat(2, 1fr); }
  .exp-card__bullets { grid-template-columns: 1fr; }
  .proj__inner { grid-template-columns: 80px 1fr; }
  .proj__cta { grid-column: 2; justify-content: flex-start; padding-top: 4px; }
}

@media (max-width: 820px) {
  :root { --gutter: 18px; }

  .nav {
    padding: 12px 14px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
  }
  .nav__menu { display: none; }
  .nav__monogram { display: none; }
  .nav__brand { font-size: 11px; letter-spacing: 0.04em; flex-shrink: 0; }
  .nav__cta {
    font-size: 10px;
    padding: 7px 12px 7px 10px;
    letter-spacing: 0.12em;
    gap: 6px;
    flex-shrink: 0;
  }
  .nav__cta-dot { width: 6px; height: 6px; }

  .hero-stage { height: 240vh; }
  .hero-stage__ui { padding: 90px 14px 20px; }
  .hero__frame { inset: 70px 12px 16px; border-radius: 14px; }
  .hero__eyebrow { display: none; }
  .hero__title-row--bottom { justify-content: flex-start; }
  .hero__title-italic { width: 100%; order: 2; padding-bottom: 6px; }
  .hero__title-word { font-size: clamp(48px, 18vw, 110px); }
  .hero__status {
    display: none;
  }
  .hero__tagline {
    position: relative;
    left: auto; bottom: auto;
    max-width: 100%;
    margin-top: 20px;
    font-size: 13px;
  }
  .hero__bottom {
    grid-template-columns: 1fr;
    gap: 10px;
  }
  .hero__card--next, .hero__card--role { max-width: 100%; justify-self: stretch; }
  .hero__card-role { text-align: left; }
  .hero__scroll { display: none; }

  .cinema__hud--bl, .cinema__hud--br { display: none; }
  .cinema__captions { bottom: 14%; }

  .section-head { grid-template-columns: 1fr; gap: 14px; }
  .section-head__lede { grid-column: 1; }

  .highlights__grid { grid-template-columns: 1fr 1fr; gap: 12px; }

  .warp { height: 800vh; }   /* match desktop flythrough length (incl. expand tail) */
  .warp-heading { font-size: clamp(76px, 28vw, 220px); }
  .warp-heading--pg { font-size: clamp(80px, 22vw, 170px); }
  .warp-card { width: min(280px, 86vw); height: clamp(340px, 70vh, 460px); }
  .warp__hud { font-size: 9px; inset: 14px; }

  .exp-card { grid-template-columns: 1fr; padding: 24px; }
  .exp-card__rail { display: none; }
  .exp-card__head { flex-direction: column; align-items: flex-start; }
  .exp-card__meta { text-align: left; }

  .proj__inner { grid-template-columns: 1fr; padding: 24px; gap: 18px; }
  .proj__cta { justify-content: flex-start; }

  .contact__email { flex-direction: column; align-items: flex-start; gap: 8px; padding: 18px 22px; border-radius: 22px; }

  .footer { grid-template-columns: 1fr; gap: 8px; text-align: center; }
  .footer__col, .footer__col--right, .footer__col--center { justify-content: center; }
}

/* reduced motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .topo-bg { animation: none; }
}

/* =========================================================
   DARK THEME — "lights off" as you scroll past the gallery
   into the experience section. JS (initThemeShift) toggles
   html.is-dark at that boundary; the palette variables flip and
   the always-visible backdrop + experience/contact text fade
   over a short, smooth transition (the cream/ink consumers
   recolor automatically through their var() references).
   ========================================================= */
html.is-dark {
  --cream:   #0C0E12;   /* page surface → near-black */
  --cream-2: #14171D;
  --cream-3: #1E222A;
  --ink:     #ECE7DA;   /* text → warm off-white */
  --ink-2:   #CBC5B7;
  --ink-mute:#928B7C;
  --line:    #39414B;
  --line-soft:  rgba(236, 231, 218, 0.12);
  --line-faint: rgba(236, 231, 218, 0.06);
  --glass:        rgba(22, 25, 31, 0.55);
  --glass-stroke: rgba(236, 231, 218, 0.10);
}
/* Contour lines are dark-on-cream by default; flip to light-on-dark
   so they stay visible once the page goes dark. */
html.is-dark .topo-bg { color: rgba(236, 231, 218, 0.14); }

/* The project-gallery slide text uses hardcoded near-black colours (it was
   only ever shown on cream). When the lights go off during the gallery →
   experience hand-off the gallery is still on screen, so those darks vanish.
   Flip them to light here — only while is-dark is active, so the gallery
   reads normally (dark-on-cream) the rest of the time. */
html.is-dark .hall-slide__desc { color: rgba(236, 231, 218, 0.72); }
html.is-dark .hall-slide__num,
html.is-dark .hall-slide__meta-label,
html.is-dark .hall__hint { color: rgba(236, 231, 218, 0.5); }
html.is-dark .hall-slide__num { border-bottom-color: rgba(236, 231, 218, 0.18); }
/* Hide the scroll-hint once the lights are off — it reads as clutter against
   the dark hand-off (the light gallery keeps it). */
html.is-dark .hall__hint { opacity: 0; }
html.is-dark .hall-slide__meta { border-top-color: rgba(236, 231, 218, 0.14); }
/* Idle highlight words: the dark forest green is low-contrast on black, but
   the full neon accent glares against the muted body — use a softer, slightly
   desaturated lime that reads as the accent without overpowering the text. */
html.is-dark .hyper-word.is-hl { color: #A8BE54; }
/* Hovered (decoding) word in dark mode: flip to crisp black-on-white. The
   idle-green rule above is more specific than the base hover rule, so re-assert
   the hover colour + a pure-white pill here with matching dark-scoped weight. */
html.is-dark .is-hyper-hover .hyper-word.is-hovered { color: #0a0a0a; }
html.is-dark .is-hyper-hover .hyper-word.is-hovered::before { background: #fff; }

/* The fade itself. Only the layers that are actually on screen during
   (and after) the flip get a transition — the rest sit off-screen and
   never reveal a snap. Elements that already transition their own
   color/background (contact email pill, social links) are left alone
   so their existing easings are preserved. */
/* Slower + staggered so the change cascades with depth: the background dips
   first, the contour lines follow, then the text — read as one slow wave
   rather than a flat simultaneous flip. The accent scan blade (.theme-sweep)
   rides across on top of this. */
html, body { transition: background-color .95s var(--ease-soft), color .95s var(--ease-soft); }
.topo-bg { transition: color 1s var(--ease-soft) .12s; }
.xp__eyebrow, .xp__lede,
.tl__when, .tl__role, .tl__org-name, .tl__loc,
.tl__points li, .tl__points li b,
.contact__meta, .contact__num,
.contact__headline, .contact__headline .italic,
.contact__lede {
  transition: background-color .9s var(--ease-soft) .22s, color .9s var(--ease-soft) .22s, border-color .9s var(--ease-soft) .22s;
}

/* =========================================================
   THEME SWEEP — the "lights changing" scan blade.
   A thin glowing accent blade sweeps vertically across the screen each time
   the page flips light↔dark (fired by GSAP in initThemeShift), so the change
   reads as a wave of energy passing through rather than a flat crossfade.
   Pure transform/opacity, one shot per crossing. */
.theme-sweep {
  position: fixed;
  left: -10vw; right: -10vw;
  top: 0;
  height: 100vh;
  z-index: 210;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
  background: linear-gradient(to bottom,
    transparent 43%,
    rgba(216, 255, 56, 0.04) 47.5%,
    rgba(216, 255, 56, 0.90) 50%,
    rgba(216, 255, 56, 0.04) 52.5%,
    transparent 57%);
  filter: drop-shadow(0 0 22px rgba(216, 255, 56, 0.55));
}
@media (prefers-reduced-motion: reduce) {
  .theme-sweep { display: none; }
}

/* =========================================================
   EXPERIENCE FRAME — scroll-drawn blueprint border
   A thin frame draws itself around the experience section as you
   scroll into it: two symmetric strokes start at the top centre, run
   out to the corners, race down both sides and meet/close along the
   bottom — so the line literally travels corner-to-corner with the
   scroll. Accent brackets snap on as each corner is reached and a
   small spark leads each drawing head. Dark-mode only (the section is
   dark by the time it frames). JS · initXpFrame drives the draw.
   ========================================================= */
.xp__frame {
  position: fixed;
  top: clamp(16px, 3vh, 40px);
  bottom: clamp(16px, 3vh, 40px);
  left: clamp(16px, 3.5vw, 64px);
  right: clamp(16px, 3.5vw, 64px);
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  transition: opacity .55s var(--ease-soft);
}
/* Shown only while the experience section is in view AND the page is dark,
   so the line frames the dark "page" (never the light sections above). */
html.is-dark .xp__frame.xp__frame--active { opacity: 1; }
.xp__frame-svg { display: block; width: 100%; height: 100%; overflow: visible; }
.xp__frame-path {
  stroke: rgba(236, 231, 218, 0.92);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
  /* Hidden until JS measures each path and sets the real dash length +
     offset (initThemeShift sibling · initXpFrame). Big defaults just keep
     it hidden before that first measure, with no flash. */
  stroke-dasharray: 4000;
  stroke-dashoffset: 4000;
  filter: drop-shadow(0 0 7px rgba(216, 255, 56, 0.18));
}

/* Accent L-brackets at the four corners — lit as the draw reaches them. */
.xp__frame-corner {
  position: absolute;
  width: 16px; height: 16px;
  border: 1.5px solid var(--accent);
  opacity: 0;
  transform: scale(.55);
  transition: opacity .35s ease, transform .4s var(--ease-out-quart);
}
.xp__frame-corner.is-lit { opacity: .92; transform: scale(1); }
.xp__frame-corner--tl { top: -2px; left: -2px;  border-right: 0; border-bottom: 0; transform-origin: top left; }
.xp__frame-corner--tr { top: -2px; right: -2px; border-left: 0;  border-bottom: 0; transform-origin: top right; }
.xp__frame-corner--br { bottom: -2px; right: -2px; border-left: 0; border-top: 0;  transform-origin: bottom right; }
.xp__frame-corner--bl { bottom: -2px; left: -2px;  border-right: 0; border-top: 0;  transform-origin: bottom left; }
.xp__frame-corner.is-lit { transform: scale(1); }

/* Leading spark at each drawing head — positioned by JS in % of the box. */
.xp__frame-spark {
  position: absolute;
  width: 7px; height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px 2px rgba(216, 255, 56, 0.55);
  opacity: 0;
  transition: opacity .25s ease;
}
.xp__frame.is-drawing .xp__frame-spark { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .xp__frame-spark { display: none; }
}

/* =========================================================
   EXPERIENCE — CINEMATIC NEURAL TIMELINE (rebuilt)
   A diagonal zigzag "live wire" with flowing energy, floating
   neon orb-nodes, glass cards, atmospheric depth + a particle
   field. Dark / cyberpunk; orchestrated by JS (initXpTimeline).
   Tokens local to this section so it reads the same regardless
   of the page palette flip.
   ========================================================= */
.xpx {
  --xn: #D8FF38;                 /* primary neon */
  --xn-2: #6cf0ff;               /* cool accent for duotone depth */
  --xn-dim: rgba(216,255,56,.32);
  --xglass: rgba(20,26,30,.42);
  --xstroke: rgba(216,255,56,.16);
  --xtext: rgba(236,231,218,.92);
  --xmute: rgba(236,231,218,.52);
  position: relative;
  max-width: 1240px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  perspective: 1600px;
}

/* ---------- Sleek head ---------- */
.xpx__head { text-align: center; max-width: 760px; }
.xpx__head { margin-left: auto; margin-right: auto; }
.xpx__title {
  margin: 14px 0 0;
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(40px, 3.6vw, 92px);
  line-height: .92;
  letter-spacing: -0.01em;
  color: var(--xtext, #ECE7DA);
  text-transform: uppercase;
}
.xpx__title em {
  font-style: normal;
  color: var(--xn, #D8FF38);
  text-shadow: 0 0 26px rgba(216,255,56,.55);
}

/* ---------- Atmosphere ---------- */
.xpx__atmos {
  position: absolute;
  inset: -6% -8%;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  border-radius: 28px;
}
.xpx__grid {
  position: absolute; inset: 0;
  background-image:
    linear-gradient(rgba(216,255,56,.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(216,255,56,.05) 1px, transparent 1px);
  background-size: 64px 64px;
  -webkit-mask-image: radial-gradient(120% 80% at 50% 42%, #000 30%, transparent 78%);
          mask-image: radial-gradient(120% 80% at 50% 42%, #000 30%, transparent 78%);
  transform: translate3d(var(--px,0), var(--py,0), 0);
  opacity: .5;
}
.xpx__blob {
  position: absolute;
  width: 56vw; height: 56vw;
  max-width: 760px; max-height: 760px;
  border-radius: 50%;
  filter: blur(70px);
  opacity: .55;
  will-change: transform;
}
.xpx__blob--1 {
  top: -8%; left: 8%;
  background: radial-gradient(circle, rgba(216,255,56,.20), transparent 62%);
  animation: xpxDrift1 26s ease-in-out infinite alternate;
  transform: translate3d(calc(var(--px,0) * 2), calc(var(--py,0) * 2), 0);
}
.xpx__blob--2 {
  bottom: -12%; right: 4%;
  background: radial-gradient(circle, rgba(108,240,255,.13), transparent 64%);
  animation: xpxDrift2 32s ease-in-out infinite alternate;
  transform: translate3d(calc(var(--px,0) * -2.4), calc(var(--py,0) * -1.6), 0);
}
@keyframes xpxDrift1 { to { transform: translate3d(6%, 5%, 0) scale(1.12); } }
@keyframes xpxDrift2 { to { transform: translate3d(-5%, -6%, 0) scale(1.15); } }
.xpx__particles { position: absolute; inset: 0; width: 100%; height: 100%; opacity: .7; }
.xpx__scan {
  position: absolute; inset: 0;
  background: repeating-linear-gradient(to bottom,
    rgba(255,255,255,.022) 0px, rgba(255,255,255,.022) 1px,
    transparent 2px, transparent 4px);
  mix-blend-mode: overlay;
  opacity: .5;
  animation: xpxScan 8s linear infinite;
}
@keyframes xpxScan { to { background-position: 0 240px; } }
.xpx__cursor {
  position: absolute;
  width: 520px; height: 520px;
  left: var(--mx, 50%); top: var(--my, 40%);
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(216,255,56,.10), transparent 60%);
  opacity: 0;
  transition: opacity .5s ease;
}
.xpx[data-hot] .xpx__cursor { opacity: 1; }
body.perf-lite .xpx__blob,
body.perf-lite .xpx__scan { animation: none; }
body.perf-lite .xpx__particles { display: none; }

/* ---------- The rail ---------- */
.xpx__rail {
  position: relative;
  z-index: 1;
  height: clamp(980px, 128vh, 1320px);
  margin-top: clamp(40px, 6vh, 76px);
  transform: rotateX(8deg) rotateZ(-1.2deg);
  transform-style: preserve-3d;
}
@media (max-width: 900px) { .xpx__rail { transform: none; } }

.xpx__line { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.xpx__line-base {
  stroke: rgba(216,255,56,.22);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 6px rgba(216,255,56,.25));
}
.xpx__line-flow {
  stroke: var(--xn, #D8FF38);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 10px rgba(216,255,56,.9));
  animation: xpxFlow 2.4s linear infinite;
}
@keyframes xpxFlow { to { stroke-dashoffset: -120; } }
body.perf-lite .xpx__line-flow { animation: none; }

/* ---------- Nodes ---------- */
.xpn {
  position: absolute;
  left: var(--x); top: var(--y);
  width: 0; height: 0;
  z-index: 2;
}
.xpn__orb {
  position: absolute;
  left: 0; top: 0;
  /* Centre the 26px orb on the node point via margin, so `transform` is free
     for scale only (no translate to fight GSAP / the proximity var). */
  margin: -13px 0 0 -13px;
  width: 26px; height: 26px;
  --orb: 1; --prox: 0;
  transform: scale(calc(var(--orb) + var(--prox) * 0.5));
  transition: transform .45s cubic-bezier(.22,1,.36,1);
}
.xpn__orb i {  /* core */
  position: absolute; inset: 6px;
  border-radius: 50%;
  background: radial-gradient(circle at 38% 34%, #fff, var(--xn) 58%, #9bc400 100%);
  box-shadow: 0 0 16px 2px rgba(216,255,56,.8), 0 0 40px 6px rgba(216,255,56,.35);
}
.xpn__orb b {  /* halo ring */
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 1px solid rgba(216,255,56,.5);
  animation: xpxPulse 2.8s ease-out infinite;
}
@keyframes xpxPulse {
  0%   { transform: scale(1);   opacity: .8; }
  100% { transform: scale(2.6); opacity: 0; }
}
.xpn__orb::after { /* float wrapper handled via animation on orb */ }
.xpn { animation: xpxFloat 6s ease-in-out infinite; animation-delay: calc(var(--i) * -2s); }
@keyframes xpxFloat { 50% { transform: translateY(-10px); } }
body.perf-lite .xpn { animation: none; }
body.perf-lite .xpn__orb b { animation: none; opacity: .35; }

/* glow line from orb to card */
.xpn__card {
  position: absolute;
  top: 50%;
  width: clamp(300px, 30vw, 408px);
  padding: 22px 24px;
  border-radius: 16px;
  background: var(--xglass);
  border: 1px solid var(--xstroke);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
          backdrop-filter: blur(14px) saturate(1.1);
  box-shadow:
    0 30px 70px -30px rgba(0,0,0,.7),
    inset 0 1px 0 rgba(255,255,255,.06),
    0 0 0 0 rgba(216,255,56,0);
  transform: translateY(-50%) translateZ(0);
  transition: transform .5s cubic-bezier(.22,1,.36,1),
              box-shadow .5s ease, border-color .5s ease, opacity .6s ease;
}
.xpn--right .xpn__card { left: 30px; }
.xpn--left  .xpn__card { right: 30px; }
/* a faint connector from the orb into the card */
.xpn__card::before {
  content: "";
  position: absolute; top: 50%;
  width: 30px; height: 1px;
  background: linear-gradient(90deg, var(--xn), transparent);
}
.xpn--right .xpn__card::before { left: -30px; }
.xpn--left  .xpn__card::before { right: -30px; transform: scaleX(-1); }

.xpn__tag {
  position: absolute; top: -10px;
  right: 18px;
  font-family: var(--f-mono);
  font-size: 9.5px; letter-spacing: .2em;
  padding: 3px 8px;
  color: #10160a;
  background: var(--xn);
  border-radius: 999px;
  box-shadow: 0 0 16px rgba(216,255,56,.6);
}
.xpn__tag--ghost { display: none; }
.xpn__when {
  font-family: var(--f-mono);
  font-size: 11.5px; letter-spacing: .18em;
  color: var(--xn);
  opacity: .85;
}
.xpn__role {
  margin: 8px 0 10px;
  font-family: var(--f-display);
  font-weight: 400;
  font-size: clamp(22px, 1.2vw, 30px);
  line-height: 1.02;
  letter-spacing: .005em;
  color: var(--xtext);
}
.xpn__org { display: flex; align-items: center; gap: 10px; }
.xpn__logo {
  width: 30px; height: 30px; flex: none;
  display: grid; place-items: center;
  border-radius: 8px;
  background: rgba(216,255,56,.12);
  border: 1px solid rgba(216,255,56,.3);
  color: var(--xn);
  font-family: var(--f-display); font-size: clamp(10px, 0.47vw, 12px); letter-spacing: .02em;
}
.xpn__org-name {
  font-family: var(--f-sans); font-weight: 500;
  font-size: clamp(15px, 0.71vw, 18px);
  color: var(--xtext);
}
.xpn__loc {
  margin-top: 6px;
  font-family: var(--f-mono);
  font-size: 10.5px; letter-spacing: .14em;
  color: var(--xmute);
}
.xpn__points {
  margin: 16px 0 0;
  display: flex; flex-direction: column; gap: 9px;
  /* details "expand" — compact + dimmed until the node is active/hovered */
  opacity: .42;
  max-height: 4.4em;
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent);
          mask-image: linear-gradient(to bottom, #000 55%, transparent);
  overflow: hidden;
  transition: opacity .5s ease, max-height .6s cubic-bezier(.22,1,.36,1);
}
.xpn__points li {
  position: relative;
  padding-left: 16px;
  font-family: var(--f-sans);
  font-size: clamp(10px, 0.53vw, 13.5px); line-height: 1.5;
  color: rgba(236,231,218,.8);
}
.xpn__points li::before {
  content: ""; position: absolute; left: 0; top: 8px;
  width: 7px; height: 1.5px; background: var(--xn);
  box-shadow: 0 0 6px rgba(216,255,56,.8);
}
.xpn__points li b { color: #fff; font-weight: 600; }

/* Active (in view) OR hovered → expand + brighten + lift in 3D */
.xpn.is-active .xpn__points,
.xpn:hover .xpn__points {
  opacity: 1;
  max-height: 22em;
  -webkit-mask-image: none; mask-image: none;
}
.xpn.is-active .xpn__card,
.xpn:hover .xpn__card {
  transform: translateY(-50%) translateZ(48px) scale(1.018);
  border-color: rgba(216,255,56,.42);
  box-shadow:
    0 44px 90px -34px rgba(0,0,0,.8),
    inset 0 1px 0 rgba(255,255,255,.08),
    0 0 34px -2px rgba(216,255,56,.26);
}
.xpn.is-active .xpn__orb { --orb: 1.28; }
.xpn.is-active .xpn__orb i { box-shadow: 0 0 26px 5px rgba(216,255,56,.95), 0 0 64px 12px rgba(216,255,56,.5); }

@media (prefers-reduced-motion: reduce) {
  .xpn, .xpn__orb b, .xpx__blob, .xpx__scan, .xpx__line-flow { animation: none !important; }
  .xpn__points { opacity: 1; max-height: none; -webkit-mask-image: none; mask-image: none; }
}

/* ---------- Responsive: stack into a single clean column ---------- */
@media (max-width: 900px) {
  .xpx__rail { height: auto; display: flex; flex-direction: column; gap: 26px; padding-left: 22px; }
  .xpx__line { display: none; }
  .xpn { position: relative; left: auto !important; top: auto !important; width: auto; height: auto; animation: none; }
  .xpn__orb { position: absolute; left: -22px; top: 30px; }
  .xpn__card { position: relative; top: auto; left: auto !important; right: auto !important; width: auto; transform: none !important; }
  .xpn__card::before { display: none; }
  .xpn__points { opacity: 1; max-height: none; -webkit-mask-image: none; mask-image: none; }
}

/* =========================================================
   EXPERIENCE — 3D PERSPECTIVE TIMELINE (overrides the vertical
   version). The three roles recede into the screen: newest in
   front (large), oldest at the back (small + dim). Only the
   title sits by each orb; the full detail reveals on hover.
   The connecting wire is a 2D overlay redrawn from the orbs'
   projected screen positions (JS · initXpTimeline).
   ========================================================= */
.xpx { perspective: 1150px; perspective-origin: 50% 46%; }

.xpx__wire {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  z-index: 1; overflow: visible; pointer-events: none;
}
.xpx__wire-base {
  fill: none; stroke: rgba(216,255,56,.22); stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  filter: drop-shadow(0 0 6px rgba(216,255,56,.25));
}
.xpx__wire-flow {
  fill: none; stroke: var(--xn, #D8FF38); stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
  filter: drop-shadow(0 0 10px rgba(216,255,56,.9));
  animation: xpxFlow 2.4s linear infinite;
}
body.perf-lite .xpx__wire-flow { animation: none; }

.xpx__stage {
  position: relative; z-index: 2;
  height: clamp(480px, 66vh, 700px);
  margin-top: clamp(8px, 2.5vh, 32px);
  transform-style: preserve-3d;
  transition: transform .35s cubic-bezier(.22,1,.36,1);
  will-change: transform;
}

/* Node placed in 3D — perspective makes the front one large, the back one
   small, with no manual scaling. */
.xpn {
  position: absolute;
  left: var(--ax); top: var(--ay);
  width: 0; height: 0;
  transform: translate(-50%, -50%) translateZ(var(--tz, 0px));
  transform-style: preserve-3d;
  animation: none;
}
.xpn[data-depth="back"]  { opacity: .8;  filter: blur(.7px); }
.xpn[data-depth="mid"]   { opacity: .93; }
.xpn[data-depth="front"] { opacity: 1; }
.xpn:hover { z-index: 6; }

/* Title-card: compact, centred beneath the orb. Points hidden until hover. */
.xpn__card,
.xpn--right .xpn__card,
.xpn--left  .xpn__card {
  position: absolute;
  left: 50%; right: auto; top: 26px;
  width: 248px;
  transform: translateX(-50%);
  text-align: center;
  padding: 14px 16px 15px;
}
.xpn__card::before { display: none; }
.xpn__tag { right: 14px; }
.xpn__org { justify-content: center; }
.xpn__loc { text-align: center; }
.xpn__points {
  text-align: left;
  opacity: 0; max-height: 0; margin-top: 0;
  -webkit-mask-image: none; mask-image: none;
  overflow: hidden;
  transition: opacity .4s ease, max-height .55s cubic-bezier(.22,1,.36,1), margin-top .4s ease;
}
.xpn:hover .xpn__points,
.xpn.is-active .xpn__points {
  opacity: 1; max-height: 24em; margin-top: 14px;
}
.xpn:hover .xpn__card,
.xpn.is-active .xpn__card {
  transform: translateX(-50%) scale(1.05);
  border-color: rgba(216,255,56,.42);
  box-shadow:
    0 44px 90px -34px rgba(0,0,0,.8),
    inset 0 1px 0 rgba(255,255,255,.08),
    0 0 34px -2px rgba(216,255,56,.28);
}

/* Reveal arm state (JS / GSAP handles the fly-in; this is the no-GSAP guard). */
@media (prefers-reduced-motion: reduce) {
  .xpn__points { opacity: 1; max-height: none; margin-top: 14px; }
}

/* Mobile: drop the 3D, stack as a clean vertical list. */
@media (max-width: 900px) {
  .xpx { perspective: none; }
  .xpx__wire { display: none; }
  .xpx__stage { height: auto; display: flex; flex-direction: column; gap: 22px; transform: none !important; }
  .xpn {
    position: relative; left: auto; top: auto; width: auto; height: auto;
    transform: none !important; opacity: 1 !important; filter: none !important;
    padding-left: 30px;
  }
  .xpn[data-depth] { opacity: 1 !important; }
  .xpn__orb { position: absolute; left: 0; top: 16px; margin: 0; }
  .xpn__card,
  .xpn--right .xpn__card, .xpn--left .xpn__card {
    position: relative; left: auto; top: auto; width: auto;
    transform: none !important; text-align: left;
  }
  .xpn__org, .xpn__loc { justify-content: flex-start; text-align: left; }
  .xpn__points { opacity: 1; max-height: none; margin-top: 14px; }
}

/* =========================================================
   EXPERIENCE — DATA-GLOBE SHOWCASE (rebuilt)
   A rotating WebGL data-globe (vendor/globe.js) is the persistent
   backdrop; experiences scroll past as glass panels with a HUD,
   scene strip and reveal-on-scroll, à la the scroll-sync template
   — but scoped (no global scroll hijack). Driven by initXpShowcase.
   ========================================================= */
.xpx2 { position: relative; z-index: 1; }

/* Cursor-follow guide arrow (canvas). Fixed to the viewport, above the cards,
   click-through. Only visible while the section is on screen (.is-on). */
.xp-arrow {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 5;
  opacity: 0;
  transition: opacity .45s ease;
}
.xpx2.is-on .xp-arrow { opacity: 1; }

/* --- rotating globe backdrop (fixed; appears once you're mostly into the
   section and powers down when it ends — scale + blur + fade "materialise"). --- */
.xpx2__globe {
  position: fixed; inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.74);
  transform-origin: 50% 48%;
  filter: blur(12px) brightness(0.4) saturate(0.7);
  transition: opacity .9s ease, transform 1.1s cubic-bezier(.16,1,.3,1),
              filter 1s ease;
  will-change: opacity, transform, filter;
}
.xpx2__globe canvas { display: block; }
.xpx2__vignette {
  position: fixed; inset: 0;
  z-index: 2; pointer-events: none; opacity: 0;
  transition: opacity .9s ease;
  background:
    radial-gradient(58% 58% at 50% 46%, transparent 42%, rgba(5,6,10,.62) 100%),
    linear-gradient(to bottom, rgba(5,6,10,.5), transparent 18%, transparent 82%, rgba(5,6,10,.5));
}
.xpx2.is-on .xpx2__globe {
  opacity: 1; transform: scale(1); filter: blur(0) brightness(1) saturate(1);
}
.xpx2.is-on .xpx2__vignette { opacity: 1; }
body.perf-lite .xpx2.is-on .xpx2__globe { opacity: .55; }

/* --- Section label (top-left, gallery-logo style) + HUD (top-right) + scene
   strip (left). The label and HUD are tucked just inside the frame's top
   corners via the same inset the frame uses + a small gap. --- */
.xpx2__tag, .xpx2__hud, .xpx2__strip {
  position: fixed; z-index: 5;
  opacity: 0; pointer-events: none;
  transition: opacity .5s ease;
  font-family: var(--f-mono);
}
.xpx2.is-on .xpx2__tag,
.xpx2.is-on .xpx2__hud,
.xpx2.is-on .xpx2__strip { opacity: 1; }
.xpx2__tag {
  top: calc(clamp(16px, 3vh, 40px) + 18px);
  left: calc(clamp(16px, 3.5vw, 64px) + 22px);
  font-size: 11px; letter-spacing: .2em; text-transform: uppercase;
  color: rgba(236,231,218,.7);
}
.xpx2__hud {
  top: calc(clamp(16px, 3vh, 40px) + 18px);
  right: calc(clamp(16px, 3.5vw, 64px) + 22px);
  text-align: right; color: rgba(236,231,218,.5);
}
.xpx2__pct { font-size: clamp(10px, 0.47vw, 12px); letter-spacing: .18em; }
.xpx2__bar {
  width: 120px; height: 1px; margin: 8px 0 6px auto;
  background: rgba(236,231,218,.22); position: relative; overflow: hidden;
}
.xpx2__bar i { position: absolute; inset: 0 auto 0 0; width: 0%; background: var(--accent); box-shadow: 0 0 8px var(--accent); transition: width .15s linear; }
.xpx2__scene { font-size: 10px; letter-spacing: .22em; color: var(--accent); }
.xpx2__strip {
  left: clamp(26px, 4vw, 58px); top: 50%; translate: 0 -50%;
  display: flex; flex-direction: column; gap: 10px;
}
.xpx2__dot { width: 5px; height: 5px; border-radius: 50%; background: rgba(236,231,218,.28); transition: background .3s, scale .3s, box-shadow .3s; }
.xpx2__dot.is-on { background: var(--accent); scale: 1.9; box-shadow: 0 0 10px var(--accent); }

/* --- panels --- */
.xpx2__scroll { position: relative; z-index: 3; }
.xpp {
  min-height: 100vh;
  display: flex; align-items: center;
  padding: clamp(80px, 12vh, 150px) clamp(28px, 8vw, 130px);
}

/* ------------------------------------------------------------------
   Experience cards — restyled to mirror the project-gallery text
   (.hall-slide): mono index eyebrow, serif title wrapping a display
   text-scramble + italic serif subtitle, light low-weight body, and
   minimal accent-tick pointers. Same reveal hooks (.visible) as before.
   ------------------------------------------------------------------ */
.xpcard {
  max-width: clamp(280px, 16.8vw, 430px);
  padding: 32px 32px 34px;
  background: rgba(8, 10, 15, 0.42);
  border-left: 1px solid rgba(216,255,56,.24);
  -webkit-backdrop-filter: blur(16px) saturate(1.05);
          backdrop-filter: blur(16px) saturate(1.05);
  box-shadow: 0 40px 90px -40px rgba(0,0,0,.85), inset 0 1px 0 rgba(255,255,255,.05);
  border-radius: 2px;
}
.xpcard.right { margin-left: auto; text-align: right; border-left: none; border-right: 1px solid rgba(216,255,56,.24); }

/* Eyebrow index — mirrors .hall-slide__num */
.xpcard__num {
  display: inline-block;
  font-family: var(--f-mono);
  font-size: 10px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--accent);
  text-shadow: 0 0 14px rgba(216,255,56,.45);
  border-bottom: 1px solid rgba(216,255,56,.32);
  padding-bottom: 6px; margin-bottom: 24px;
  opacity: 0; translate: 0 10px; transition: opacity .5s ease, translate .5s ease;
}
.xpcard__num.visible { opacity: 1; translate: 0 0; }

/* Title — mirrors .hall-slide__title: serif h2 hosting a display
   text-scramble word + an italic serif subtitle below it. */
.xpcard__title {
  margin: 0;
  font-family: var(--f-serif, "Instrument Serif", serif);
  font-weight: 400;
  font-size: clamp(40px, 2.5vw, 64px);
  line-height: 1.05; letter-spacing: -.01em;
  color: var(--ink);
  opacity: 0; translate: 0 18px; transition: opacity .5s ease .08s, translate .5s ease .08s;
}
.xpcard__title.visible { opacity: 1; translate: 0 0; }
.xpcard .text-scramble { font-size: clamp(34px, 2.15vw, 54px); }
.xpcard.right .text-scramble { align-items: flex-end; }
/* Let multi-word role titles wrap at word boundaries. Spaces are dropped
   from layout (display:none) and word spacing comes from column-gap, so a
   wrapped line carries no trailing space to nudge it off the right edge —
   every line stays flush to the card's text alignment. */
.xpcard .text-scramble__row { flex-wrap: wrap; column-gap: .26em; }
.xpcard .text-scramble__space { display: none; }
.xpcard.right .text-scramble__row { justify-content: flex-end; }
/* Company lockup below the title: a monochrome logo glyph on a dark glass
   tile + the company name — tuned to blend with the section's HUD vibe. */
.xpcard__sub {
  display: flex; align-items: center; gap: 13px;
  margin-top: 18px;
  opacity: 0; translate: 0 10px; transition: opacity .5s ease .16s, translate .5s ease .16s;
}
.xpcard.right .xpcard__sub { flex-direction: row-reverse; justify-content: flex-start; }
.xpcard__sub.visible { opacity: 1; translate: 0 0; }
.xpcard__logo {
  position: relative; height: 44px; width: 44px; flex: none;
  border-radius: 10px; overflow: hidden;
  box-shadow: 0 10px 24px -12px rgba(0,0,0,.8), 0 0 0 1px rgba(216,255,56,.16);
}
.xpcard__logo img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  filter: saturate(.92) brightness(.96);
}
/* A little tint so the original-colour logos sit in the section's vibe. */
.xpcard__logo::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: rgba(216,255,56,.16);
  mix-blend-mode: soft-light;
}
.xpcard__co {
  font-family: var(--f-sans); font-weight: 500;
  font-size: clamp(14px, 0.63vw, 16px); letter-spacing: .04em;
  line-height: 1.25; color: rgba(236,231,218,.82);
}

/* Body — mirrors .hall-slide__desc */
.xpcard__body {
  margin: 22px 0 0;
  font-family: var(--f-sans); font-weight: 300;
  font-size: clamp(15px, 0.69vw, 17.5px); line-height: 1.72;
  color: rgba(236,231,218,.62);
  opacity: 0; translate: 0 10px; transition: opacity .5s ease .22s, translate .5s ease .22s;
}
.xpcard__body.visible { opacity: 1; translate: 0 0; }
.xpcard__body b { color: #fff; font-weight: 600; }

/* Minimal pointers — thin accent tick + low-weight text. */
.xpcard__points {
  margin: 26px 0 0; display: flex; flex-direction: column; gap: 13px;
  opacity: 0; translate: 0 10px; transition: opacity .5s ease .3s, translate .5s ease .3s;
}
.xpcard__points.visible { opacity: 1; translate: 0 0; }
.xpcard__points li {
  position: relative; padding-left: 18px;
  font-family: var(--f-sans); font-weight: 300; font-size: clamp(10px, 0.51vw, 13px); line-height: 1.6;
  color: rgba(236,231,218,.7); text-align: left;
}
.xpcard.right .xpcard__points li { padding-left: 0; padding-right: 18px; text-align: right; }
.xpcard__points li::before {
  content: ""; position: absolute; left: 0; top: 9px;
  width: 9px; height: 1px; background: var(--accent); opacity: .85;
}
.xpcard.right .xpcard__points li::before { left: auto; right: 0; }
.xpcard__points li b { color: #fff; font-weight: 600; }

/* "wow" closing pointer — set off by a hairline divider and a glowing,
   gently pulsing accent node instead of the flat tick. */
.xpcard__points li.xpcard__wow {
  margin-top: 14px; padding-top: 15px;
  border-top: 1px solid rgba(216,255,56,.16);
  font-size: clamp(10px, 0.53vw, 13.5px); color: rgba(236,231,218,.95); letter-spacing: .004em;
}
.xpcard.right .xpcard__points li.xpcard__wow { padding-right: 18px; }
.xpcard__points li.xpcard__wow::before {
  width: 7px; height: 7px; top: 22px; border-radius: 50%;
  background: var(--accent); opacity: 1;
  box-shadow: 0 0 9px var(--accent), 0 0 18px rgba(216,255,56,.55);
  animation: xpWowPulse 2.1s ease-in-out infinite;
}
@keyframes xpWowPulse {
  0%, 100% { box-shadow: 0 0 7px var(--accent), 0 0 14px rgba(216,255,56,.38); transform: scale(1); }
  50%      { box-shadow: 0 0 13px var(--accent), 0 0 28px rgba(216,255,56,.78); transform: scale(1.2); }
}
@media (prefers-reduced-motion: reduce) {
  .xpcard__points li.xpcard__wow::before { animation: none; }
}

/* text-scramble in the dark experience context: lift the underline
   base off the near-black panel (its default is a dark-on-light tint). */
html.is-dark .xpcard .text-scramble__underline-base { background: rgba(236,231,218,.18); }

@media (prefers-reduced-motion: reduce) {
  .xpcard__num, .xpcard__title, .xpcard__sub, .xpcard__body, .xpcard__points {
    opacity: 1 !important; translate: 0 0 !important;
  }
}
@media (max-width: 760px) {
  .xpx2__strip { display: none; }
  .xpp { padding: 90px 20px; }
  .xpcard, .xpcard.right { max-width: 100%; text-align: left; }
  .xpcard.right { margin-left: 0; border-right: none; border-left: 1px solid rgba(216,255,56,.24); }
  .xpcard.right .text-scramble { align-items: flex-start; }
  .xpcard.right .xpcard__points li { padding-left: 18px; padding-right: 0; text-align: left; }
  .xpcard.right .xpcard__points li::before { left: 0; right: auto; }
}

/* --- Depth layers behind the experience cards: starfield + glow orbs.
   They sit below the globe (z0) so the earth floats over them, and the cards'
   glass blur diffuses them for depth. Appear/disappear with the globe. --- */
.xpx2__stars, .xpx2__orbs {
  position: fixed; inset: 0; z-index: 0;
  pointer-events: none; opacity: 0;
  transition: opacity 1s ease;
}
.xpx2.is-on .xpx2__stars { opacity: 1; }
.xpx2.is-on .xpx2__orbs { opacity: 1; }

/* Two parallax star layers (far + near). Their vertical position is driven by
   scroll via --starY (set in JS): the repeating background tiles infinitely, so
   as the page scrolls down the stars slide up and new ones wrap in from the
   bottom. The near layer moves faster than the far one for depth. */
.xpx2__stars::before, .xpx2__stars::after {
  content: ""; position: absolute; inset: 0;
  background-repeat: repeat;
  will-change: background-position;
}
.xpx2__stars::before {
  background-image:
    radial-gradient(1px 1px at 12% 18%, rgba(255,255,255,.85), transparent),
    radial-gradient(1px 1px at 33% 62%, rgba(255,255,255,.55), transparent),
    radial-gradient(1.4px 1.4px at 58% 28%, rgba(255,255,255,.75), transparent),
    radial-gradient(1px 1px at 72% 78%, rgba(255,255,255,.5), transparent),
    radial-gradient(1px 1px at 85% 42%, rgba(255,255,255,.7), transparent),
    radial-gradient(1px 1px at 45% 88%, rgba(255,255,255,.5), transparent),
    radial-gradient(1.3px 1.3px at 22% 80%, rgba(255,255,255,.6), transparent),
    radial-gradient(1px 1px at 92% 12%, rgba(255,255,255,.6), transparent);
  background-size: 320px 320px;
  background-position: 0 calc(var(--starY, 0) * -0.30px);
  opacity: .95;
  animation: xpxTwinkle 6s ease-in-out infinite;
}
.xpx2__stars::after {
  background-image:
    radial-gradient(1.8px 1.8px at 20% 35%, rgba(216,255,56,.6), transparent),
    radial-gradient(2px 2px at 65% 15%, rgba(255,255,255,.85), transparent),
    radial-gradient(1.6px 1.6px at 80% 70%, rgba(216,255,56,.45), transparent),
    radial-gradient(2px 2px at 40% 80%, rgba(255,255,255,.7), transparent),
    radial-gradient(1.6px 1.6px at 8% 60%, rgba(255,255,255,.8), transparent),
    radial-gradient(2.2px 2.2px at 92% 40%, rgba(216,255,56,.6), transparent);
  background-size: 520px 520px;
  background-position: 0 calc(var(--starY, 0) * -0.52px);
  opacity: .85;
  animation: xpxTwinkle 8s ease-in-out infinite reverse;
}
@keyframes xpxTwinkle { 0%,100% { opacity: .6; } 50% { opacity: 1; } }

/* Soft drifting glow orbs — both accent green, blurred for atmosphere. */
.xpx2__orbs::before, .xpx2__orbs::after {
  content: ""; position: absolute; border-radius: 50%;
  filter: blur(80px);
  will-change: transform;
}
.xpx2__orbs::before {
  width: 42vw; height: 42vw; max-width: 620px; max-height: 620px;
  left: -8vw; top: 6vh;
  background: radial-gradient(circle, rgba(216,255,56,.16), transparent 64%);
  animation: xpxOrb1 30s ease-in-out infinite alternate;
}
.xpx2__orbs::after {
  width: 46vw; height: 46vw; max-width: 680px; max-height: 680px;
  right: -10vw; bottom: 2vh;
  background: radial-gradient(circle, rgba(216,255,56,.15), transparent 66%);
  animation: xpxOrb2 38s ease-in-out infinite alternate;
}
@keyframes xpxOrb1 { to { transform: translate3d(8vw, 6vh, 0) scale(1.15); } }
@keyframes xpxOrb2 { to { transform: translate3d(-7vw, -5vh, 0) scale(1.2); } }

body.perf-lite .xpx2__stars::before,
body.perf-lite .xpx2__stars::after,
body.perf-lite .xpx2__orbs::before,
body.perf-lite .xpx2__orbs::after { animation: none; }
body.perf-lite .xpx2__orbs { opacity: .6; }
@media (prefers-reduced-motion: reduce) {
  .xpx2__stars::before, .xpx2__stars::after,
  .xpx2__orbs::before, .xpx2__orbs::after { animation: none; }
}
