/* ---------- Tokens: near-black + off-white, one restrained interface-blue
   signal instead of a decorative rainbow sweep. The blue is used sparingly
   and deliberately (status dot, active nav state, one underline, the cube's
   rim light) — never as a background gradient wash. ---------- */
:root{
  --bg:#07080a;
  --bg-rgb:7,8,10;
  --surface:#0d0f12;
  --surface-2:#15171b;
  --text:#f2f1ec;
  --text-rgb:242,241,236;
  --text-muted:rgba(242,241,236,0.58);
  --text-faint:rgba(242,241,236,0.42);
  --hairline:rgba(242,241,236,0.12);
  --hairline-strong:rgba(242,241,236,0.28);
  --invert-bg:#f2f1ec;
  --invert-text:#07080a;
  --shadow:rgba(0,0,0,0.6);
  --display:'Bricolage Grotesque', ui-sans-serif, -apple-system, 'Segoe UI', sans-serif;
  --mono:'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* The one accent color in the whole system — an interface signal, not a
     decoration. Used for: status indicators, active states, one rim light,
     the odd underline. Never a gradient, never a background wash. */
  --accent:#4a8fff;
  --accent-rgb:74,143,255;
  --accent-dim:rgba(74,143,255,0.5);
  /* Pedestal glow behind the Z3N diamond (see .cube-stage::before) — a
     faint ambient light in dark mode, a much stronger grounding shadow in
     light mode, where the gem's own transparency needs real contrast to
     read against a light page. Not derivable from --text alone (a usable
     dark-mode opacity is much too faint once mixed from a near-black
     light-mode --text), so it's its own token per theme. */
  --cube-glow:rgba(242,241,236,0.15);
}
@media (prefers-color-scheme: light){
  :root:not([data-theme="dark"]){
    --bg:#faf9f6;
    --bg-rgb:250,249,246;
    --surface:#f1f0eb;
    --surface-2:#e6e4dd;
    --text:#101114;
    --text-rgb:16,17,20;
    --text-muted:rgba(16,17,20,0.6);
    --text-faint:rgba(16,17,20,0.46);
    --hairline:rgba(16,17,20,0.12);
    --hairline-strong:rgba(16,17,20,0.28);
    --invert-bg:#101114;
    --invert-text:#faf9f6;
    --shadow:rgba(0,0,0,0.12);
    --accent:#2f6fe0;
    --accent-rgb:47,111,224;
    --accent-dim:rgba(47,111,224,0.45);
    --cube-glow:rgba(16,17,20,0.6);
  }
}
:root[data-theme="light"]{
  --bg:#faf9f6; --bg-rgb:250,249,246; --surface:#f1f0eb; --surface-2:#e6e4dd;
  --text:#101114; --text-rgb:16,17,20; --text-muted:rgba(16,17,20,0.6); --text-faint:rgba(16,17,20,0.46);
  --hairline:rgba(16,17,20,0.12); --hairline-strong:rgba(16,17,20,0.28);
  --invert-bg:#101114; --invert-text:#faf9f6; --shadow:rgba(0,0,0,0.12);
  --accent:#2f6fe0; --accent-rgb:47,111,224; --accent-dim:rgba(47,111,224,0.45);
  --cube-glow:rgba(16,17,20,0.6);
}
:root[data-theme="dark"]{
  --bg:#07080a; --bg-rgb:7,8,10; --surface:#0d0f12; --surface-2:#15171b;
  --text:#f2f1ec; --text-rgb:242,241,236; --text-muted:rgba(242,241,236,0.58); --text-faint:rgba(242,241,236,0.42);
  --hairline:rgba(242,241,236,0.12); --hairline-strong:rgba(242,241,236,0.28);
  --invert-bg:#f2f1ec; --invert-text:#07080a; --shadow:rgba(0,0,0,0.6);
  --accent:#4a8fff; --accent-rgb:74,143,255; --accent-dim:rgba(74,143,255,0.5);
  --cube-glow:rgba(242,241,236,0.15);
}

*{box-sizing:border-box;}
html{-webkit-tap-highlight-color:transparent;}
@media (prefers-reduced-motion: no-preference){ html{scroll-behavior:smooth;} }
/* Lenis drives scroll easing itself while active (tags <html> with these classes) —
   native CSS smooth-scroll would otherwise fight its own easing on every jump. */
html.lenis-smooth{scroll-behavior:auto !important;}
body{
  background:var(--bg); color:var(--text); font-family:var(--display); font-weight:400; overflow-x:hidden;
  -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
}
::selection{background:var(--text); color:var(--bg);}
a{color:inherit;}
img{max-width:100%;}
a, button{-webkit-tap-highlight-color:transparent;}

/* Thin, theme-matched scrollbar instead of the OS default — a small detail,
   but a default chunky gray scrollbar is one of the fastest ways a page
   reads as unstyled. */
html{scrollbar-width:thin; scrollbar-color:var(--hairline-strong) transparent;}
::-webkit-scrollbar{width:10px; height:10px;}
::-webkit-scrollbar-track{background:transparent;}
::-webkit-scrollbar-thumb{background:var(--hairline-strong); border-radius:20px; border:2px solid var(--bg); background-clip:padding-box;}
::-webkit-scrollbar-thumb:hover{background:var(--text-faint); background-clip:padding-box;}
/* Keyboard-only focus ring — :focus-visible skips it for mouse/touch clicks,
   so it shows up exactly where it's useful without adding visual noise to
   every tap. Elements with their own focus handling (.view, the nav overlay
   links moved via JS) already carry a more specific override. */
:focus-visible{outline:2px solid var(--text); outline-offset:3px; border-radius:2px;}

/* Skip link: off-screen (not just invisible — display:none/opacity:0 would
   pull it out of tab order in some AT) until it's the focused element,
   then it's the very first thing keyboard/screen-reader users see. Click
   handling lives in script.js rather than letting the #main-content href
   fire normally, since a real hash change would be caught by the
   hashchange router below and read as "navigate to a view named
   main-content" — falling back to Home regardless of the current page. */
.skip-link{
  position:fixed; top:12px; left:12px; z-index:9999;
  transform:translateY(-140%);
  background:var(--text); color:var(--invert-text);
  font-family:var(--mono); font-size:13px; font-weight:600;
  padding:10px 18px; border-radius:6px; text-decoration:none;
  transition:transform .2s cubic-bezier(.16,1,.3,1);
}
.skip-link:focus-visible{ transform:translateY(0); outline:2px solid var(--text); outline-offset:3px; }

/* ---------- Custom cursor: a thin ring + dot that inverts on hover
   (fine pointers only — see initCustomCursor() in the script) ----------
   Idle, it's just an outline in var(--text) — the same color as the page's
   own text, so it flips from black to white with the theme automatically
   instead of relying on a blend trick to stay legible. The moment it
   crosses onto a button/link it blooms into a solid white disc switched to
   mix-blend-mode:difference, which inverts whatever's underneath it
   per-pixel as it moves across — the actual "cursor inverts color on
   buttons" effect, not just a cursor that happens to always read legibly.
   Tracking is instant (the script sets one `transform:translate()` on
   pointermove, no per-frame lag); the grow/shrink states are plain
   width/height changes so they never fight that transform, and a nested
   center-dot span fades out on hover instead of trying to scale in step
   with the ring's own resize. */
@media (pointer:fine){
  *{cursor:none !important;}
}
/* Named .site-cursor rather than .cursor — the latter is already the
   blinking terminal-caret class used in the loader markup (see further
   down), and reusing it here would collide with that rule's
   own layout properties (display:inline-block etc.), which — coming later
   in the stylesheet — would silently win over this one's position:fixed. */
.site-cursor{
  position:fixed; top:0; left:0; width:20px; height:20px; z-index:9999;
  border-radius:50%; border:1.5px solid var(--text); pointer-events:none;
  opacity:0;
  transition:opacity .25s ease, width .35s cubic-bezier(.16,1,.3,1), height .35s cubic-bezier(.16,1,.3,1),
    border-color .25s ease, background-color .25s ease;
}
.site-cursor.show{ opacity:1; }
.site-cursor-core{
  position:absolute; top:50%; left:50%; width:3px; height:3px; margin:-1.5px 0 0 -1.5px;
  border-radius:50%; background:var(--text);
  transition:opacity .2s ease;
}
/* Press: just shrinks — width/height again, so it composes with the hover
   state below (a real button press) without a compound rule per state. */
.site-cursor.down{ width:14px; height:14px; }
/* Hover over anything interactive: blooms into a solid disc and switches
   to difference-blend white — this is the invert. */
.site-cursor.hovering{
  width:64px; height:64px; border-color:transparent; background:#fff;
  mix-blend-mode:difference;
}
.site-cursor.hovering .site-cursor-core{ opacity:0; }
.site-cursor.hovering.down{ width:54px; height:54px; }
/* Reward for attention: hovering a project screenshot grows the bloom a
   little further and reveals a small "View" label. This one state opts out
   of the difference-blend invert trick (a legible label needs guaranteed
   contrast regardless of whatever photo happens to be underneath it) in
   favor of a solid, theme-matched disc instead. */
.site-cursor-label{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  font-family:var(--mono); font-size:0; letter-spacing:.06em; text-transform:uppercase; color:var(--invert-text);
  transition:font-size .3s cubic-bezier(.16,1,.3,1);
}
.site-cursor.labeled{
  width:76px; height:76px; mix-blend-mode:normal; background:var(--text); border-color:transparent;
}
.site-cursor.labeled .site-cursor-core{ opacity:0; }
.site-cursor.labeled .site-cursor-label{ font-size:11px; }

/* ---------- Theme toggle: a small fixed button, deliberately outside the
   header and the nav overlay (the header keeps its one-button philosophy,
   and this stays reachable without opening the menu). initThemeToggle() in
   the script sets/reads data-theme on <html> and persists the choice; the
   icon swap below is pure CSS (opacity + transform cross-fade) driven by
   that same attribute, so it never fights the script for `transform`. */
.theme-toggle{
  position:fixed; right:20px; bottom:20px; z-index:50;
  width:42px; height:42px; border-radius:50%;
  background:color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter:blur(10px);
  border:1px solid var(--hairline-strong); color:var(--text); padding:0;
  display:flex; align-items:center; justify-content:center; cursor:pointer;
  box-shadow:0 6px 20px -6px var(--shadow);
  transition:transform .2s cubic-bezier(.16,1,.3,1), border-color .2s ease, background .3s ease, box-shadow .2s ease;
}
.theme-toggle:hover{ transform:scale(1.08); border-color:var(--text); box-shadow:0 8px 24px -6px var(--shadow); }
.theme-toggle:active{ transform:scale(.9); }
.theme-icon{
  position:absolute; width:19px; height:19px;
  transition:transform .4s cubic-bezier(.34,1.56,.64,1), opacity .3s ease;
}
/* Default (dark): moon showing, current-state convention. */
.theme-icon-sun{ opacity:0; transform:rotate(-60deg) scale(.5); }
.theme-icon-moon{ opacity:1; transform:rotate(0deg) scale(1); }
:root[data-theme="light"] .theme-icon-sun{ opacity:1; transform:rotate(0deg) scale(1); }
:root[data-theme="light"] .theme-icon-moon{ opacity:0; transform:rotate(60deg) scale(.5); }
@media (max-width:480px){
  .theme-toggle{ right:14px; bottom:14px; width:38px; height:38px; }
  .theme-icon{ width:17px; height:17px; }
}

/* ---------- Ambient background: quiet, monochrome life behind everything
   after the hero (the hero's own canvas is opaque and sits above this).
   A few big soft blobs drift and breathe very slowly, and a fine film-grain
   layer flickers on top of them — the same "alive, not static" idea as the
   hero, just turned down to a whisper so the actual content stays in front. ---------- */
.ambient{position:fixed; inset:0; z-index:-1; overflow:hidden; pointer-events:none;}
.ambient-blob{position:absolute; border-radius:50%; filter:blur(90px); opacity:.14; background:radial-gradient(circle, var(--text) 0%, transparent 70%); will-change:transform;}
.ambient-blob.b1{width:46vw; height:46vw; top:-14%; left:-10%; animation:driftA 26s ease-in-out infinite alternate;}
.ambient-blob.b2{width:38vw; height:38vw; bottom:-14%; right:-8%; animation:driftB 32s ease-in-out infinite alternate;}
.ambient-blob.b3{width:30vw; height:30vw; top:38%; left:58%; animation:driftC 22s ease-in-out infinite alternate;}
@keyframes driftA{0%{transform:translate(0,0) scale(1);} 100%{transform:translate(6vw,8vh) scale(1.15);}}
@keyframes driftB{0%{transform:translate(0,0) scale(1);} 100%{transform:translate(-8vw,-6vh) scale(1.1);}}
@keyframes driftC{0%{transform:translate(0,0) scale(1);} 100%{transform:translate(-5vw,5vh) scale(.9);}}
.ambient-grain{
  position:absolute; inset:-100px;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  opacity:.035; mix-blend-mode:overlay; animation:grainShift 1s steps(6) infinite;
}
@keyframes grainShift{
  0%{transform:translate(0,0);} 10%{transform:translate(-2%,-4%);} 20%{transform:translate(-6%,2%);}
  30%{transform:translate(4%,-2%);} 40%{transform:translate(-4%,6%);} 50%{transform:translate(-2%,4%);}
  60%{transform:translate(4%,0);} 70%{transform:translate(0,3%);} 80%{transform:translate(-3%,0);}
  90%{transform:translate(3%,4%);} 100%{transform:translate(0,0);}
}

.wrap{max-width:1200px; margin:0 auto; padding-inline:clamp(20px,5vw,56px);}

.eyebrow{
  font-family:var(--mono); font-size:12px; letter-spacing:.14em; text-transform:uppercase;
  color:var(--text-muted); display:flex; align-items:center; gap:10px;
}
.eyebrow::before{content:""; width:5px; height:5px; background:var(--accent); display:inline-block; border-radius:50%; box-shadow:0 0 8px var(--accent-dim);}

h1,h2,h3{font-family:var(--display); font-weight:600; letter-spacing:-0.02em; text-wrap:balance; margin:0;}
p{margin:0;}

.btn-link{
  font-family:var(--mono); font-size:13px; letter-spacing:.03em; text-transform:uppercase;
  display:inline-flex; align-items:center; gap:8px; text-decoration:none; color:var(--text-muted);
  border-bottom:1px solid var(--hairline-strong); padding-bottom:4px;
  transition:color .2s ease, border-color .2s ease, gap .2s ease, transform .15s cubic-bezier(.16,1,.3,1);
  /* --mx/--my come from the magnetic-pull effect (fine pointers only, see
     initMagnetic() in the script) — 0px when idle, so this is a no-op on
     touch and for anyone who never moves a mouse near the link. */
  transform:translate(var(--mx,0px), var(--my,0px));
}
.btn-link:hover{color:var(--text); border-color:var(--text); gap:12px;}
.btn-link:active{transform:translate(var(--mx,0px), var(--my,0px)) scale(.95);}
.btn-link .arrow{transition:transform .2s ease;}
.btn-link:hover .arrow{transform:translate(2px,-2px);}

/* Primary action gets a filled pill instead of the plain underline —
   guarantees full-contrast legibility no matter what's rendering behind it
   (the hero canvas included) and gives the page a clear "do this first"
   beat instead of two visually equal links. */
.btn-primary{
  color:var(--invert-text); background:var(--text); border:1px solid var(--text);
  border-radius:999px; padding:12px 22px 12px 24px; text-shadow:none;
  box-shadow:0 10px 28px -10px var(--shadow);
  transition:color .2s ease, background .2s ease, border-color .2s ease, gap .2s ease,
    transform .15s cubic-bezier(.16,1,.3,1), box-shadow .25s ease;
}
.btn-primary:hover{
  color:var(--text); background:var(--bg); gap:12px;
  box-shadow:0 14px 34px -10px var(--shadow);
}

/* ---------- Loader ---------- */
#loader{
  position:fixed; inset:0; z-index:100; background:var(--bg);
  display:flex; align-items:center; justify-content:center;
  font-family:var(--mono); transition:opacity .5s ease, visibility .5s ease;
}
#loader.done{opacity:0; visibility:hidden; pointer-events:none;}
.loader-box{width:min(340px,80vw); font-size:12px; letter-spacing:.08em; text-transform:uppercase; color:var(--text-faint);}
.loader-line{opacity:0; white-space:pre; margin-bottom:9px; transform:translateY(4px); transition:opacity .3s ease, transform .3s ease;}
.loader-line.show{opacity:1; transform:translateY(0);}
/* System boot sequence: one restrained accent tag, everything else stays
   quiet mono text — this is the terminal identity's entry point, not a
   syntax-highlighted rainbow (see the brief: terminal is the door in, not
   the whole house). */
.lo-tag{color:var(--accent); font-weight:600; margin-right:10px;}
.loader-ready{color:var(--text); font-weight:600; display:flex; align-items:center; gap:9px;}
.lo-ready-dot{width:6px; height:6px; border-radius:50%; background:var(--accent); flex:none; box-shadow:0 0 8px var(--accent-dim);}
.loader-bar{margin-top:16px; height:1px; width:100%; background:var(--hairline); position:relative; overflow:hidden;}
.loader-bar > span{
  position:absolute; inset:0 100% 0 0;
  background:var(--accent);
  transition:right 1s cubic-bezier(.4,0,.2,1);
}
.loader-bar.fill > span{right:0; box-shadow:0 0 12px var(--accent-dim);}
.cursor{display:inline-block; width:7px; height:13px; background:var(--text); margin-left:2px; vertical-align:-2px; animation:blink 1s steps(1) infinite;}
@keyframes blink{0%,49%{opacity:1;}50%,100%{opacity:0;}}

/* ---------- Nav: one futuristic trigger instead of a link row, so the
   whole site down to a mark + a single button — every page reached
   through the one overlay menu it opens. ---------- */
header{
  position:sticky; top:0; z-index:60; background:none; border-bottom:1px solid transparent;
  transition:background .3s ease, border-color .3s ease, backdrop-filter .3s ease;
}
/* Frosted once the page has actually scrolled (see the 'scrolled' toggle in
   script.js) — an edgeless transparent header reads fine over the hero, but
   left transparent everywhere it goes muddy against whatever content is
   passing underneath it. */
header.scrolled{
  background:color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);
  border-bottom-color:var(--hairline);
}
/* Scroll progress: a thin accent line pinned to the header's own bottom
   edge, width driven by --progress (0–1, set in initHeaderScrollState() in
   script.js). One small, honest system readout rather than a decoration. */
.scroll-progress{
  position:absolute; left:0; bottom:-1px; height:1px; width:100%; transform-origin:left;
  transform:scaleX(var(--progress, 0)); background:var(--accent);
  box-shadow:0 0 6px var(--accent-dim); pointer-events:none;
}
.nav-inner{display:flex; align-items:center; justify-content:space-between; padding:18px clamp(20px,5vw,56px); width:100%;}
.mark{
  font-family:var(--mono); font-weight:600; font-size:14px; text-decoration:none; letter-spacing:.02em;
  white-space:nowrap; display:flex; align-items:center; gap:8px;
  transition:opacity .2s ease, transform .15s cubic-bezier(.16,1,.3,1);
}
.mark:hover{opacity:.7;}
.mark:active{transform:scale(.96);}
/* Z3N logomark: an open ring (an ensō circle — a nod at "zen") with a Z
   chevron cut through the middle, in the same stroke-based HUD language as
   the nav trigger's corner brackets. It breathes gently at idle (reusing
   that button's own keyframe) and snaps a little sharper on hover. */
.mark-logo{width:1.7em; height:1.7em; flex-shrink:0; transition:transform .3s cubic-bezier(.16,1,.3,1);}
/* No transform-origin here on purpose: cornerBreathe only animates opacity,
   and adding a CSS transform-origin to an SVG shape that also carries its
   own transform="rotate(...)" attribute makes some browsers recompute that
   rotation around the wrong center — which is exactly what made the ring
   vanish off-canvas during testing. */
.mark-logo-ring{animation:cornerBreathe 3.6s ease-in-out infinite;}
.mark:hover .mark-logo{transform:scale(1.15) rotate(10deg);}

/* A viewfinder-style trigger instead of a plain circle: four corner
   brackets that idle-breathe, spread apart on hover, and snap into an
   "armed" open state with a one-shot radar pulse on click — reads more
   like a HUD reticle than a stock hamburger button. */
.nav-toggle{
  position:relative; width:50px; height:50px; flex-shrink:0;
  background:transparent; border:none; padding:0; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  transition:transform .15s cubic-bezier(.16,1,.3,1);
}
.nav-toggle:active{transform:scale(.9);}
.nav-toggle-corner{
  position:absolute; width:15px; height:15px; border:1px solid var(--hairline-strong);
  transition:transform .4s cubic-bezier(.34,1.56,.64,1), border-color .3s ease;
  animation:cornerBreathe 3.6s ease-in-out infinite;
}
.nav-toggle-corner.tl{ top:2px; left:2px; border-right:none; border-bottom:none; animation-delay:0s; }
.nav-toggle-corner.tr{ top:2px; right:2px; border-left:none; border-bottom:none; animation-delay:.3s; }
.nav-toggle-corner.bl{ bottom:2px; left:2px; border-right:none; border-top:none; animation-delay:.6s; }
.nav-toggle-corner.br{ bottom:2px; right:2px; border-left:none; border-top:none; animation-delay:.9s; }
@keyframes cornerBreathe{ 0%,100%{ opacity:.5; } 50%{ opacity:1; } }
.nav-toggle:hover .nav-toggle-corner{ border-color:var(--text); animation-play-state:paused; opacity:1; }
.nav-toggle:hover .nav-toggle-corner.tl{ transform:translate(-3px,-3px); }
.nav-toggle:hover .nav-toggle-corner.tr{ transform:translate(3px,-3px); }
.nav-toggle:hover .nav-toggle-corner.bl{ transform:translate(-3px,3px); }
.nav-toggle:hover .nav-toggle-corner.br{ transform:translate(3px,3px); }
.nav-toggle.open .nav-toggle-corner{ border-color:var(--text); animation-play-state:paused; opacity:1; }
.nav-toggle.open .nav-toggle-corner.tl{ transform:translate(-5px,-5px); }
.nav-toggle.open .nav-toggle-corner.tr{ transform:translate(5px,-5px); }
.nav-toggle.open .nav-toggle-corner.bl{ transform:translate(-5px,5px); }
.nav-toggle.open .nav-toggle-corner.br{ transform:translate(5px,5px); }
.nav-toggle-pulse{
  position:absolute; inset:9px; border-radius:50%; border:1px solid var(--text);
  opacity:0; pointer-events:none;
}
.nav-toggle-pulse.fire{ animation:navPulse .6s cubic-bezier(.16,1,.3,1); }
@keyframes navPulse{ 0%{ transform:scale(.5); opacity:.9; } 100%{ transform:scale(2.2); opacity:0; } }
.nav-toggle-bars{ position:relative; width:14px; height:10px; }
.nav-toggle-bars span{
  position:absolute; left:0; width:100%; height:1px; background:var(--text);
  transition:top .3s cubic-bezier(.16,1,.3,1), transform .3s cubic-bezier(.16,1,.3,1), opacity .2s ease;
}
.nav-toggle-bars span:first-child{ top:0; }
.nav-toggle-bars span:nth-child(2){ top:4.5px; }
.nav-toggle-bars span:last-child{ top:9px; }
.nav-toggle.open .nav-toggle-bars span:first-child{ top:4.5px; transform:rotate(45deg); }
.nav-toggle.open .nav-toggle-bars span:nth-child(2){ opacity:0; }
.nav-toggle.open .nav-toggle-bars span:last-child{ top:4.5px; transform:rotate(-45deg); }

/* Fullscreen overlay menu — the site's only nav now lives here */
.nav-overlay{
  position:fixed; inset:0; z-index:55;
  background:color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter:blur(16px);
  display:flex; align-items:center; justify-content:center;
  opacity:0; visibility:hidden; transition:opacity .4s ease, visibility .4s ease;
}
.nav-overlay.open{ opacity:1; visibility:visible; }
.nav-overlay-inner{ display:flex; flex-direction:column; align-items:center; gap:clamp(30px,5vw,48px); }
.nav-overlay-list{ list-style:none; margin:0; padding:0; text-align:center; }
.nav-overlay-list li{ overflow:hidden; }
.nav-overlay-list a{
  display:flex; align-items:baseline; justify-content:center; gap:20px; text-decoration:none;
  padding:12px 0; color:var(--text-muted);
  font-family:var(--display); font-weight:600; font-size:clamp(32px,7vw,68px); letter-spacing:-0.02em;
  transform:translateY(28px); opacity:0;
  transition:transform .5s cubic-bezier(.16,1,.3,1), opacity .5s ease, color .2s ease;
}
.nav-overlay.open .nav-overlay-list a{ transform:translateY(0); opacity:1; }
.nav-overlay.open .nav-overlay-list a:active{ transform:scale(.97); transition-duration:.15s; }
.nav-overlay-list li:nth-child(1) a{ transition-delay:.06s; }
.nav-overlay-list li:nth-child(2) a{ transition-delay:.12s; }
.nav-overlay-list li:nth-child(3) a{ transition-delay:.18s; }
.nav-overlay-list a:hover, .nav-overlay-list a.active{ color:var(--text); }
.nav-overlay-index{ font-family:var(--mono); font-size:13px; color:var(--text-faint); transition:color .2s ease;}
.nav-overlay-list a.active .nav-overlay-index{ color:var(--accent); }

/* Small horizontal social row beneath the main links */
.nav-overlay-social{
  display:flex; align-items:center; gap:18px;
  opacity:0; transform:translateY(16px);
  transition:opacity .5s ease, transform .5s cubic-bezier(.16,1,.3,1); transition-delay:.32s;
}
.nav-overlay.open .nav-overlay-social{ opacity:1; transform:translateY(0); }
.nav-overlay-social a{
  font-family:var(--mono); font-size:11px; letter-spacing:.08em; text-transform:uppercase;
  color:var(--text-faint); text-decoration:none; padding-bottom:3px;
  border-bottom:1px solid transparent;
  display:inline-block;
  transition:color .2s ease, border-color .2s ease, transform .15s cubic-bezier(.16,1,.3,1);
}
.nav-overlay-social a:hover{ color:var(--text); border-color:var(--hairline-strong); }
.nav-overlay-social a:active{ transform:scale(.93); }
.nav-overlay-social .dot{ width:3px; height:3px; border-radius:50%; background:var(--text-faint); display:inline-block; }

/* ---------- Views / transitions ---------- */
main{position:relative;}
.view{padding-block:clamp(48px,8vw,88px); animation:viewIn .5s cubic-bezier(.16,1,.3,1) both;}
.view:focus{outline:none;}
.view.leaving{animation:viewOut .32s ease both;}
@keyframes viewIn{from{opacity:0; transform:translateY(14px);} to{opacity:1; transform:translateY(0);}}
@keyframes viewOut{from{opacity:1; transform:translateY(0);} to{opacity:0; transform:translateY(-10px);}}
/* Page-transition canvas: draws a brief matrix-rain burst on nav clicks
   (see playMatrixTransition() in script.js) instead of a plain wipe. */
#wipe{position:fixed; inset:0; z-index:65; width:100%; height:100%; pointer-events:none;}

.reveal{opacity:0; transform:translateY(18px); transition:opacity .6s ease, transform .6s cubic-bezier(.16,1,.3,1);}
.reveal.in{opacity:1; transform:translateY(0);}

/* ---------- Home / hero (full-bleed cinematic) ---------- */
.view[data-view="home"]{padding-top:0;}
.hero-full{
  position:relative; width:100vw; margin-left:calc(50% - 50vw); margin-right:calc(50% - 50vw);
  /* Pulled up by exactly the (transparent, sticky) header's own height so the
     colorful field reaches the true top of the page and shows through behind
     the logo/nav button instead of leaving a flat black strip above it. The
     header's darkening gradient just below (::before) already keeps it legible
     against whatever the canvas is doing there. */
  margin-top:-86px;
  min-height:88svh; display:flex; align-items:center; justify-content:center;
  overflow:hidden; background:var(--bg);
}
@media (max-width:480px){ .hero-full{ margin-top:-72px; } }
canvas#hero-canvas{position:absolute; inset:0; width:100%; height:100%; display:block; will-change:transform;}
.hero-full::before{
  content:""; position:absolute; inset:0; pointer-events:none; z-index:1;
  background:linear-gradient(to bottom, rgb(var(--bg-rgb)) 0%, rgba(var(--bg-rgb),0) 10%, rgba(var(--bg-rgb),0) 88%, rgb(var(--bg-rgb)) 100%);
}
/* Light safety-net vignette behind the headline — the real clearing now happens
   in script.js's drawHero() itself (it reads #hero-content's live bounding box
   every frame and keeps the color field almost fully clear there), so this only
   needs to smooth the transition, not do the heavy lifting. */
.hero-full::after{
  content:""; position:absolute; inset:0; pointer-events:none; z-index:1;
  background:radial-gradient(ellipse 64% 60% at 50% 46%, rgba(var(--bg-rgb),0.5) 0%, rgba(var(--bg-rgb),0.22) 55%, rgba(var(--bg-rgb),0) 82%);
}
.hero-content{position:relative; z-index:2; text-align:center; padding:0 clamp(20px,6vw,56px); max-width:900px; will-change:transform, opacity;}
.hero-content .eyebrow{justify-content:center;}
/* Halo, not a box: every readable bit of hero copy gets a soft glow in the
   page's own background color (light in light mode, dark in dark mode)
   stacked behind it. It reads as "the color field politely steps back
   here" rather than a UI chrome box sitting on top of the art, and it keeps
   working no matter how the noise field happens to be colored underneath at
   any given frame — unlike a fixed-opacity scrim, it never over- or
   under-covers a specific hue.
   .hero-headline is deliberately left out of this group: at the size "Z3N"
   renders now, the layered blurs around every thick stroke overlap into a
   single hard-edged block instead of a soft glow — the noise field's own
   text-safe-zone clearing in drawHero() (script.js) already keeps that
   whole area of the canvas nearly clear, so the giant wordmark doesn't
   need a halo of its own on top of that. */
.hero-content .eyebrow, .hero-name, .hero-roles, .hero-desc, .hero-cta .btn-link, .hero-scroll-cue{
  text-shadow:0 0 10px rgba(var(--bg-rgb),.9), 0 0 26px rgba(var(--bg-rgb),.7), 0 2px 5px rgba(var(--bg-rgb),.9);
}
/* A small glass pill rather than plain haloed text — it's the first thing
   in the hero, doing a "someone's actually behind this" job real product
   sites use their status/changelog badge for, so it earns its own frame. */
.status-pill{
  display:inline-flex; align-items:center; gap:8px; margin-bottom:18px;
  padding:7px 14px 7px 12px; border-radius:999px; border:1px solid var(--hairline-strong);
  background:color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
  font-family:var(--mono); font-size:12px; letter-spacing:.02em; color:var(--text-muted);
}
.status-pill .status-dot{
  width:7px; height:7px; border-radius:50%; background:#4ade80; flex:none;
  animation:statusPulse 2.2s ease-out infinite;
}
@keyframes statusPulse{
  0%{box-shadow:0 0 0 0 rgba(74,222,128,.55);}
  70%{box-shadow:0 0 0 7px rgba(74,222,128,0);}
  100%{box-shadow:0 0 0 0 rgba(74,222,128,0);}
}
@media (prefers-reduced-motion: reduce){ .status-pill .status-dot{animation:none;} }

/* Copy-to-clipboard toast (see initCopyEmail() in script.js): one shared
   element, repositioned above whatever email link was just clicked rather
   than a fixed corner placement, so the confirmation reads as a direct
   response to that click. */
.copy-toast{
  position:fixed; z-index:300; pointer-events:none; transform:translate(-50%,-6px);
  font-family:var(--mono); font-size:11px; letter-spacing:.02em; white-space:nowrap;
  background:var(--text); color:var(--invert-text); padding:6px 12px; border-radius:999px;
  opacity:0; transition:opacity .2s ease, transform .2s cubic-bezier(.16,1,.3,1);
}
.copy-toast.show{ opacity:1; transform:translate(-50%,-14px); }
@media (prefers-reduced-motion: reduce){ .copy-toast{transition:opacity .2s ease;} .copy-toast.show{transform:translate(-50%,-6px);} }

/* The one oversized typography moment on the page — "Z3N" as the mark
   itself, not a "hi i'm a developer" headline. Wide negative tracking at
   this size keeps the three characters reading as a single wordmark rather
   than three separate huge letters. The existing per-letter variable-weight
   interaction (initVariableHeadline in script.js) still applies here — the
   cursor "locking onto" the mark like a HUD readout. */
.hero-headline{
  font-size:clamp(88px,20vw,260px); line-height:0.86; margin-top:20px; color:var(--text);
  letter-spacing:-0.04em; font-weight:700;
}
/* Interactive variable type: Bricolage Grotesque is a true variable font
   (weight + optical-size axes) — letters near the cursor pick up extra
   weight and optical size, easing back as the pointer moves away. Falls
   back to plain static text automatically when JS doesn't wrap it. */
.vf-word{display:inline-block;}
.vf-letter{
  display:inline-block; font-variation-settings:'wght' 600, 'opsz' 40;
  transition:font-variation-settings .4s cubic-bezier(.16,1,.3,1);
}
.hero-name{
  font-family:var(--mono); font-size:clamp(13px,1.6vw,15px); letter-spacing:.14em; text-transform:uppercase;
  color:var(--text-muted); margin-top:20px;
}
.hero-roles{
  font-family:var(--display); font-weight:500; font-size:clamp(16px,2.1vw,21px); line-height:1.5;
  color:var(--text-faint); margin-top:14px;
}
.hero-desc{max-width:40ch; margin-inline:auto; font-size:clamp(15px,1.6vw,17px); line-height:1.6; color:var(--text-muted); margin-top:26px;}
.hero-cta{display:flex; flex-wrap:wrap; gap:18px 30px; margin-top:38px; justify-content:center;}
.hero-scroll-cue{
  position:absolute; left:50%; bottom:26px; transform:translateX(-50%); z-index:2;
  display:flex; flex-direction:column; align-items:center; gap:8px;
  font-family:var(--mono); font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--text-faint);
  background:none; border:none; padding:0; cursor:pointer;
  transition:opacity .2s ease, transform .15s cubic-bezier(.16,1,.3,1);
}
.hero-scroll-cue:hover{ opacity:.8; }
.hero-scroll-cue:active{ transform:translateX(-50%) scale(.92); }
.hero-scroll-cue .line{width:1px; height:26px; background:linear-gradient(to bottom, rgba(var(--text-rgb),.6), transparent); animation:scrollcue 1.8s ease-in-out infinite;}
@keyframes scrollcue{0%{transform:scaleY(0.3); transform-origin:top;}50%{transform:scaleY(1); transform-origin:top;}100%{transform:scaleY(0.3); transform-origin:bottom;}}

/* ---------- ZEN diamond: a WebGL centerpiece between the hero and the stat
   strip (see cube.js). No card/box around it on purpose — it floats
   straight on the page's own background (and the ambient blobs/grain behind
   everything else) in both themes, the way a rendered product shot would
   just sit on the page rather than in a frame. Ships fallback-first:
   .cube-fallback is visible by default and only fades out once cube.js
   proves the scene actually rendered a frame (.cube-ready), so a blocked
   CDN, no WebGL, or a script error never leaves an empty gap. */
.zen-cube-wrap{margin-top:clamp(56px,9vw,96px); text-align:center;}
.cube-stage{position:relative; margin:28px auto 0; width:min(560px,88vw); aspect-ratio:1/1;}
/* Soft pedestal glow behind the gem: alpha:true on the renderer means any
   pixel the diamond doesn't actually cover with a reflection/refraction
   shows the real page color straight through — on a light background that
   reads as "gem is invisible" far more than it ever did on a dark one. This
   gives the transparent canvas something with real contrast to sit in
   front of either way, adapting automatically since it's mixed from the
   page's own --text color (a faint light bloom in dark mode, a grounding
   soft shadow in light mode). */
.cube-stage::before{
  content:""; position:absolute; inset:2%; z-index:0; border-radius:50%;
  background:radial-gradient(circle, var(--cube-glow) 0%, var(--cube-glow) 35%, transparent 66%);
  filter:blur(22px); pointer-events:none;
}
.cube-stage canvas{position:absolute; inset:0; z-index:1; width:100%; height:100%; display:block; cursor:grab;}
.cube-stage canvas:active{cursor:grabbing;}
.cube-hint{
  position:absolute; left:50%; bottom:0; transform:translateX(-50%); z-index:2;
  font-family:var(--mono); font-size:11px; letter-spacing:.1em; text-transform:uppercase;
  color:var(--text-muted); pointer-events:none; opacity:0;
  text-shadow:0 0 10px rgba(var(--bg-rgb),.9), 0 0 22px rgba(var(--bg-rgb),.7);
  transition:opacity .4s ease;
}
.cube-stage.cube-ready .cube-hint{opacity:1;}
.cube-stage.dragged .cube-hint{opacity:0;}
.cube-fallback{
  position:absolute; inset:0; z-index:3; display:flex; align-items:center; justify-content:center;
  transition:opacity .5s ease, visibility .5s ease;
}
.cube-fallback span{
  font-family:var(--mono); font-weight:600; font-size:clamp(32px,8vw,56px); letter-spacing:.08em;
  color:var(--text); animation:cubeFallbackPulse 2.6s ease-in-out infinite;
}
@keyframes cubeFallbackPulse{0%,100%{opacity:.6;}50%{opacity:1;}}
.cube-stage.cube-ready .cube-fallback{opacity:0; visibility:hidden;}
@media (prefers-reduced-motion: reduce){ .cube-fallback span{animation:none;} }

/* stat strip */
.stats{display:grid; grid-template-columns:repeat(3,1fr); margin-top:clamp(40px,7vw,64px);}
.stat{padding:28px clamp(12px,3vw,28px); transition:background .25s ease;}
.stat:hover{background:color-mix(in srgb, var(--text) 4%, transparent);}
.stat:hover .stat-num{transform:scale(1.06);}
.stat:active .stat-num{transform:scale(1.02);}
.stat-num{
  font-family:var(--display); font-weight:700; font-size:clamp(30px,5vw,52px); font-variant-numeric:tabular-nums; letter-spacing:-0.02em;
  display:inline-block; transition:transform .25s cubic-bezier(.16,1,.3,1);
}
.stat-label{font-family:var(--mono); font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--text-muted); margin-top:8px; display:block;}
@media (max-width:620px){.stats{grid-template-columns:1fr; gap:6px;}}

/* ticker (logo-strip equivalent) */
.ticker-wrap{margin-top:clamp(48px,8vw,80px); padding-block:16px; overflow:hidden;}
.ticker{display:flex; gap:44px; width:max-content; animation:scroll 24s linear infinite; font-family:var(--mono); font-size:12px; letter-spacing:.06em; color:var(--text-faint); text-transform:uppercase;}
.ticker-wrap:hover .ticker{animation-play-state:paused;}
.ticker span{display:flex; align-items:center; gap:10px; white-space:nowrap; transition:color .2s ease;}
.ticker span:hover{color:var(--text);}
.ticker span::before{content:"//"; color:var(--text-muted);}
@keyframes scroll{from{transform:translateX(0);}to{transform:translateX(-50%);}}

/* manifesto */
.manifesto{margin-top:clamp(64px,10vw,120px); text-align:center; padding-block:clamp(24px,6vw,48px);}
.manifesto p{font-family:var(--display); font-weight:500; font-size:clamp(24px,4.4vw,44px); line-height:1.25; max-width:20ch; margin:0 auto; text-wrap:balance;}
.manifesto .eyebrow{justify-content:center; margin-bottom:22px;}

/* ---------- Section heading pattern ---------- */
.head-row{display:flex; justify-content:space-between; align-items:flex-end; gap:24px; flex-wrap:wrap; margin-bottom:clamp(36px,6vw,64px);}
.head-row h2{font-size:clamp(30px,5vw,50px);}
.head-note{max-width:34ch; font-size:14px; color:var(--text-muted); line-height:1.55; font-family:var(--display);}

/* ---------- Work: each project as its own full-width "artifact" moment
   instead of an equal-size card grid — a big running number, a header block,
   then a large image allowed to bleed past the text column on desktop. Even
   rows mirror the layout (image/text swap sides via `order`) so the stack
   reads as alternating chapters rather than a repeating template. ---------- */
.artifact-stack{margin-top:clamp(28px,4vw,40px); display:flex; flex-direction:column;}
.artifact{padding-block:clamp(48px,7vw,84px); border-top:1px solid var(--hairline);}
.artifact:first-child{border-top:none;}
.artifact-top{display:flex; align-items:baseline; gap:10px; margin-bottom:clamp(16px,3vw,28px);}
.artifact-num{
  font-family:var(--display); font-weight:700; letter-spacing:-0.03em; color:var(--text);
  font-size:clamp(44px,7vw,88px); line-height:1;
}
.artifact-total{font-family:var(--mono); font-size:13px; color:var(--text-faint); letter-spacing:.04em;}
.artifact-main{display:grid; grid-template-columns:0.95fr 1.15fr; gap:clamp(28px,5vw,64px); align-items:center;}
.artifact-head{order:1;}
.artifact:nth-child(even) .artifact-head{order:2;}
.artifact:nth-child(even) .artifact-art{order:1;}
.artifact-title{font-size:clamp(26px,3.4vw,40px);}
.artifact-desc{margin-top:12px; color:var(--text-muted); font-size:15px; line-height:1.65; max-width:42ch;}
.artifact-art{
  order:2; aspect-ratio:16/11; overflow:hidden; background:var(--surface-2); position:relative; will-change:clip-path, transform;
  display:flex; flex-direction:column; border-radius:6px;
  box-shadow:0 4px 10px -4px var(--shadow), 0 30px 60px -24px var(--shadow);
}
/* Escape the container: on wide viewports the image column is allowed to
   bleed a little past its grid track instead of sitting flush inside it —
   the "artifact partially escaping its frame" effect from the brief, kept
   subtle rather than full-bleed. */
@media (min-width:1000px){
  .artifact-art{width:112%;}
  .artifact:not(:nth-child(even)) .artifact-art{margin-left:-6%;}
  .artifact:nth-child(even) .artifact-art{margin-right:-6%;}
}
/* Browser-chrome header: same fake-window language as the Shylor Bot
   terminal below, so a real screenshot and a mocked-up terminal still read
   as one consistent "here's the actual thing" treatment instead of two
   different metaphors. The URL pill shows the project's real domain — a
   small detail that plants "this is a live site" before the visitor's eye
   even reaches the screenshot. */
.browser-chrome{
  display:flex; align-items:center; gap:6px; flex:none;
  padding:10px 12px; background:var(--surface); border-bottom:1px solid var(--hairline);
}
.browser-dot{width:8px; height:8px; border-radius:50%; background:var(--hairline-strong); flex:none;}
.browser-url{
  margin-left:6px; flex:1; min-width:0; text-align:center;
  font-family:var(--mono); font-size:11px; color:var(--text-faint);
  background:var(--surface-2); border:1px solid var(--hairline); border-radius:20px;
  padding:3px 10px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.project-shot{position:relative; display:block; flex:1; min-height:0; overflow:hidden;}
/* --px/--py come from initArtifactParallax() (fine pointers only, see
   script.js) — 0px at rest. --scale is the plain hover state; folding it
   into the same transform (rather than a separate :hover declaration) is
   what lets it compose with the live parallax instead of one silently
   overriding the other. */
.artifact-art img{
  width:106%; height:106%; margin:-3%; display:block; object-fit:cover; object-position:top;
  --px:0px; --py:0px; --scale:1;
  transform:translate(var(--px),var(--py)) scale(var(--scale));
  transition:transform .5s cubic-bezier(.16,1,.3,1);
}
.artifact-art:hover img{--scale:1.06;}
/* Hover cue: confirms the screenshot is actually clickable (it duplicates
   the "Visit site" link below for mouse users — see the aria-hidden
   tabindex="-1" on .project-shot itself, which keeps it out of the tab
   order so keyboard/AT users aren't offered the same destination twice). */
.project-shot-cue{
  position:absolute; inset:0; z-index:1; display:flex; align-items:center; justify-content:center; gap:8px;
  background:color-mix(in srgb, var(--bg) 55%, transparent); backdrop-filter:blur(2px); -webkit-backdrop-filter:blur(2px);
  font-family:var(--mono); font-size:12px; letter-spacing:.04em; text-transform:uppercase; color:var(--text);
  opacity:0; transition:opacity .3s ease;
}
.artifact-art:hover .project-shot-cue{opacity:1;}
.project-shot-cue .arrow{transition:transform .2s ease;}
.artifact-art:hover .project-shot-cue .arrow{transform:translate(2px,-2px);}
.tag-row{display:flex; flex-wrap:wrap; gap:8px; margin-top:20px;}
.tag{
  font-family:var(--mono); font-size:11px; letter-spacing:.04em; text-transform:uppercase; color:var(--text-muted);
  border:1px solid var(--hairline-strong); padding:5px 10px; border-radius:3px; display:inline-block;
  transition:transform .2s cubic-bezier(.16,1,.3,1), border-color .2s ease, color .2s ease;
}
.tag:hover, .tag:active{transform:scale(1.08); border-color:var(--accent); color:var(--accent);}
.project-visit{margin-top:24px; display:inline-flex;}
.project-status{
  margin-top:24px; display:inline-flex; align-items:center; gap:8px;
  font-family:var(--mono); font-size:13px; color:var(--text-faint);
}
.project-status .status-dot{width:6px; height:6px; border-radius:50%; background:var(--text-faint); flex:none;}

/* ---------- Shylor Bot's artifact-art: a lightweight CSS/HTML terminal
   mockup rather than a live screenshot — this project is a bot to
   self-host, not a deployed site with a screen to render. */
.bot-art{display:flex; align-items:center; justify-content:center; padding:6%;}
.bot-terminal{
  width:100%; max-width:340px; background:var(--bg); border:1px solid var(--hairline-strong);
  border-radius:10px; overflow:hidden; font-family:var(--mono);
  box-shadow:0 24px 48px -20px var(--shadow);
}
.bot-terminal-head{
  display:flex; align-items:center; gap:6px; padding:10px 12px; background:var(--surface);
  border-bottom:1px solid var(--hairline);
}
.bot-dot{width:8px; height:8px; border-radius:50%; background:var(--hairline-strong);}
.bot-terminal-title{margin-left:6px; font-size:11px; color:var(--text-faint);}
.bot-terminal-body{padding:16px; font-size:13px; line-height:1.7;}
.bot-line{margin:0 0 10px; word-break:break-word;}
.bot-line-in{color:var(--text-muted);}
.bot-line-out{color:var(--text);}
.bot-line:last-child{margin-bottom:0;}
@media (max-width:760px){
  .artifact-main{grid-template-columns:1fr; gap:28px;}
  .artifact-head{order:1 !important;}
  .artifact-art{order:2 !important;}
}

/* ---------- Process strip: a short, honest "how I build" beat closing out
   the Work page — a distinct character (quiet, numbered, no imagery) from
   the artifacts above it, while still reading as the same site. ---------- */
.process-strip{margin-top:clamp(72px,11vw,128px); padding-top:clamp(48px,7vw,72px); border-top:1px solid var(--hairline);}
.process-list{list-style:none; margin:0; padding:0; display:flex; flex-direction:column;}
.process-list li{
  display:flex; align-items:baseline; gap:clamp(20px,4vw,40px);
  padding-block:22px; border-bottom:1px solid var(--hairline);
}
.process-list li:first-child{border-top:1px solid var(--hairline);}
.process-num{
  font-family:var(--display); font-weight:700; font-size:clamp(20px,2.6vw,28px); color:var(--text-faint);
  flex:none; width:2ch;
}
.process-text{font-size:clamp(16px,2vw,19px); line-height:1.5; color:var(--text-muted); max-width:56ch;}
@media (max-width:600px){
  .process-list li{gap:16px;}
}

/* Portrait figure: shared bordered-card photo treatment used on the
   About and Contact pages (grayscale image + mono caption). */
.portrait-figure{
  border:1px solid var(--hairline); border-radius:14px; overflow:hidden; background:var(--surface);
  box-shadow:0 3px 8px -4px var(--shadow), 0 22px 46px -18px var(--shadow);
}
.portrait-figure img{width:100%; aspect-ratio:2/3; object-fit:cover; display:block; filter:grayscale(1) contrast(1.05);}
.portrait-figure figcaption{
  padding:12px 16px; font-family:var(--mono); font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(--text-faint);
  border-top:1px solid var(--hairline);
}

/* ---------- About ---------- */
.about-head{margin-top:clamp(32px,5vw,44px); display:grid; grid-template-columns:1.3fr 0.7fr; gap:clamp(32px,6vw,72px); align-items:end;}
.about-lede{font-size:clamp(19px,2.6vw,27px); line-height:1.5; max-width:62ch; font-family:var(--display); font-weight:400;}
.about-lede b{font-weight:600;}
@media (max-width:760px){
  .about-head{grid-template-columns:1fr; align-items:start;}
  .about-photo{max-width:320px;}
}
.interests{margin-top:clamp(48px,7vw,72px); display:grid; grid-template-columns:repeat(4,1fr); gap:1px; background:var(--hairline); border:1px solid var(--hairline); border-radius:8px; overflow:hidden;}
.interest{
  background:var(--surface); padding:26px 20px; display:flex; flex-direction:column; gap:14px; position:relative;
  transition:background .25s ease, transform .3s cubic-bezier(.16,1,.3,1), box-shadow .3s ease;
}
.interest:hover{background:var(--surface-2); transform:translateY(-4px); box-shadow:0 2px 6px -3px var(--shadow), 0 18px 36px -16px var(--shadow); z-index:1;}
.interest:active{transform:translateY(-1px) scale(.98);}
.interest svg{width:22px; height:22px; stroke:var(--text); fill:none; stroke-width:1.6; transition:transform .3s cubic-bezier(.16,1,.3,1);}
.interest:hover svg{transform:scale(1.15) rotate(-4deg);}
.interest-label{font-family:var(--mono); font-size:13px; letter-spacing:.03em;}
/* Spotlight glow, shared by .interest/.edu-card: a soft radial highlight
   that tracks the pointer (--sx/--sy from initSpotlight() in script.js).
   border-radius:inherit clips it to each card's own shape without needing
   overflow:hidden on the card itself. */
.interest::before, .edu-card::before{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none; border-radius:inherit;
  background:radial-gradient(160px circle at var(--sx,50%) var(--sy,50%), color-mix(in srgb, var(--text) 9%, transparent), transparent 70%);
  opacity:0; transition:opacity .35s ease;
}
.interest:hover::before, .edu-card:hover::before{opacity:1;}
.interest > *, .edu-card > *{position:relative;}
@media (max-width:700px){.interests{grid-template-columns:repeat(2,1fr);}}

.subhead{font-family:var(--mono); font-size:12px; letter-spacing:.12em; text-transform:uppercase; color:var(--text-muted); margin-bottom:20px; display:block;}
.skills-block{margin-top:clamp(56px,8vw,88px);}

/* Skills/tools as a plain editorial list — grouped rows (key = category,
   value = the actual tools) instead of a bouncing icon cloud, matching the
   contact list's own key/value rhythm elsewhere on the site. */
.skills-list{display:flex; flex-direction:column; margin-top:6px; border-top:1px solid var(--hairline);}
.skills-row{
  display:flex; flex-wrap:wrap; align-items:baseline; gap:8px 20px;
  padding-block:20px; border-bottom:1px solid var(--hairline);
  transition:background .2s ease, padding-inline .2s ease;
}
.skills-row:hover{background:color-mix(in srgb, var(--text) 4%, transparent); padding-inline:12px;}
.skills-key{
  font-family:var(--mono); font-size:12px; letter-spacing:.1em; text-transform:uppercase;
  color:var(--text-faint); min-width:120px;
}
.skills-val{font-family:var(--display); font-size:clamp(16px,2vw,19px); color:var(--text-muted);}
.skills-row:hover .skills-val{color:var(--text);}

.edu-card{
  margin-top:clamp(56px,8vw,88px); border:1px solid var(--hairline); border-radius:8px; position:relative;
  padding:clamp(22px,4vw,32px); display:flex; justify-content:space-between; align-items:center; gap:20px; flex-wrap:wrap;
  transition:border-color .25s ease, box-shadow .25s ease;
}
.edu-card:hover, .edu-card:active{border-color:var(--hairline-strong); box-shadow:0 2px 6px -4px var(--shadow), 0 16px 34px -18px var(--shadow);}
.edu-name{font-size:clamp(20px,2.6vw,26px); font-family:var(--display); font-weight:600;}
.edu-sub{color:var(--text-muted); margin-top:8px; font-size:14px;}
.edu-status{font-family:var(--mono); font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(--text); border:1px solid var(--hairline-strong); border-radius:20px; padding:6px 14px; white-space:nowrap;}

/* ---------- Contact ---------- */
.cta-banner{padding-block:clamp(40px,7vw,64px); margin-bottom:clamp(40px,7vw,64px); text-align:center;}
.cta-banner h2{font-size:clamp(32px,6vw,64px);}
.contact-body{margin-top:clamp(28px,5vw,48px); display:grid; grid-template-columns:1.3fr 0.7fr; gap:clamp(32px,6vw,72px); align-items:center;}
.contact-list{display:flex; flex-direction:column;}
.contact-photo{max-width:340px; margin-inline:auto 0;}
@media (max-width:760px){
  .contact-body{grid-template-columns:1fr;}
  .contact-photo{max-width:320px; margin-inline:auto;}
}
.contact-row{
  display:flex; justify-content:space-between; align-items:baseline; gap:16px; flex-wrap:wrap;
  padding-block:24px; font-family:var(--mono); text-decoration:none;
  transition:background .2s ease, padding-inline .2s ease;
}
.contact-row:hover{background:color-mix(in srgb, var(--text) 4%, transparent); padding-inline:12px;}
.contact-row:active{background:color-mix(in srgb, var(--text) 8%, transparent);}
.contact-key{font-size:12px; letter-spacing:.1em; color:var(--text-faint); text-transform:uppercase; min-width:110px;}
.contact-val{font-size:clamp(17px,2.6vw,24px); text-decoration:none; color:var(--text-muted); transition:color .2s ease; display:inline-flex; flex-wrap:wrap; align-items:center; gap:10px;}
.contact-row:hover .contact-val{color:var(--text);}
.contact-val::after{
  content:"↗"; font-size:.7em; color:var(--text-faint);
  opacity:0; transform:translate(-4px,4px);
  transition:opacity .25s ease, transform .25s cubic-bezier(.16,1,.3,1);
}
.contact-row:hover .contact-val::after{opacity:1; transform:translate(0,0); color:var(--text);}

/* ---------- Footer ---------- */
footer{margin-top:clamp(64px,10vw,120px); border-top:1px solid var(--hairline);}
.footer-main{
  padding-block:clamp(48px,7vw,72px); display:grid;
  grid-template-columns:minmax(0,1.6fr) minmax(0,1fr) minmax(0,1fr); gap:clamp(32px,5vw,64px);
}
.footer-brand .mark{font-size:14px;}
.footer-tagline{margin-top:18px; max-width:34ch; color:var(--text-muted); font-size:14px; line-height:1.6;}
.footer-col .subhead{margin-bottom:16px;}
.footer-links{display:flex; flex-direction:column; align-items:flex-start; gap:12px;}
.footer-links a{
  font-family:var(--mono); font-size:13px; color:var(--text-muted); text-decoration:none;
  padding-bottom:1px; border-bottom:1px solid transparent;
  transition:color .2s ease, border-color .2s ease, transform .15s cubic-bezier(.16,1,.3,1);
}
.footer-links a:hover{color:var(--text); border-color:var(--hairline-strong);}
.footer-links a.active{color:var(--text); border-color:var(--accent);}
.footer-links a:active{transform:translateX(2px);}
.footer-bottom{
  padding-block:22px; border-top:1px solid var(--hairline);
  display:flex; justify-content:space-between; align-items:center; gap:16px; flex-wrap:wrap;
  font-family:var(--mono); font-size:11px; letter-spacing:.02em; color:var(--text-faint);
}
.footer-top{
  display:inline-flex; align-items:center; gap:6px; background:none; border:none; padding:0; cursor:pointer;
  font-family:var(--mono); font-size:11px; letter-spacing:.02em; color:var(--text-faint);
  transition:color .2s ease, transform .15s cubic-bezier(.16,1,.3,1);
}
.footer-top:hover{color:var(--text);}
.footer-top:hover .arrow{transform:translateY(-2px);}
.footer-top:active{transform:scale(.95);}
.footer-top .arrow{display:inline-block; transition:transform .2s ease;}
@media (max-width:700px){
  .footer-main{grid-template-columns:1fr 1fr; row-gap:40px;}
  .footer-brand{grid-column:1 / -1;}
}
@media (max-width:420px){
  .footer-main{grid-template-columns:1fr; row-gap:36px;}
}

@media (max-width:480px){
  .nav-inner{padding-block:14px;}
  .mark{font-size:12px;}
  .nav-toggle{width:44px; height:44px;}
}

@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{animation-duration:0.001ms !important; animation-iteration-count:1 !important; transition-duration:0.001ms !important;}
  html{scroll-behavior:auto;}
}