/* ==========================================================================
   Effects layer (driven by assets/js/fx.js): intro, custom cursor, magnetic buttons, tilt, split headings,
   marquee, image wipes, hero dot field, footer wordmark, button sweeps, smooth scrolling.
   html.reduce-motion (device setting or the footer switch) swaps movement for gentle fades.
   ========================================================================== */

/* ---------------------------------------------------------------- shared */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
@keyframes fx-fade-in { from { opacity: 0; } }
@keyframes fx-fade-out { to { opacity: 0; } }

/* Lenis smooth scrolling (only the parts this site needs from lenis.css) */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-stopped { overflow: clip; }
.lenis [data-lenis-prevent] { overscroll-behavior: contain; }

/* ---------------------------------------------------------------- intro (first visit) */
.intro-curtain { display: none; }
html.intro .intro-curtain {
  position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; background: #000;
  animation: ic-lift 0.7s cubic-bezier(0.76, 0, 0.24, 1) 1.05s forwards;
}
@keyframes ic-lift { to { clip-path: inset(0 0 100% 0); visibility: hidden; } }
.ic-in { display: grid; justify-items: center; gap: 18px; }
.ic-mark { display: grid; place-items: center; width: 64px; height: 64px; border-radius: 16px; background: #fff; color: #000; font: 700 24px/1 var(--font); letter-spacing: -0.04em;
  animation: ic-pop 0.7s var(--ease-spring) 0.05s both; }
@keyframes ic-pop { from { scale: 0.4; opacity: 0; rotate: -12deg; } }
.ic-name { overflow: hidden; font: 600 22px/1.2 var(--font); letter-spacing: -0.03em; }
.ic-name span { display: block; animation: ic-rise 0.7s var(--ease-out) 0.25s both; }
@keyframes ic-rise { from { transform: translateY(110%); } }
.ic-bar { width: 160px; height: 2px; background: rgba(255, 255, 255, 0.14); overflow: hidden; border-radius: 2px; }
.ic-bar i { display: block; height: 100%; background: #fff; transform-origin: left; animation: ic-load 0.75s cubic-bezier(0.65, 0, 0.35, 1) 0.25s both; }
@keyframes ic-load { from { transform: scaleX(0); } }
/* the hero waits for the curtain */
html.intro .hero-name .ln > span { animation-delay: 1.25s; }
html.intro .hero-name .ln + .ln > span { animation-delay: 1.34s; }
html.intro .hero .reveal.in { animation-delay: calc(var(--d, 0ms) + 1.3s); }

/* ---------------------------------------------------------------- reveal stagger uses --d (set in main.js) */
.js .reveal.in { animation-delay: var(--d, 0ms); }

/* ---------------------------------------------------------------- custom cursor (fine pointers) */
html.has-cursor, html.has-cursor * { cursor: none !important; }
/* its own view-transition layer, so a page change never freezes a copy of it into the sliding snapshot */
/* the layer covers the whole viewport (it never takes clicks), so a page-transition snapshot of it can't clip the
   ring wherever it is */
.cursor { position: fixed; inset: 0; z-index: 2000; pointer-events: none; mix-blend-mode: difference; opacity: 0; transition: opacity var(--dur-2); view-transition-name: cursor; }
::view-transition-old(cursor) { display: none; }
::view-transition-new(cursor) { animation: none; mix-blend-mode: difference; }
::view-transition-group(cursor) { animation-duration: 0s; z-index: 100; }
.cursor.on { opacity: 1; }
/* Position comes from the `translate` property (set in fx.js) and size from `scale`. Browsers apply translate
   after scale, so resizing happens around the ring's own centre. Positioning with `transform` instead made
   every scale change multiply the ring's distance from the corner of the screen, which threw it off on click. */
.cursor-dot, .cursor-ring { position: absolute; left: 0; top: 0; border-radius: 50%; will-change: translate, scale; }
.cursor-dot { width: 6px; height: 6px; margin: -3px 0 0 -3px; background: #fff; transition: scale var(--dur-2) var(--ease-spring); }
.cursor-ring { width: 36px; height: 36px; margin: -18px 0 0 -18px; border: 1px solid #fff; display: grid; place-items: center;
  transition: scale var(--dur-3) var(--ease-spring), background var(--dur-2), border-color var(--dur-2); }
.cursor-label { font: 600 10px/1 var(--font); letter-spacing: 0.06em; text-transform: uppercase; color: #000; opacity: 0; scale: 0.6; transition: opacity var(--dur-1), scale var(--dur-2) var(--ease-spring); }
.cursor.hover .cursor-ring { scale: 1.55; }
.cursor.hover .cursor-dot { scale: 0; }
.cursor.label .cursor-ring { scale: 2.3; background: #fff; }
.cursor.label .cursor-label { opacity: 1; scale: 0.43; } /* counter-scales the ring so the text stays ~10px */
.cursor.down .cursor-ring { scale: 0.8; }
/* pressing on a labelled thing (dragging the 3D engine, the demo balls): shrink to a small ring so it doesn't
   cover what you're moving */
.cursor.label.down .cursor-ring { scale: 0.75; background: transparent; }
.cursor.label.down .cursor-label { opacity: 0; }

/* ---------------------------------------------------------------- magnetic buttons + tilt (transition lists include the new properties) */
.btn { position: relative; isolation: isolate; transition: border-color var(--dur-2), background var(--dur-2), color var(--dur-2), transform var(--dur-1) var(--ease), translate var(--dur-3) var(--ease-spring); }
.icon-btn { transition: border-color var(--dur-2), color var(--dur-2), background var(--dur-2), translate var(--dur-3) var(--ease-spring); }
.po-min { transition: background var(--dur-2), border-color var(--dur-2), color var(--dur-2), transform var(--dur-1), translate var(--dur-3) var(--ease-spring); }
.brand { transition: translate var(--dur-3) var(--ease-spring); }

.tilt, .tilt:hover, .tile.tilt.hot { transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) translateY(var(--lift, 0px)); }
.tile.tilt:hover, .tile.tilt.hot { --lift: -4px; }
.tilt:active { scale: 0.99; }
.interest.tilt, .edu.tilt, .skill-group.tilt, .more-card.tilt { transition: border-color var(--dur-2), background var(--dur-2), transform var(--dur-3) var(--ease-out); }
/* While the pointer is moving, follow it quickly and smoothly; the springy settle is only for letting go.
   (A spring re-started on every mouse move is what made them wobble.) */
.tilt.tracking, .magnetic.tracking { transition-duration: 140ms !important; transition-timing-function: cubic-bezier(0.2, 0.6, 0.3, 1) !important; }

/* ---------------------------------------------------------------- buttons: a fill sweeps across on hover */
/* Outline buttons: a white sweep blended with "difference" sits on top, so the label turns black exactly where
   the fill has reached it (changing the text colour separately ran ahead of the sweep and hid part of it). */
.btn::before {
  content: ""; position: absolute; inset: -1px; border-radius: inherit; pointer-events: none;
  clip-path: inset(0 100% 0 0 round 999px); transition: clip-path var(--dur-3) var(--ease-out);
}
.btn:not(.btn-primary)::before { z-index: 1; background: #fff; mix-blend-mode: difference; }
.btn:hover::before { clip-path: inset(0 0 0 0 round 999px); }
.btn:not(.btn-primary):hover { background: transparent; border-color: #fff; }
/* the white primary button gets a soft grey sweep behind its black label */
.btn-primary::before { z-index: -1; background: #e2e2e2; }
.btn-primary:hover { background: #fff; border-color: #fff; }

/* text links: an underline draws in from the left */
.foot-links a, .launch-all, .foot-talk {
  background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat; padding-bottom: 2px;
  transition: background-size var(--dur-3) var(--ease-out), color var(--dur-2);
}
.foot-links a:hover, .launch-all:hover, .foot-talk:hover { background-size: 100% 1px; }

/* ---------------------------------------------------------------- split headings: letters rise in */
/* each word is its own mask (overflow: clip keeps the text baseline), so letters rise out of it without
   ever crossing the line below */
.split .w { display: inline-block; white-space: nowrap; overflow: clip; overflow-clip-margin: 0.12em; vertical-align: bottom; }
.split .ch { display: inline-block; }
.js .split:not(.split-in) .ch { translate: 0 105%; }
.split.split-in .ch { animation: ch-in var(--dur-3) var(--ease-out) backwards; animation-delay: calc(var(--i) * 22ms); }
@keyframes ch-in { from { translate: 0 105%; } }

/* ---------------------------------------------------------------- marquee */
.marquee { display: grid; gap: 10px; margin: clamp(40px, 6vw, 72px) 0 0; padding: 18px 0; border-block: 1px solid var(--line); overflow: hidden; }
.mq-row { display: flex; overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.mq-track { display: flex; flex: none; margin: 0; padding: 0; list-style: none; animation: mq 46s linear infinite; }
.mq-row.rev .mq-track { animation-duration: 38s; animation-direction: reverse; }
.mq-track li { display: flex; align-items: center; gap: 28px; padding-right: 28px; white-space: nowrap; font: 600 clamp(22px, 3vw, 36px)/1.2 var(--font); letter-spacing: -0.035em; color: var(--ink); }
.mq-row.rev .mq-track li { color: transparent; -webkit-text-stroke: 1px rgba(255, 255, 255, 0.5); }
.mq-track li::after { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: 0.5; }
.mq-row.rev .mq-track li::after { background: rgba(255, 255, 255, 0.35); }
.marquee:hover .mq-track { animation-play-state: paused; }
@keyframes mq { to { transform: translateX(-100%); } }

/* ---------------------------------------------------------------- hero: a dot field that lights up around the pointer */
.hero { isolation: isolate; }
.hero::before, .hero::after {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background-image: radial-gradient(circle, #fff 1px, transparent 1.6px); background-size: 28px 28px;
}
.hero::before { opacity: 0.07; -webkit-mask-image: radial-gradient(ellipse 75% 65% at 60% 45%, #000, transparent 80%); mask-image: radial-gradient(ellipse 75% 65% at 60% 45%, #000, transparent 80%); }
.hero::after { opacity: 0; transition: opacity var(--dur-3);
  -webkit-mask-image: radial-gradient(240px circle at var(--hx, 50%) var(--hy, 50%), #000, transparent 75%); mask-image: radial-gradient(240px circle at var(--hx, 50%) var(--hy, 50%), #000, transparent 75%); }
.hero.lit::after { opacity: 0.55; }
.hero-fig { transition: translate 420ms var(--ease-out); translate: var(--px, 0) var(--py, 0); }

/* ---------------------------------------------------------------- images wipe up into view (project panels) */
/* hidden with opacity, never a clip: Chrome treats an image clipped to nothing as off screen, so neither the
   reveal nor the browser's own lazy-loading would ever fire for it */
.js .wipe:not(.wiped) { opacity: 0; }
.wipe.wiped { animation: wipe-in 1.1s var(--ease-out) backwards; }
@keyframes wipe-in { from { clip-path: inset(100% 0 0 0); scale: 1.08; } to { clip-path: inset(0 0 0 0); scale: 1; } }

/* ---------------------------------------------------------------- small details */
/* icons draw themselves when their card arrives, and again on hover */
.js .draw-icons:not(.in) svg [pathLength] { stroke-dasharray: 1; stroke-dashoffset: 1; }
.draw-icons.in svg [pathLength] { stroke-dasharray: 1; animation: draw 1.3s var(--ease-out) 0.15s backwards; }
.draw-icons.in:hover svg [pathLength] { animation: draw2 0.9s var(--ease-out); }
@keyframes draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes draw2 { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
/* skill chips pop in one by one */
.skill-group.in .chips span, .two-col .in .chips span { animation: chip-in var(--dur-3) var(--ease-spring) backwards; animation-delay: calc(var(--i, 0) * 40ms + 160ms); }
@keyframes chip-in { from { opacity: 0; scale: 0.8; } }
/* the current role's dot breathes */
.job.now::after { content: ""; position: absolute; left: 168px; top: 30px; width: 9px; height: 9px; border-radius: 50%; border: 1px solid #fff; animation: pulse 2.4s var(--ease-out) infinite; pointer-events: none; }
@keyframes pulse { from { transform: scale(1); opacity: 0.9; } to { transform: scale(3.2); opacity: 0; } }
@media (max-width: 720px) { .job.now::after { left: 0; top: 6px; } }
/* contact: a ring of text turning around the photo */
/* max-width: none, or the site-wide svg cap squeezes it to the photo's width and pulls it off centre */
.contact-photo { margin: 14px; }   /* room for the text ring, which reaches past the photo */
.contact-photo .ring { position: absolute; left: -22%; top: -22%; width: 144%; height: 144%; max-width: none; animation: spin 26s linear infinite; pointer-events: none; }
.contact-photo .ring text { font: 600 9.5px var(--font); letter-spacing: 0.32em; fill: var(--muted); text-transform: uppercase; }
/* nav tucks away while scrolling down and returns on the way up */
.nav { transition: translate var(--dur-3) var(--ease-out); }
.nav.nav-hidden { translate: 0 -100%; }

/* ---------------------------------------------------------------- footer */
.footer { position: relative; overflow: hidden; padding: 0; }
.footer .wrap.foot-top { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding-top: clamp(40px, 6vw, 64px); }
.foot-cta { margin: 0; font: 500 clamp(18px, 2vw, 22px)/1.4 var(--font); letter-spacing: -0.02em; color: var(--ink-2); }
.foot-talk { color: var(--ink); white-space: nowrap; }
.foot-up { flex: none; display: grid; place-items: center; width: 48px; height: 48px; border: 1px solid var(--line-3); border-radius: 50%; background: transparent; color: var(--ink); cursor: pointer;
  transition: background var(--dur-2), color var(--dur-2), border-color var(--dur-2), translate var(--dur-3) var(--ease-spring); }
.foot-up:hover { background: #fff; color: #000; border-color: #fff; }
.foot-up svg { width: 18px; height: 18px; transition: transform var(--dur-3) var(--ease-spring); }
.foot-up:hover svg { transform: translateY(-3px); }
.foot-mark { display: flex; justify-content: center; padding: clamp(18px, 3vw, 32px) var(--gutter) 0; user-select: none; }
.foot-mark span {
  font: 700 min(14.4vw, 196px)/0.86 var(--font); letter-spacing: -0.065em; white-space: nowrap;
  color: transparent; -webkit-text-stroke: 1px rgba(255, 255, 255, 0.2);
  background: linear-gradient(90deg, #fff 0 var(--fill, 0%), transparent var(--fill, 0%)); -webkit-background-clip: text; background-clip: text;
}
.footer .wrap.foot-bottom { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 10px; padding-top: 22px; padding-bottom: 32px; font: 400 13.5px/1.5 var(--font); color: var(--dim); }
.foot-links { align-items: center; flex-wrap: wrap; column-gap: 20px; row-gap: 6px; }
@media (max-width: 560px) { .footer .wrap.foot-top { align-items: flex-start; } }

/* ---------------------------------------------------------------- the motion switch (nav) */
.nav-actions { display: flex; align-items: center; gap: 10px; }
.motion-toggle { display: inline-flex; align-items: center; gap: 9px; height: 36px; padding: 0 9px 0 14px; border: 1px solid var(--line-2); border-radius: 999px; background: transparent; color: var(--ink-2); font: 500 13.5px/1 var(--font); cursor: pointer; transition: border-color var(--dur-2), color var(--dur-2); }
.motion-toggle:hover { border-color: var(--line-3); color: var(--ink); }
html:not(.js) .motion-toggle { display: none; }   /* it needs the scripts to do anything */
.mt-switch { position: relative; width: 28px; height: 16px; border-radius: 999px; background: var(--line-3); transition: background var(--dur-2); }
.mt-switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--muted); transition: translate var(--dur-2) var(--ease-spring), background var(--dur-2); }
.motion-toggle[aria-pressed="true"] .mt-switch { background: #fff; }
.motion-toggle[aria-pressed="true"] .mt-switch::after { translate: 12px 0; background: #000; }
/* just above the phone menu there's barely room for the tabs and both buttons, so tighten the tabs */
@media (min-width: 861px) and (max-width: 1040px) {
  .nav-in { gap: 16px; }
  .nav-links a { padding-inline: 11px; }
}
@media (max-width: 860px) {
  .nav-actions { margin-left: auto; }
  .nav-in .nav-toggle { margin-left: 0; }
  .nav-in { gap: 10px; }
}
@media (pointer: coarse) { .motion-toggle { height: 44px; } }

/* ---------------------------------------------------------------- reduced motion: fades instead of movement */
html.reduce-motion .reveal:not(.in) { translate: none; }
html.reduce-motion .reveal.in { animation-name: fx-fade-in; }
html.reduce-motion .hero-name .ln > span { transform: none; animation: fx-fade-in var(--dur-4) ease both; }
html.reduce-motion .wipe.wiped { animation: fx-fade-in var(--dur-4) ease backwards; }
html.reduce-motion .split .ch { animation: none !important; opacity: 1 !important; translate: none !important; }
html.reduce-motion .mq-track, html.reduce-motion .contact-photo .ring, html.reduce-motion .job.now::after { animation: none; }
html.reduce-motion .skill-group.in .chips span, html.reduce-motion .draw-icons.in svg * { animation: none; stroke-dashoffset: 0; }
html.reduce-motion .nav-pill, html.reduce-motion .nav, html.reduce-motion .hero-fig { transition: none; }
html.reduce-motion .nav.nav-hidden { translate: none; }
html.reduce-motion .job-body > div > * { translate: none; }
html.reduce-motion .gb-next.reveal::before, html.reduce-motion .gb-next.reveal::after { transform: none; }
html.reduce-motion::view-transition-old(root) { animation: fx-fade-out var(--dur-2) ease both; }
html.reduce-motion::view-transition-new(root) { animation: fx-fade-in var(--dur-3) ease both; }
