/* Bingying Zheng — personal site v2
   Phase A  (0 – 1×vh):     Hero — full body, centered, clickable
   Phase B  (1×vh – 2.2×vh): Morph — scroll-driven, scales up + clips to half-body, stays centered
   Phase C  (2.2×vh+):       Post-hero — half-body fixed center, cards L/R, 8 real sections
*/

const { useState, useEffect, useRef } = React;

const clamp = (v, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v));
const lerp  = (a, b, t) => a + (b - a) * t;
const ease  = t => t < 0.5 ? 2*t*t : -1 + (4-2*t)*t;

/* ─── Sound effects (Web Audio API — no external files) ─── */
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let _actx = null;
const getACtx = () => { if (!_actx) _actx = new AudioCtx(); return _actx; };

/* windchime decoded buffer cache */
let _chimeBuf = null;
let _chimeLoading = false;

const sounds = {
  hover() {
    const ctx = getACtx();
    const o = ctx.createOscillator();
    const g = ctx.createGain();
    o.connect(g); g.connect(ctx.destination);
    o.type = 'sine';
    o.frequency.setValueAtTime(660, ctx.currentTime);
    o.frequency.exponentialRampToValueAtTime(880, ctx.currentTime + 0.07);
    g.gain.setValueAtTime(0.05, ctx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1);
    o.start(); o.stop(ctx.currentTime + 0.1);
  },
  pop() {
    const ctx = getACtx();
    [440, 554, 659].forEach((freq, i) => {
      const o = ctx.createOscillator();
      const g = ctx.createGain();
      o.connect(g); g.connect(ctx.destination);
      o.type = 'sine'; o.frequency.value = freq;
      const t = ctx.currentTime + i * 0.04;
      g.gain.setValueAtTime(0.07, t);
      g.gain.exponentialRampToValueAtTime(0.001, t + 0.18);
      o.start(t); o.stop(t + 0.18);
    });
  },
  book() {
    const ctx = getACtx();
    const len = Math.floor(ctx.sampleRate * 0.09);
    const buf = ctx.createBuffer(1, len, ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < len; i++) d[i] = (Math.random()*2-1) * Math.exp(-i / (ctx.sampleRate * 0.022));
    const src = ctx.createBufferSource();
    const filt = ctx.createBiquadFilter();
    const g = ctx.createGain();
    filt.type = 'bandpass'; filt.frequency.value = 800; filt.Q.value = 0.6;
    src.buffer = buf; src.connect(filt); filt.connect(g); g.connect(ctx.destination);
    g.gain.setValueAtTime(0.22, ctx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.09);
    src.start();
  },
  click() { this.hover(); },
};

/* ─── DATA ─── */
const WORK_CARDS = [
  { num: '01', title: 'SEO & GEO',        desc: 'AI-assisted audits · technical issues · content gaps · AI search visibility.',        tag: 'Live report',   accent: '#7e946e', href: 'work/seo.html' },
  { num: '02', title: 'GOOGLE ADS',       desc: '+200% ROAS lift. AUD $18K budget turnaround in 8 weeks through creative refresh.',     tag: 'Case study',    accent: '#4285F4' },
  { num: '03', title: 'SOCIAL MEDIA',     desc: '小红书 KOL · WeChat · Weibo · LinkedIn. 110M+ impressions cross-brand.',              tag: 'Case study',    accent: '#e1497a' },
  { num: '04', title: 'MORE AI WORKS',    desc: 'n8n workflows · behavioural triggers · re-engagement funnels. 100+ qualified enquiries.', tag: 'Case study', accent: '#7c3aed' },
  { num: '05', title: 'CREATIVES',        desc: 'Brand assets, ad creatives, social content, motion graphics across industries.',       tag: 'Design & video', accent: '#c95c3a' },
  { num: '06', title: 'EMAIL MARKETING',  desc: 'HubSpot · GoHighLevel · 40–82% open rates. Multi-branch automation.',                  tag: 'Automation',    accent: '#f0c64b', darkTag: true },
];

const CHAPTERS = [
  { kind: 'workhub', chapter: 'portfolio',           label: 'My Work',         cards: [] },
  { kind: 'human',   chapter: 'beyond the metrics',  label: 'The\nHuman',      cards: [] },
];

const REACTIONS = [
  { key: 'intro', text: "Hi, I'm Bing!",        mood: 'sparkle', illo: 'star'   },
  { key: 'dog',   text: 'I love dogs 🐶',        mood: 'heart',   illo: 'dog'    },
  { key: 'game',  text: 'one more game?',        mood: 'sparkle', illo: 'switch' },
  { key: 'cake',  text: 'I love cake 🍰',         mood: 'blush',   illo: 'cake'   },
];

/* ─── Floating illustration SVGs — hand-drawn style ─── */
function IlloSVG({ type }) {
  if (type === 'dog') return (
    <svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
      {/* floppy ears */}
      <path d="M 14,28 C 6,18 4,6 14,4 C 20,3 22,12 20,26 Z" fill="#D4956A" stroke="#0F100D" strokeWidth="2.5" strokeLinejoin="round"/>
      <path d="M 58,28 C 66,18 68,6 58,4 C 52,3 50,12 52,26 Z" fill="#D4956A" stroke="#0F100D" strokeWidth="2.5" strokeLinejoin="round"/>
      {/* inner ear */}
      <path d="M 15,25 C 11,17 11,9 16,8 C 19,7 20,14 19,24 Z" fill="#E8B48A"/>
      <path d="M 57,25 C 61,17 61,9 56,8 C 53,7 52,14 53,24 Z" fill="#E8B48A"/>
      {/* head */}
      <path d="M 36,62 C 16,62 8,50 8,38 C 8,24 18,16 36,16 C 54,16 64,24 64,38 C 64,50 56,62 36,62 Z" fill="#FDDCB0" stroke="#0F100D" strokeWidth="3"/>
      {/* eyes — big cute circles */}
      <circle cx="24" cy="36" r="7" fill="#0F100D"/>
      <circle cx="48" cy="36" r="7" fill="#0F100D"/>
      <circle cx="26" cy="34" r="2.5" fill="#fff"/>
      <circle cx="50" cy="34" r="2.5" fill="#fff"/>
      {/* snout */}
      <ellipse cx="36" cy="48" rx="10" ry="7" fill="#F5C49A" stroke="#0F100D" strokeWidth="2"/>
      {/* nose */}
      <ellipse cx="36" cy="44" rx="4" ry="3" fill="#0F100D"/>
      {/* smile */}
      <path d="M 30,52 Q 36,58 42,52" stroke="#0F100D" strokeWidth="2.5" fill="none" strokeLinecap="round"/>
      {/* tongue */}
      <path d="M 33,54 Q 36,60 39,54" fill="#F47B7B" stroke="#0F100D" strokeWidth="1.5"/>
    </svg>
  );
  if (type === 'switch') return (
    <svg width="84" height="56" viewBox="0 0 84 56" fill="none" xmlns="http://www.w3.org/2000/svg">
      {/* left joy-con */}
      <path d="M 2,18 C 2,10 6,4 14,4 L 22,4 L 22,52 L 14,52 C 6,52 2,46 2,38 Z" fill="#E8001A" stroke="#0F100D" strokeWidth="2.5"/>
      <circle cx="12" cy="18" r="5.5" fill="#0F100D"/>
      <circle cx="12" cy="38" r="3.5" fill="#0F100D" opacity="0.35"/>
      {/* body */}
      <rect x="20" y="2" width="44" height="52" rx="6" fill="#1C1C2E" stroke="#0F100D" strokeWidth="2.5"/>
      <rect x="24" y="6" width="36" height="44" rx="4" fill="#2E3070"/>
      {/* screen glare */}
      <path d="M 28,10 L 36,10 L 30,18 Z" fill="#fff" opacity="0.12"/>
      {/* right joy-con */}
      <path d="M 62,4 L 70,4 C 78,4 82,10 82,18 L 82,38 C 82,46 78,52 70,52 L 62,52 Z" fill="#00B8E0" stroke="#0F100D" strokeWidth="2.5"/>
      <circle cx="72" cy="18" r="5.5" fill="#0F100D"/>
      <circle cx="72" cy="36" r="3.5" fill="#0F100D" opacity="0.35"/>
    </svg>
  );
  if (type === 'cake') return (
    <svg width="60" height="72" viewBox="0 0 60 72" fill="none" xmlns="http://www.w3.org/2000/svg">
      {/* candle */}
      <path d="M 27,6 C 27,4 29,2 30,2 C 31,2 33,4 33,6 L 33,20 L 27,20 Z" fill="#FDE68A" stroke="#0F100D" strokeWidth="2"/>
      {/* flame */}
      <path d="M 30,2 C 28,5 26,8 28,11 C 29,13 31,13 32,11 C 34,8 32,5 30,2 Z" fill="#FF8C42" stroke="#0F100D" strokeWidth="1.5"/>
      <path d="M 30,4 C 29,6 29,9 30,10 C 31,9 31,6 30,4 Z" fill="#FDE68A"/>
      {/* top tier */}
      <path d="M 10,22 C 10,18 18,16 30,16 C 42,16 50,18 50,22 L 50,36 C 50,40 42,42 30,42 C 18,42 10,40 10,36 Z" fill="#FBCFE8" stroke="#0F100D" strokeWidth="2.5"/>
      <path d="M 10,22 C 10,26 18,28 30,28 C 42,28 50,26 50,22" stroke="#0F100D" strokeWidth="2" fill="none"/>
      {/* frosting drips */}
      <path d="M 16,28 Q 14,33 16,35" stroke="#fff" strokeWidth="3" fill="none" strokeLinecap="round"/>
      <path d="M 28,28 Q 26,34 28,36" stroke="#fff" strokeWidth="3" fill="none" strokeLinecap="round"/>
      <path d="M 40,28 Q 38,33 40,35" stroke="#fff" strokeWidth="3" fill="none" strokeLinecap="round"/>
      {/* bottom tier */}
      <path d="M 4,40 C 4,36 15,34 30,34 C 45,34 56,36 56,40 L 56,58 C 56,62 45,64 30,64 C 15,64 4,62 4,58 Z" fill="#FDE68A" stroke="#0F100D" strokeWidth="2.5"/>
      <path d="M 4,40 C 4,44 15,46 30,46 C 45,46 56,44 56,40" stroke="#0F100D" strokeWidth="2" fill="none"/>
      {/* bottom base */}
      <ellipse cx="30" cy="64" rx="26" ry="6" fill="#F5C842" stroke="#0F100D" strokeWidth="2"/>
      {/* sprinkles */}
      <line x1="18" y1="54" x2="22" y2="50" stroke="#F9A8D4" strokeWidth="3" strokeLinecap="round"/>
      <line x1="30" y1="56" x2="34" y2="52" stroke="#6DB8F0" strokeWidth="3" strokeLinecap="round"/>
      <line x1="40" y1="54" x2="44" y2="50" stroke="#F9A8D4" strokeWidth="3" strokeLinecap="round"/>
    </svg>
  );
  /* star / intro */
  return (
    <svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M 30,4 L 35,22 L 54,22 L 39,33 L 45,52 L 30,41 L 15,52 L 21,33 L 6,22 L 25,22 Z" fill="#F0C64B" stroke="#0F100D" strokeWidth="2.5" strokeLinejoin="round"/>
    </svg>
  );
}

/* ─── Hero prop SVG components ─── */

function Macbook() {
  return (
    <svg viewBox="0 0 420 320" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: 'auto' }}>
      <defs>
        <filter id="mb-rough" x="-2%" y="-2%" width="104%" height="104%">
          <feTurbulence type="fractalNoise" baseFrequency="0.7" numOctaves="2" seed="3" />
          <feDisplacementMap in="SourceGraphic" scale="1.2" />
        </filter>
      </defs>
      <g filter="url(#mb-rough)">
        {/* shadow under */}
        <ellipse cx="210" cy="290" rx="180" ry="10" fill="rgba(0,0,0,0.18)" />

        {/* keyboard base */}
        <path d="M 50,250 L 60,278 L 360,278 L 370,250 Z"
              fill="#e2e2dc" stroke="#0f100d" strokeWidth="4" strokeLinejoin="round" />
        {/* notch on the base front */}
        <rect x="190" y="272" width="40" height="6" rx="3" fill="#0f100d" />

        {/* monitor frame */}
        <rect x="60" y="40" width="300" height="215" rx="10"
              fill="#1a1a1a" stroke="#0f100d" strokeWidth="5" />
        {/* screen */}
        <rect x="74" y="54" width="272" height="190" rx="4"
              fill="#0a0a0a" stroke="#0f100d" strokeWidth="2" />

        {/* tiny camera dot */}
        <circle cx="210" cy="48" r="2" fill="#666" />

        {/* mac window dots */}
        <circle cx="92" cy="74" r="4" fill="#ff5f57" />
        <circle cx="106" cy="74" r="4" fill="#febc2e" />
        <circle cx="120" cy="74" r="4" fill="#28c840" />
        {/* WORK text */}
        <text x="210" y="170"
              textAnchor="middle"
              fontFamily="'Bowlby One', 'Archivo Black', sans-serif"
              fontSize="68"
              fill="#fbf6e7"
              letterSpacing="2">WORK</text>
        {/* underline */}
        <path d="M 130,190 Q 210,200 290,188" stroke="#f0c64b" strokeWidth="4" fill="none" strokeLinecap="round" />
        {/* small file-name tab top-right */}
        <rect x="280" y="62" width="58" height="18" rx="3" fill="#2a2a2a" stroke="#1a1a1a" strokeWidth="1" />
        <text x="309" y="75" textAnchor="middle"
              fontFamily="'Space Mono', monospace"
              fontSize="9"
              fill="#999">portfolio</text>

        {/* highlight glints */}
        <path d="M 80,60 L 92,52" stroke="rgba(255,255,255,0.15)" strokeWidth="2" />
      </g>
    </svg>
  );
}

function ResumeChalkboard() {
  const [hovered, setHovered] = useState(false);
  return (
    <svg viewBox="0 0 380 360" xmlns="http://www.w3.org/2000/svg"
         style={{ width: '100%', height: 'auto', transition: 'transform 200ms ease',
                  transform: hovered ? 'translateY(-5px) rotate(1.5deg)' : 'none' }}
         onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}>
      <defs>
        <filter id="cb-rough" x="-2%" y="-2%" width="104%" height="104%">
          <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="2" seed="5" />
          <feDisplacementMap in="SourceGraphic" scale="1.5" />
        </filter>
        <filter id="cb-chalk">
          <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="7" />
          <feDisplacementMap in="SourceGraphic" scale="2.2" />
          <feGaussianBlur stdDeviation="0.3" />
        </filter>
      </defs>

      {/* hanger string */}
      <path d="M 110,20 L 190,52 L 270,20" stroke="#0f100d" strokeWidth="3" fill="none" strokeLinecap="round" />
      <circle cx="190" cy="52" r="6" fill="#fdfaf0" stroke="#0f100d" strokeWidth="2.5" />

      <g filter="url(#cb-rough)">
        {/* wooden frame */}
        <rect x="36" y="58" width="308" height="280" rx="6" fill="#7a5a36" stroke="#0f100d" strokeWidth="5" />
        {/* small wood grain marks */}
        <path d="M 60,90 L 100,92 M 280,110 L 320,112 M 60,310 L 110,308" stroke="#5d3f1f" strokeWidth="2" fill="none" opacity="0.5" />

        {/* chalkboard surface */}
        <rect x="52" y="74" width="276" height="248" rx="3"
              fill={hovered ? '#243528' : '#1c2820'} stroke="#0f100d" strokeWidth="3"
              style={{ transition: 'fill 200ms ease' }} />

        {/* chalkboard slight texture */}
        <rect x="52" y="74" width="276" height="248" rx="3" fill="url(#chalkTex)" opacity="0.0" />
      </g>

      {/* CHALK CONTENT */}
      <g filter="url(#cb-chalk)" stroke="#f1ead6" fill="none" strokeLinecap="round" strokeLinejoin="round" opacity="0.95">
        {/* "RESUME" header */}
        <text x="190" y="115"
              textAnchor="middle"
              fontFamily="'Caveat Brush', cursive"
              fontSize="36"
              fill="#f1ead6"
              stroke="none"
              letterSpacing="3">Resume</text>
        {/* underline squiggle */}
        <path d="M 138,128 Q 165,134 195,128 T 245,128" strokeWidth="2.5" />

        {/* document outline */}
        <rect x="100" y="148" width="180" height="148" rx="4" strokeWidth="2.5" />

        {/* photo block top-left */}
        <rect x="112" y="160" width="34" height="34" rx="2" strokeWidth="2" />
        {/* face icon inside */}
        <circle cx="129" cy="171" r="5" strokeWidth="1.5" />
        <path d="M 117,194 Q 129,182 141,194" strokeWidth="1.5" />

        {/* name + lines top right */}
        <path d="M 156,168 L 256,168" strokeWidth="3" />
        <path d="M 156,180 L 220,180" strokeWidth="1.8" />
        <path d="M 156,190 L 240,190" strokeWidth="1.8" />

        {/* section divider */}
        <path d="M 112,210 L 268,210" strokeWidth="1.5" opacity="0.6" />

        {/* body bullet rows */}
        <circle cx="118" cy="226" r="2.5" fill="#f1ead6" stroke="none" />
        <path d="M 128,226 L 256,226" strokeWidth="1.5" />
        <circle cx="118" cy="242" r="2.5" fill="#f1ead6" stroke="none" />
        <path d="M 128,242 L 232,242" strokeWidth="1.5" />
        <circle cx="118" cy="258" r="2.5" fill="#f1ead6" stroke="none" />
        <path d="M 128,258 L 248,258" strokeWidth="1.5" />
        <circle cx="118" cy="274" r="2.5" fill="#f1ead6" stroke="none" />
        <path d="M 128,274 L 220,274" strokeWidth="1.5" />
      </g>

      {/* chalk piece + eraser on the ledge */}
      <g>
        <rect x="52" y="322" width="276" height="14" fill="#5d3f1f" stroke="#0f100d" strokeWidth="3" />
        {/* chalk */}
        <rect x="78" y="318" width="40" height="8" rx="2" fill="#fdfaf0" stroke="#0f100d" strokeWidth="2" transform="rotate(-3 98 322)" />
        {/* eraser */}
        <rect x="230" y="316" width="56" height="14" rx="2" fill="#d34a4a" stroke="#0f100d" strokeWidth="2.5" />
        <rect x="230" y="324" width="56" height="6" fill="#a83838" stroke="#0f100d" strokeWidth="2" />
      </g>
      {/* chalk dust puffs on hover */}
      {hovered && (
        <g style={{ animation: 'bubble-pop 0.4s ease-out forwards' }}>
          <circle cx="96"  cy="308" r="3" fill="#f1ead6" opacity="0.7" />
          <circle cx="108" cy="302" r="2" fill="#f1ead6" opacity="0.5" />
          <circle cx="84"  cy="300" r="2.5" fill="#f1ead6" opacity="0.6" />
        </g>
      )}
    </svg>
  );
}

function DeskScene({ onWorkClick }) {
  const [hovered, setHovered] = useState(false);

  // lamp turns on automatically when desk is hovered
  const lampOn = hovered;

  // clicking the lamp area navigates to work page
  const handleLampClick = (e) => { e.stopPropagation(); sounds.pop(); if (onWorkClick) onWorkClick(); };
  const handleWorkClick = (e) => { e.stopPropagation(); if (onWorkClick) onWorkClick(); };

  return (
    <svg viewBox="0 0 560 420" xmlns="http://www.w3.org/2000/svg"
         style={{ width: '100%', height: 'auto', transition: 'transform 200ms ease',
                  transform: hovered ? 'translateY(-6px) rotate(-1deg)' : 'none' }}
         onMouseEnter={() => { setHovered(true); sounds.hover(); }}
         onMouseLeave={() => setHovered(false)}>
      <defs>
        <filter id="dk-rough" x="-2%" y="-2%" width="104%" height="104%">
          <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="2" seed="9" />
          <feDisplacementMap in="SourceGraphic" scale="1.4" />
        </filter>
        <radialGradient id="lampGlow" cx="50%" cy="0%" r="80%">
          <stop offset="0%"  stopColor="#fff3a8" stopOpacity="0.85" />
          <stop offset="60%" stopColor="#ffe07a" stopOpacity="0.35" />
          <stop offset="100%" stopColor="#ffd24a" stopOpacity="0" />
        </radialGradient>
      </defs>

      <g filter="url(#dk-rough)">
        {/* DESKTOP */}
        <path d="M 30,330 L 530,330 L 520,360 L 40,360 Z"
              fill="#c08d5a" stroke="#0f100d" strokeWidth="5" strokeLinejoin="round" />
        {/* desk grain */}
        <path d="M 60,342 L 500,342" stroke="#8e6534" strokeWidth="1.6" opacity="0.55" />
        <path d="M 90,350 L 470,350" stroke="#8e6534" strokeWidth="1.4" opacity="0.4" />
        {/* desk legs */}
        <rect x="56"  y="360" width="14" height="60" fill="#8e6534" stroke="#0f100d" strokeWidth="4" />
        <rect x="490" y="360" width="14" height="60" fill="#8e6534" stroke="#0f100d" strokeWidth="4" />

        {/* ============ MACBOOK on the desk ============ */}
        <g transform="translate(176 158)">
          {/* shadow under base */}
          <ellipse cx="120" cy="180" rx="120" ry="6" fill="rgba(0,0,0,0.22)" />

          {/* keyboard base */}
          <path d="M 10,160 L 20,178 L 220,178 L 230,160 Z"
                fill="#e2e2dc" stroke="#0f100d" strokeWidth="4" strokeLinejoin="round" />
          <rect x="106" y="174" width="28" height="5" rx="2.5" fill="#0f100d" />

          {/* monitor frame */}
          <rect x="20" y="0" width="200" height="160" rx="8"
                fill="#1a1a1a" stroke="#0f100d" strokeWidth="4.5" />
          {/* screen — warms up when lamp on, brightens on hover */}
          <rect x="30" y="10" width="180" height="140" rx="3"
                fill={hovered ? '#1a2a1a' : lampOn ? '#1c1604' : '#0a0a0a'} stroke="#0f100d" strokeWidth="2"
                style={{ transition: 'fill 220ms ease' }} />
          <circle cx="120" cy="6" r="1.6" fill="#666" />

          {/* mac window dots */}
          <circle cx="44" cy="22" r="3" fill="#ff5f57" />
          <circle cx="54" cy="22" r="3" fill="#febc2e" />
          <circle cx="64" cy="22" r="3" fill="#28c840" />

          {/* Screen — brightens on hover */}
          <rect x="30" y="10" width="180" height="140" rx="3"
                fill="transparent" style={{ cursor: onWorkClick ? 'pointer' : 'default' }}
                onClick={handleWorkClick} />
          <text x="120" y="100"
                textAnchor="middle"
                fontFamily="'Bowlby One', 'Archivo Black', sans-serif"
                fontSize="50"
                fill={hovered ? '#ffffff' : '#fbf6e7'}
                letterSpacing="1.5"
                style={{ transition: 'fill 200ms ease', pointerEvents: 'none' }}>WORK</text>
          <path d="M 60,118 Q 120,127 180,117" stroke="#f0c64b" strokeWidth="3" fill="none" strokeLinecap="round" style={{ pointerEvents: 'none' }} />
          {hovered && (
            <rect x="30" y="10" width="180" height="140" rx="3"
                  fill="rgba(255,255,255,0.07)" style={{ pointerEvents: 'none' }} />
          )}
        </g>

        {/* ============ PLANT ============ */}
        <g transform="translate(460 286)">
          <ellipse cx="14" cy="-6"  rx="10" ry="16" fill="#5b8848" stroke="#0f100d" strokeWidth="3" transform="rotate(-25 14 -6)" />
          <ellipse cx="36" cy="-10" rx="10" ry="16" fill="#7aa860" stroke="#0f100d" strokeWidth="3" transform="rotate(12 36 -10)" />
          <ellipse cx="25" cy="-20" rx="10" ry="18" fill="#5b8848" stroke="#0f100d" strokeWidth="3" />
          <path d="M 0,4 L 52,4 L 48,46 L 4,46 Z" fill="#c95c3a" stroke="#0f100d" strokeWidth="4" strokeLinejoin="round" />
          <rect x="-3" y="0" width="58" height="10" fill="#c95c3a" stroke="#0f100d" strokeWidth="4" />
        </g>
      </g>

      {/* ============ LAMP ============ */}
      {lampOn && (
        <g style={{ pointerEvents: 'none' }}>
          <path d="M 96,191 L 135,141 L 480,290 L 220,330 Z"
                fill="url(#lampGlow)" />
          <path d="M 30,330 L 530,330 L 520,360 L 40,360 Z"
                fill="#fff3a8" opacity="0.22" />
          <ellipse cx="306" cy="324" rx="120" ry="14" fill="#fff3a8" opacity="0.45" />
        </g>
      )}

      <g onClick={handleLampClick} style={{ cursor: 'pointer' }}>
        <rect x="20" y="80" width="180" height="260" fill="transparent" />

        <g filter="url(#dk-rough)">
          <ellipse cx="70" cy="328" rx="36" ry="7" fill="#2a2a2a" stroke="#0f100d" strokeWidth="3.5" />
          <rect x="58" y="318" width="24" height="12" rx="2" fill="#3a3a3a" stroke="#0f100d" strokeWidth="3" />

          <line x1="70" y1="318" x2="70" y2="130" stroke="#0f100d" strokeWidth="11" strokeLinecap="round" />
          <line x1="70" y1="318" x2="70" y2="130" stroke="#444"    strokeWidth="7"  strokeLinecap="round" />

          <circle cx="70" cy="130" r="9" fill="#3a3a3a" stroke="#0f100d" strokeWidth="3" />

          <g transform="translate(70 130) rotate(-52)">
            <path d="M -14,0 L 14,0 L 32,58 L -32,58 Z"
                  fill={lampOn ? '#ffd54a' : '#f0c64b'}
                  stroke="#0f100d" strokeWidth="4" strokeLinejoin="round"
                  style={{ transition: 'fill 200ms ease' }} />
            <ellipse cx="0" cy="0" rx="14" ry="3.5"
                     fill={lampOn ? '#e6b73a' : '#c99a2c'}
                     stroke="#0f100d" strokeWidth="3" />
            <ellipse cx="0" cy="58" rx="32" ry="6"
                     fill={lampOn ? '#fff1b8' : '#d6a834'}
                     stroke="#0f100d" strokeWidth="3.5"
                     style={{ transition: 'fill 200ms ease' }} />
            {lampOn && (
              <>
                <ellipse cx="0" cy="58" rx="22" ry="5" fill="#fffce0" opacity="0.95" />
                <ellipse cx="0" cy="58" rx="10" ry="3.5" fill="#ffffff" />
              </>
            )}
            <path d="M -10,8 L -22,52" stroke="rgba(255,255,255,0.35)" strokeWidth="2.5" />
          </g>
        </g>

      </g>
    </svg>
  );
}

function VinylPlayer({ onPlayChange }) {
  const [playing, setPlaying] = useState(false);
  const audioRef = useRef(null);

  useEffect(() => {
    const audio = new Audio('assets/bgm.mp3');
    audio.loop = true;
    audio.volume = 0.6;
    audioRef.current = audio;
    return () => { audio.pause(); audio.src = ''; };
  }, []);

  const toggle = () => {
    const next = !playing;
    setPlaying(next);
    if (next) audioRef.current.play().catch(() => {});
    else { audioRef.current.pause(); }
    if (onPlayChange) onPlayChange(next);
  };

  return (
    <div style={{ position: 'relative', width: '100%' }}>
      <svg viewBox="0 0 360 300" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', cursor: 'pointer' }} onClick={toggle}>
        <defs>
          <filter id="vp-rough" x="-2%" y="-2%" width="104%" height="104%">
            <feTurbulence type="fractalNoise" baseFrequency="0.7" numOctaves="2" seed="4" />
            <feDisplacementMap in="SourceGraphic" scale="1.2" />
          </filter>
        </defs>

        {/* shadow */}
        <ellipse cx="180" cy="278" rx="146" ry="6" fill="rgba(0,0,0,0.18)" />

        <g filter="url(#vp-rough)">
          {/* turntable base */}
          <rect x="30" y="62" width="300" height="218" rx="6"
                fill="#7a5a36" stroke="#0f100d" strokeWidth="5" />
          {/* wood grain */}
          <path d="M 50,250 L 310,252" stroke="#5d3f1f" strokeWidth="1.5" opacity="0.55" />
          <path d="M 50,232 L 310,234" stroke="#5d3f1f" strokeWidth="1.2" opacity="0.4" />

          {/* control panel right side */}
          <rect x="246" y="86" width="68" height="160" rx="3" fill="#3a2a1a" stroke="#0f100d" strokeWidth="3" />
          {/* tonearm pivot dot */}
          <circle cx="280" cy="108" r="9" fill="#1a1a1a" stroke="#0f100d" strokeWidth="3" />
          {/* tonearm */}
          <g style={{
            transformOrigin: '280px 108px',
            transform: playing ? 'rotate(28deg)' : 'rotate(-32deg)',
            transition: 'transform 600ms cubic-bezier(0.4, 0.0, 0.2, 1)'
          }}>
            <line x1="280" y1="108" x2="180" y2="170" stroke="#0f100d" strokeWidth="6" strokeLinecap="round" />
            <line x1="280" y1="108" x2="180" y2="170" stroke="#aaa"    strokeWidth="3" strokeLinecap="round" />
            <rect x="170" y="160" width="20" height="22" rx="3" fill="#1a1a1a" stroke="#0f100d" strokeWidth="2.5" />
          </g>

          {/* control buttons */}
          <circle cx="266" cy="216" r="9" fill="#e1497a" stroke="#0f100d" strokeWidth="2.5" />
          <circle cx="294" cy="216" r="9" fill="#f0c64b" stroke="#0f100d" strokeWidth="2.5" />
          {/* slider */}
          <rect x="258" y="130" width="44" height="6" rx="3" fill="#2a1a0a" stroke="#0f100d" strokeWidth="2" />
          <rect x="276" y="124" width="10" height="18" rx="2" fill="#fdfaf0" stroke="#0f100d" strokeWidth="2" />

          {/* VINYL — spins when playing */}
          <g style={{
            transformOrigin: '130px 170px',
            animation: playing ? 'spin 2.5s linear infinite' : 'none'
          }}>
            <circle cx="130" cy="170" r="80" fill="#0f100d" stroke="#0f100d" strokeWidth="4" />
            {/* grooves */}
            <circle cx="130" cy="170" r="70" stroke="#2a2a2a" strokeWidth="1" fill="none" />
            <circle cx="130" cy="170" r="58" stroke="#2a2a2a" strokeWidth="1" fill="none" />
            <circle cx="130" cy="170" r="46" stroke="#2a2a2a" strokeWidth="1" fill="none" />
            {/* center label */}
            <circle cx="130" cy="170" r="28" fill="#e1497a" stroke="#0f100d" strokeWidth="3" />
            <text x="130" y="167" textAnchor="middle"
                  fontFamily="'Caveat Brush', cursive" fontSize="11" fill="#fdfaf0">soya</text>
            <text x="130" y="180" textAnchor="middle"
                  fontFamily="'Space Mono', monospace" fontSize="6" fill="#fdfaf0" letterSpacing="1">vol. 1</text>
            <circle cx="130" cy="170" r="3" fill="#0f100d" stroke="#fdfaf0" strokeWidth="1.5" />
          </g>
        </g>

        {/* music notes float up when playing */}
        {playing && (
          <g style={{ animation: 'noteFloat 2.4s ease-out infinite' }}>
            <text x="60" y="100" fontSize="24" fill="#e1497a" fontFamily="serif">♪</text>
          </g>
        )}
        {playing && (
          <g style={{ animation: 'noteFloat 2.4s ease-out infinite', animationDelay: '0.6s' }}>
            <text x="84" y="84" fontSize="20" fill="#6db8f0" fontFamily="serif">♫</text>
          </g>
        )}
        {playing && (
          <g style={{ animation: 'noteFloat 2.4s ease-out infinite', animationDelay: '1.2s' }}>
            <text x="48" y="90" fontSize="22" fill="#f0c64b" fontFamily="serif">♩</text>
          </g>
        )}
      </svg>

    </div>
  );
}

function CoffeeMug() {
  return (
    <svg viewBox="0 0 240 280" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: 'auto' }}>
      <defs>
        <filter id="cf-rough" x="-2%" y="-2%" width="104%" height="104%">
          <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="2" seed="6" />
          <feDisplacementMap in="SourceGraphic" scale="1.2" />
        </filter>
      </defs>

      {/* STEAM — three trails */}
      <g style={{ animation: 'steam1 3.2s ease-in-out infinite' }}>
        <path d="M 92,80 Q 100,60 92,40 Q 84,20 96,4" stroke="#fdfaf0" strokeWidth="5" fill="none" strokeLinecap="round" opacity="0.85" />
      </g>
      <g style={{ animation: 'steam2 3.4s ease-in-out infinite', animationDelay: '0.4s' }}>
        <path d="M 120,84 Q 128,60 122,38 Q 116,16 130,2" stroke="#fdfaf0" strokeWidth="5" fill="none" strokeLinecap="round" opacity="0.85" />
      </g>
      <g style={{ animation: 'steam3 3.0s ease-in-out infinite', animationDelay: '0.8s' }}>
        <path d="M 148,82 Q 156,60 150,38 Q 144,18 156,2" stroke="#fdfaf0" strokeWidth="5" fill="none" strokeLinecap="round" opacity="0.85" />
      </g>

      <g filter="url(#cf-rough)">
        {/* shadow */}
        <ellipse cx="120" cy="248" rx="86" ry="6" fill="rgba(0,0,0,0.2)" />

        {/* handle */}
        <path d="M 190,130 Q 224,130 224,170 Q 224,210 190,210"
              stroke="#0f100d" strokeWidth="6" fill="none" strokeLinecap="round" />

        {/* mug body */}
        <path d="M 50,108 L 60,236 Q 120,256 180,236 L 190,108 Z"
              fill="#fdfaf0" stroke="#0f100d" strokeWidth="5" strokeLinejoin="round" />
        {/* coffee surface */}
        <ellipse cx="120" cy="108" rx="70" ry="14" fill="#5b3a1f" stroke="#0f100d" strokeWidth="4.5" />
        <ellipse cx="120" cy="104" rx="60" ry="8" fill="#7a5230" />

        {/* a little heart on the mug */}
        <path d="M 120,180 C 102,162 102,148 114,148 C 120,148 120,156 120,160 C 120,156 120,148 126,148 C 138,148 138,162 120,180 Z"
              fill="#e1497a" stroke="#0f100d" strokeWidth="3" strokeLinejoin="round" />
      </g>
    </svg>
  );
}

function PictureFrame() {
  const [idx, setIdx] = useState(0);
  const photos = [
    { bg: '#f4a8c1', emoji: '🌸', caption: 'spring 24' },
    { bg: '#6db8f0', emoji: '🌊', caption: 'beach day' },
    { bg: '#f0c64b', emoji: '🌻', caption: 'sunshine' }
  ];
  const p = photos[idx];
  return (
    <svg viewBox="0 0 280 320" xmlns="http://www.w3.org/2000/svg"
         style={{ width: '100%', cursor: 'pointer' }}
         onClick={() => setIdx((idx + 1) % photos.length)}>
      <defs>
        <filter id="pf-rough" x="-2%" y="-2%" width="104%" height="104%">
          <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="2" seed="13" />
          <feDisplacementMap in="SourceGraphic" scale="1.2" />
        </filter>
      </defs>

      {/* nail in wall */}
      <circle cx="140" cy="30" r="4" fill="#3a3a3a" stroke="#0f100d" strokeWidth="2" />
      {/* hanging string */}
      <path d="M 78,68 Q 140,28 202,68" stroke="#0f100d" strokeWidth="2.5" fill="none" strokeLinecap="round" />

      <g filter="url(#pf-rough)">
        {/* outer frame (wood) */}
        <rect x="40" y="60" width="200" height="240" fill="#7a5a36" stroke="#0f100d" strokeWidth="5" />
        {/* frame grain */}
        <path d="M 56,90 L 60,290 M 224,90 L 220,290" stroke="#5d3f1f" strokeWidth="1.5" opacity="0.4" />

        {/* mat */}
        <rect x="58" y="78" width="164" height="204" fill="#fdfaf0" stroke="#0f100d" strokeWidth="3" />

        {/* photo — changes on click */}
        <rect x="74" y="94" width="132" height="160" fill={p.bg} stroke="#0f100d" strokeWidth="3" />
        <text x="140" y="172"
              textAnchor="middle"
              fontSize="60">{p.emoji}</text>

        {/* caption tag */}
        <rect x="100" y="266" width="80" height="16" fill="#fdfaf0" stroke="#0f100d" strokeWidth="2" />
        <text x="140" y="278"
              textAnchor="middle"
              fontFamily="'Caveat Brush', cursive"
              fontSize="14"
              fill="#0f100d">{p.caption}</text>
      </g>
    </svg>
  );
}

function Bookshelf() {
  const [hoverIdx, setHoverIdx] = useState(null);
  const books = [
    { color: '#e89a6b', title: 'BRAND' },
    { color: '#f0c64b', title: 'GROW' },
    { color: '#a6c8a0', title: 'SEO' },
    { color: '#6db8f0', title: 'DATA' },
    { color: '#e1497a', title: 'ADS' }
  ];
  return (
    <svg viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg"
         style={{ width: '100%' }}>
      <defs>
        <filter id="bs-rough" x="-2%" y="-2%" width="104%" height="104%">
          <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="2" seed="14" />
          <feDisplacementMap in="SourceGraphic" scale="1.2" />
        </filter>
      </defs>

      <g filter="url(#bs-rough)">
        {/* shelf frame */}
        <rect x="30" y="30" width="260" height="260" fill="#b59672" stroke="#0f100d" strokeWidth="5" />
        {/* inner shelves (wood) */}
        <rect x="40" y="40" width="240" height="6" fill="#7a5a36" stroke="#0f100d" strokeWidth="2.5" />
        <rect x="40" y="156" width="240" height="6" fill="#7a5a36" stroke="#0f100d" strokeWidth="2.5" />
        <rect x="40" y="274" width="240" height="6" fill="#7a5a36" stroke="#0f100d" strokeWidth="2.5" />
        {/* shelf sides shadow */}
        <rect x="40" y="46" width="6" height="110" fill="rgba(0,0,0,0.18)" />
        <rect x="40" y="162" width="6" height="112" fill="rgba(0,0,0,0.18)" />

        {/* TOP SHELF — 5 books with hover pop-out */}
        {books.map((b, i) => {
          const x = 60 + i * 36;
          const lifted = hoverIdx === i;
          return (
            <g key={i}
               style={{
                 transform: lifted ? `translate(0px, -10px) rotate(${i % 2 === 0 ? -4 : 4}deg)` : 'translate(0,0)',
                 transformOrigin: `${x + 14}px 156px`,
                 transition: 'transform 240ms cubic-bezier(0.25, 0.85, 0.3, 1.1)',
                 cursor: 'pointer'
               }}
               onMouseEnter={() => { setHoverIdx(i); sounds.book(); }}
               onMouseLeave={() => setHoverIdx(null)}>
              <rect x={x} y="60" width="28" height="96" fill={b.color} stroke="#0f100d" strokeWidth="3" />
              {/* spine line */}
              <line x1={x + 5} y1="68" x2={x + 5} y2="148" stroke="#0f100d" strokeWidth="1.2" opacity="0.5" />
              {/* title vertical */}
              <text x={x + 14} y="108"
                    textAnchor="middle"
                    fontFamily="'Bowlby One', sans-serif"
                    fontSize="10"
                    fill="rgba(0,0,0,0.7)"
                    transform={`rotate(-90 ${x + 14} 108)`}
                    letterSpacing="1">{b.title}</text>
            </g>
          );
        })}

        {/* MIDDLE SHELF — props (cup, plant, frame) — resting on the bottom shelf (top y=274) */}
        {/* small plant pot */}
        <g transform="translate(56 234)">
          <ellipse cx="14" cy="-10" rx="9" ry="14" fill="#5b8848" stroke="#0f100d" strokeWidth="3" transform="rotate(-15 14 -10)" />
          <ellipse cx="30" cy="-14" rx="9" ry="14" fill="#7aa860" stroke="#0f100d" strokeWidth="3" transform="rotate(12 30 -14)" />
          <ellipse cx="22" cy="-22" rx="9" ry="16" fill="#5b8848" stroke="#0f100d" strokeWidth="3" />
          <path d="M 0,4 L 44,4 L 40,40 L 4,40 Z" fill="#c95c3a" stroke="#0f100d" strokeWidth="3.5" />
          <rect x="-2" y="0" width="48" height="9" fill="#c95c3a" stroke="#0f100d" strokeWidth="3.5" />
        </g>
        {/* small picture */}
        <rect x="124" y="216" width="60" height="58" fill="#fdfaf0" stroke="#0f100d" strokeWidth="3.5" />
        <rect x="130" y="222" width="48" height="38" fill="#f4a8c1" stroke="#0f100d" strokeWidth="2" />
        <text x="154" y="248" textAnchor="middle" fontSize="22">♡</text>
        {/* candle / cup */}
        <g transform="translate(200 224)">
          <rect x="0" y="6" width="38" height="44" fill="#fdfaf0" stroke="#0f100d" strokeWidth="3.5" />
          {/* handle */}
          <path d="M 38,16 Q 52,16 52,28 Q 52,40 38,40" stroke="#0f100d" strokeWidth="3.5" fill="none" />
          {/* coffee top */}
          <ellipse cx="19" cy="8" rx="18" ry="4" fill="#5b3a1f" stroke="#0f100d" strokeWidth="3" />
        </g>
      </g>
    </svg>
  );
}

function SleepingCat() {
  const [awake, setAwake] = useState(false);
  const onClick = () => {
    setAwake(true);
    setTimeout(() => setAwake(false), 1800);
  };
  return (
    <div style={{ position: 'relative', width: '100%' }} onClick={onClick}>
      <div style={{
        position: 'absolute',
        top: 4, right: '12%',
        opacity: awake ? 1 : 0,
        transform: awake ? 'translateY(0) scale(1)' : 'translateY(12px) scale(0.7)',
        transition: 'all 200ms cubic-bezier(0.25, 0.85, 0.3, 1.1)',
        background: '#fdfaf0',
        border: '3px solid #0f100d',
        padding: '6px 14px',
        borderRadius: 16,
        fontFamily: "'Caveat Brush', cursive",
        fontSize: 22,
        color: '#0f100d',
        boxShadow: '3px 3px 0 #0f100d',
        pointerEvents: 'none'
      }}>meow~ 😺</div>

      <svg viewBox="0 0 300 220" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', cursor: 'pointer' }}>
        <defs>
          <filter id="ct-rough" x="-2%" y="-2%" width="104%" height="104%">
            <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="2" seed="15" />
            <feDisplacementMap in="SourceGraphic" scale="1.2" />
          </filter>
        </defs>

        {/* Z's when sleeping */}
        {!awake && (
          <g style={{ animation: 'zfade 2.6s ease-in-out infinite' }}>
            <text x="200" y="80" fontFamily="'Bowlby One', sans-serif" fontSize="22" fill="#0f100d" opacity="0.7">z</text>
            <text x="220" y="56" fontFamily="'Bowlby One', sans-serif" fontSize="16" fill="#0f100d" opacity="0.55">z</text>
            <text x="236" y="38" fontFamily="'Bowlby One', sans-serif" fontSize="12" fill="#0f100d" opacity="0.4">z</text>
          </g>
        )}

        <g filter="url(#ct-rough)">
          {/* cushion */}
          <ellipse cx="150" cy="194" rx="120" ry="14" fill="#e89a6b" stroke="#0f100d" strokeWidth="4" />
          <ellipse cx="150" cy="188" rx="120" ry="10" fill="#f0b58a" stroke="#0f100d" strokeWidth="3" />

          {/* CAT BODY — curled */}
          <ellipse cx="150" cy="160" rx="86" ry="34" fill="#3a3a3a" stroke="#0f100d" strokeWidth="5" />
          {/* tail curled around */}
          <path d="M 66,160 Q 50,140 70,124 Q 100,116 100,150 Q 96,168 78,170"
                fill="#3a3a3a" stroke="#0f100d" strokeWidth="4.5" strokeLinejoin="round" />
          {/* tail tip white */}
          <ellipse cx="78" cy="168" rx="8" ry="6" fill="#fdfaf0" stroke="#0f100d" strokeWidth="3" />

          {/* HEAD */}
          <ellipse cx="194" cy="146" rx="42" ry="36" fill="#3a3a3a" stroke="#0f100d" strokeWidth="5" />
          {/* ear left */}
          <path d="M 166,118 L 162,86 L 190,108 Z" fill="#3a3a3a" stroke="#0f100d" strokeWidth="4" strokeLinejoin="round" />
          <path d="M 170,114 L 168,96 L 184,108 Z" fill="#e1497a" />
          {/* ear right */}
          <path d="M 224,118 L 232,90 L 212,110 Z" fill="#3a3a3a" stroke="#0f100d" strokeWidth="4" strokeLinejoin="round" />
          <path d="M 220,114 L 226,98 L 214,110 Z" fill="#e1497a" />

          {/* eyes */}
          {awake ? (
            <>
              <circle cx="180" cy="142" r="6" fill="#a6c8a0" stroke="#0f100d" strokeWidth="2" />
              <circle cx="210" cy="142" r="6" fill="#a6c8a0" stroke="#0f100d" strokeWidth="2" />
              <ellipse cx="180" cy="142" rx="1.5" ry="5" fill="#0f100d" />
              <ellipse cx="210" cy="142" rx="1.5" ry="5" fill="#0f100d" />
            </>
          ) : (
            <>
              <path d="M 172,142 Q 180,148 188,142" stroke="#0f100d" strokeWidth="3.5" fill="none" strokeLinecap="round" />
              <path d="M 202,142 Q 210,148 218,142" stroke="#0f100d" strokeWidth="3.5" fill="none" strokeLinecap="round" />
            </>
          )}

          {/* nose */}
          <path d="M 192,156 L 196,156 L 194,160 Z" fill="#e1497a" stroke="#0f100d" strokeWidth="2" strokeLinejoin="round" />
          {/* mouth */}
          {awake ? (
            <path d="M 188,164 Q 194,170 200,164" stroke="#0f100d" strokeWidth="3" fill="none" strokeLinecap="round" />
          ) : (
            <>
              <path d="M 194,162 L 194,166" stroke="#0f100d" strokeWidth="2.5" strokeLinecap="round" />
              <path d="M 188,168 Q 194,172 200,168" stroke="#0f100d" strokeWidth="2.5" fill="none" strokeLinecap="round" />
            </>
          )}

          {/* whiskers */}
          <path d="M 156,156 L 176,158 M 156,162 L 176,162 M 232,156 L 212,158 M 232,162 L 212,162"
                stroke="#0f100d" strokeWidth="1.5" />

          {/* paws */}
          <ellipse cx="108" cy="180" rx="14" ry="8" fill="#3a3a3a" stroke="#0f100d" strokeWidth="3.5" />
          <ellipse cx="138" cy="184" rx="14" ry="8" fill="#3a3a3a" stroke="#0f100d" strokeWidth="3.5" />
        </g>

        <style>{`
          @keyframes zfade {
            0%, 100% { opacity: 0; transform: translateY(0); }
            40%      { opacity: 1; }
            100%     { opacity: 0; transform: translateY(-14px); }
          }
        `}</style>
      </svg>
    </div>
  );
}

function PolaroidCamera() {
  const [flash, setFlash] = useState(false);
  const onClick = () => {
    setFlash(true);
    setTimeout(() => setFlash(false), 180);
  };
  return (
    <svg viewBox="0 0 280 240" xmlns="http://www.w3.org/2000/svg"
         style={{ width: '100%', cursor: 'pointer' }}
         onClick={onClick}>
      <defs>
        <filter id="cm-rough" x="-2%" y="-2%" width="104%" height="104%">
          <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="2" seed="16" />
          <feDisplacementMap in="SourceGraphic" scale="1.2" />
        </filter>
      </defs>

      <g filter="url(#cm-rough)">
        {/* shadow */}
        <ellipse cx="140" cy="220" rx="110" ry="6" fill="rgba(0,0,0,0.18)" />

        {/* polaroid body — boxy retro */}
        <rect x="40" y="50" width="200" height="160" rx="6" fill="#fdfaf0" stroke="#0f100d" strokeWidth="5" />
        {/* color stripe (rainbow ribbon) */}
        <rect x="40" y="50" width="200" height="14" fill="#e89a6b" stroke="#0f100d" strokeWidth="3" />
        <rect x="40" y="64" width="200" height="14" fill="#f0c64b" />
        <rect x="40" y="78" width="200" height="14" fill="#a6c8a0" />
        <rect x="40" y="92" width="200" height="14" fill="#6db8f0" />
        <rect x="40" y="50" width="200" height="56" fill="none" stroke="#0f100d" strokeWidth="3" />

        {/* lens housing (dark) */}
        <rect x="80" y="120" width="120" height="76" rx="4" fill="#1a1a1a" stroke="#0f100d" strokeWidth="4" />
        {/* lens */}
        <circle cx="140" cy="156" r="28" fill="#3a3a3a" stroke="#0f100d" strokeWidth="4" />
        <circle cx="140" cy="156" r="20" fill="#0a0a0a" stroke="#0f100d" strokeWidth="2.5" />
        <circle cx="140" cy="156" r="10" fill="#3271ad" />
        <circle cx="134" cy="150" r="3" fill="#fdfaf0" />

        {/* shutter button */}
        <circle cx="208" cy="60" r="8" fill="#e1497a" stroke="#0f100d" strokeWidth="2.5" />

        {/* flash bulb */}
        <rect x="60" y="58" width="20" height="14" rx="2" fill={flash ? '#fffce0' : '#c2d4a0'} stroke="#0f100d" strokeWidth="2.5" />
      </g>

      {/* FLASH effect — full overlay */}
      {flash && (
        <rect x="0" y="0" width="280" height="240" fill="#ffffff" opacity="0.65" />
      )}
    </svg>
  );
}

function WindChime() {
  const [ringing, setRinging] = useState(false);
  const srcRef = useRef(null);
  const gainRef = useRef(null);

  const onEnter = () => {
    setRinging(true);
    const ctx = getACtx();
    if (ctx.state === 'suspended') ctx.resume();
    const play = (buf) => {
      // stop any already-playing instance
      try { srcRef.current && srcRef.current.stop(); } catch(e) {}
      const src = ctx.createBufferSource();
      const g   = ctx.createGain();
      src.buffer = buf;
      src.loop   = true;
      src.connect(g); g.connect(ctx.destination);
      g.gain.setValueAtTime(0, ctx.currentTime);
      g.gain.linearRampToValueAtTime(0.7, ctx.currentTime + 0.15);
      src.start(ctx.currentTime, 1.0); // start 1s into file (best ring section)
      srcRef.current  = src;
      gainRef.current = g;
    };
    if (_chimeBuf) { play(_chimeBuf); return; }
    if (_chimeLoading) return;
    _chimeLoading = true;
    fetch('assets/windchime.mp3')
      .then(r => r.arrayBuffer())
      .then(ab => ctx.decodeAudioData(ab))
      .then(buf => { _chimeBuf = buf; _chimeLoading = false; play(buf); })
      .catch(() => { _chimeLoading = false; });
  };

  const onLeave = () => {
    setRinging(false);
    if (gainRef.current && srcRef.current) {
      const ctx = getACtx();
      gainRef.current.gain.setValueAtTime(gainRef.current.gain.value, ctx.currentTime);
      gainRef.current.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
      try { srcRef.current.stop(ctx.currentTime + 0.4); } catch(e) {}
      srcRef.current = null; gainRef.current = null;
    }
  };

  return (
    <svg viewBox="0 0 240 320" xmlns="http://www.w3.org/2000/svg"
         style={{ width: '100%', cursor: 'default' }}
         onMouseEnter={onEnter} onMouseLeave={onLeave}>
      <defs>
        <filter id="wc-rough" x="-2%" y="-2%" width="104%" height="104%">
          <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="2" seed="17" />
          <feDisplacementMap in="SourceGraphic" scale="1.2" />
        </filter>
      </defs>

      <g filter="url(#wc-rough)">
        {/* hanger ring */}
        <circle cx="120" cy="22" r="10" fill="none" stroke="#0f100d" strokeWidth="4" />
        {/* top hanger */}
        <line x1="120" y1="32" x2="120" y2="56" stroke="#0f100d" strokeWidth="4" />
        {/* top disc */}
        <ellipse cx="120" cy="60" rx="34" ry="6" fill="#b59672" stroke="#0f100d" strokeWidth="4" />

        {/* hanging strings */}
        <line x1="92"  y1="60" x2="92"  y2="120" stroke="#0f100d" strokeWidth="1.5" />
        <line x1="108" y1="60" x2="108" y2="120" stroke="#0f100d" strokeWidth="1.5" />
        <line x1="124" y1="60" x2="124" y2="120" stroke="#0f100d" strokeWidth="1.5" />
        <line x1="140" y1="60" x2="140" y2="120" stroke="#0f100d" strokeWidth="1.5" />

        {/* the 4 chime tubes — sway when ringing */}
        <g style={{
          transformOrigin: '120px 60px',
          animation: ringing ? 'swayLeft 0.4s ease-in-out infinite alternate' : 'none'
        }}>
          <rect x="86"  y="120" width="12" height="120" rx="6" fill="#f0c64b" stroke="#0f100d" strokeWidth="3.5" />
        </g>
        <g style={{
          transformOrigin: '120px 60px',
          animation: ringing ? 'swayRight 0.4s ease-in-out infinite alternate' : 'none'
        }}>
          <rect x="102" y="120" width="12" height="100" rx="6" fill="#e89a6b" stroke="#0f100d" strokeWidth="3.5" />
        </g>
        <g style={{
          transformOrigin: '120px 60px',
          animation: ringing ? 'swayLeft 0.45s ease-in-out infinite alternate' : 'none'
        }}>
          <rect x="118" y="120" width="12" height="130" rx="6" fill="#a6c8a0" stroke="#0f100d" strokeWidth="3.5" />
        </g>
        <g style={{
          transformOrigin: '120px 60px',
          animation: ringing ? 'swayRight 0.42s ease-in-out infinite alternate' : 'none'
        }}>
          <rect x="134" y="120" width="12" height="110" rx="6" fill="#6db8f0" stroke="#0f100d" strokeWidth="3.5" />
        </g>

        {/* sound waves when ringing */}
        {ringing && (
          <>
            <path d="M 60,170 Q 50,180 60,200" stroke="#0f100d" strokeWidth="2.5" fill="none" strokeLinecap="round" style={{ animation: 'soundWave 1s ease-out' }} />
            <path d="M 44,160 Q 30,180 44,210" stroke="#0f100d" strokeWidth="2.5" fill="none" strokeLinecap="round" style={{ animation: 'soundWave 1s ease-out 0.2s' }} opacity="0.6" />
            <path d="M 180,170 Q 190,180 180,200" stroke="#0f100d" strokeWidth="2.5" fill="none" strokeLinecap="round" style={{ animation: 'soundWave 1s ease-out' }} />
            <path d="M 196,160 Q 210,180 196,210" stroke="#0f100d" strokeWidth="2.5" fill="none" strokeLinecap="round" style={{ animation: 'soundWave 1s ease-out 0.2s' }} opacity="0.6" />
          </>
        )}

        {/* clapper / hanging bead */}
        <g style={{
          transformOrigin: '120px 60px',
          animation: ringing ? 'clapperSwing 0.5s ease-in-out infinite' : 'none'
        }}>
          <line x1="120" y1="60" x2="120" y2="260" stroke="#0f100d" strokeWidth="2" />
          <path d="M 110,260 L 130,260 L 130,272 L 110,272 Z" fill="#fdfaf0" stroke="#0f100d" strokeWidth="3" />
          {/* heart bead at bottom */}
          <path d="M 120,302 C 102,288 102,274 114,274 C 120,274 120,280 120,282 C 120,280 120,274 126,274 C 138,274 138,288 120,302 Z"
                fill="#e1497a" stroke="#0f100d" strokeWidth="3" strokeLinejoin="round" />
        </g>
      </g>

      <style>{`
        @keyframes swayLeft  { from { transform: rotate(-4deg); } to { transform: rotate(4deg); } }
        @keyframes swayRight { from { transform: rotate(4deg);  } to { transform: rotate(-4deg); } }
        @keyframes clapperSwing { 0%, 100% { transform: rotate(-6deg); } 50% { transform: rotate(6deg); } }
        @keyframes soundWave { 0% { opacity: 0; } 30% { opacity: 1; } 100% { opacity: 0; transform: translateX(-10px); } }
      `}</style>
    </svg>
  );
}

function GrowingPlant() {
  return (
    <svg viewBox="0 0 260 320" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%' }}>
      <defs>
        <filter id="gp-rough" x="-2%" y="-2%" width="104%" height="104%">
          <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="2" seed="18" />
          <feDisplacementMap in="SourceGraphic" scale="1.2" />
        </filter>
      </defs>
      <g filter="url(#gp-rough)">
        <ellipse cx="130" cy="296" rx="80" ry="6" fill="rgba(0,0,0,0.18)" />
        {/* stem */}
        <line x1="130" y1="232" x2="130" y2="110" stroke="#5b8848" strokeWidth="5" strokeLinecap="round" />
        {/* leaves */}
        <ellipse cx="100" cy="180" rx="20" ry="12" fill="#7aa860" stroke="#0f100d" strokeWidth="3" transform="rotate(-30 100 180)" />
        <ellipse cx="160" cy="180" rx="20" ry="12" fill="#5b8848" stroke="#0f100d" strokeWidth="3" transform="rotate(30 160 180)" />
        <ellipse cx="106" cy="145" rx="18" ry="11" fill="#7aa860" stroke="#0f100d" strokeWidth="3" transform="rotate(-25 106 145)" />
        <ellipse cx="154" cy="145" rx="18" ry="11" fill="#5b8848" stroke="#0f100d" strokeWidth="3" transform="rotate(25 154 145)" />
        {/* flower */}
        <ellipse cx="106" cy="100" rx="14" ry="10" fill="#e1497a" stroke="#0f100d" strokeWidth="3" />
        <ellipse cx="154" cy="100" rx="14" ry="10" fill="#e1497a" stroke="#0f100d" strokeWidth="3" />
        <ellipse cx="130" cy="78"  rx="10" ry="14" fill="#e1497a" stroke="#0f100d" strokeWidth="3" />
        <ellipse cx="130" cy="122" rx="10" ry="14" fill="#e1497a" stroke="#0f100d" strokeWidth="3" />
        <circle cx="130" cy="100" r="22" fill="#f0c64b" stroke="#0f100d" strokeWidth="3.5" />
        <circle cx="130" cy="100" r="10" fill="#c95c3a" stroke="#0f100d" strokeWidth="2.5" />
        {/* pot */}
        <path d="M 80,236 L 180,236 L 174,296 L 86,296 Z" fill="#c95c3a" stroke="#0f100d" strokeWidth="5" strokeLinejoin="round" />
        <rect x="76" y="230" width="108" height="14" fill="#c95c3a" stroke="#0f100d" strokeWidth="5" />
      </g>
    </svg>
  );
}

/* ─── About page overlay ─── */
function AboutPage({ open, onClose }) {
  useEffect(() => {
    if (open) document.body.style.overflow = 'hidden';
    else document.body.style.overflow = '';
    return () => { document.body.style.overflow = ''; };
  }, [open]);

  return (
    <>
      {/* green lead layer — slightly ahead of the main overlay */}
      <div className={`about-overlay-lead${open ? ' open' : ''}`} aria-hidden/>
      <div className={`about-overlay${open ? ' open' : ''}`} aria-hidden={!open}>
      {/* close button — same style as nav menu button, always shows × */}
      <button className="menu-btn open about-close" onClick={onClose} aria-label="Close">
        <span/><span/><span/>
      </button>

      <div className="about-inner">
        {/* left — photo */}
        <div className="about-photo-col">
          <div className="about-photo-frame">
            <img src="assets/soya-rig.svg" alt="Bingying" className="about-photo-img"/>
          </div>
          <div className="about-tags">
            <span className="about-tag">🐶 dog lover</span>
            <span className="about-tag">🎮 gamer</span>
            <span className="about-tag">🍰 cake fan</span>
            <span className="about-tag">🌏 based in AU</span>
          </div>
        </div>

        {/* right — text */}
        <div className="about-text-col">
          <p className="about-eyebrow">ABOUT ME</p>
          <h1 className="about-name">Bingying<br/>Zheng</h1>
          <p className="about-role">Digital Marketing Strategist</p>
          <p className="about-bio">
            I'm Bing, originally from China and now based in Melbourne. I started out
            in social media marketing for one of China's leading game companies, then
            moved to Australia to study digital marketing and business analytics.
          </p>
          <p className="about-bio">
            Since then I've been working in digital marketing across a non-profit and
            a boutique agency — mostly SEO, GEO, and paid search. Over time I got more
            into the technical side, and then into AI. These days I'm building AI agents
            and automation tools alongside my marketing work.
          </p>
          <p className="about-bio">
            I'm interested in where AI and digital marketing are heading, and I'm
            figuring that out by building things. If you're working on something in
            that space, I'd love to connect.
          </p>

          <div className="about-links">
            <a className="about-link-btn" href="https://www.linkedin.com/in/bingying-zheng-5826b3251" target="_blank" rel="noopener">LinkedIn ↗</a>
            <a className="about-link-btn secondary" href="mailto:bingyingz041@gmail.com?subject=Getting%20in%20touch&body=Hi%20Bingying%2C%0A%0AI%27m%20reaching%20out%20regarding%20a%20%5Byour%20role%2Fposition%5D%20opportunity.%0A%0A">Get in touch ↗</a>
          </div>
        </div>
      </div>
    </div>
    </>
  );
}

/* ─── Nav ─── */
function Nav({ onOpenAbout, onGoToWork, onGoToHuman }) {
  const [menuOpen, setMenuOpen] = useState(false);
  const close = () => setMenuOpen(false);

  return (
    <>
      <nav className="nav">
        <div className="logo">
          <b>Bingying</b>
          <small>digital marketing</small>
        </div>
        <div style={{ display:'flex', alignItems:'center', gap:14 }}>
          <a className="nav-li" href="https://www.linkedin.com/in/bingying-zheng-5826b3251" target="_blank" rel="noopener">in</a>
          <a className="cta" href="mailto:bingyingz041@gmail.com?subject=Getting%20in%20touch&body=Hi%20Bingying%2C%0A%0AI%27m%20reaching%20out%20regarding%20a%20%5Byour%20role%2Fposition%5D%20opportunity.%0A%0A" style={{ textDecoration:'none' }}>LET'S TALK ↗</a>
          <button className={`menu-btn${menuOpen ? ' open' : ''}`} onClick={() => { setMenuOpen(o => !o); sounds.click(); }} aria-label="Menu">
            <span/><span/><span/>
          </button>
        </div>
      </nav>

      {/* Dropdown menu */}
      <div className={`nav-menu${menuOpen ? ' open' : ''}`}>
        <button className="nav-menu-item" onClick={() => { window.scrollTo({top:0,behavior:'smooth'}); close(); }}>
          <span className="nm-num">01</span>Home
        </button>
        <button className="nav-menu-item" onClick={() => { onOpenAbout(); close(); }}>
          <span className="nm-num">02</span>About Me
        </button>
        <button className="nav-menu-item" onClick={() => { onGoToWork(); close(); }}>
          <span className="nm-num">03</span>Work
        </button>
        <button className="nav-menu-item" onClick={() => { onGoToHuman(); close(); }}>
          <span className="nm-num">04</span>The Human
        </button>
        <a className="nav-menu-item" href="https://www.linkedin.com/in/bingying-zheng-5826b3251" target="_blank" rel="noopener" onClick={close}>
          <span className="nm-num">05</span>LinkedIn ↗
        </a>
      </div>
    </>
  );
}

/* ─── PropCard ─── */
function PropCard({ data, style, className }) {
  const base = style || {};
  if (data.kind === 'channel') return (
    <div className={`prop-card channel ${className||''}`} style={base}>
      <span className="pin" style={{ background: data.pin }} />
      <div className="head"><span className="dot" style={{ background: data.dot }} /><span className="label">{data.label}</span></div>
      <div className="sub">{data.sub}</div>
    </div>
  );
  if (data.kind === 'metric') return (
    <div className={`prop-card metric ${className||''}`} style={base}>
      <span className="pin" style={{ background: data.pin }} />
      <div className="value">{data.value}</div>
      <div className="label">{data.label.toUpperCase()}</div>
    </div>
  );
  if (data.kind === 'tags') return (
    <div className={`prop-card tags ${className||''}`} style={base}>
      <span className="pin" style={{ background: data.pin }} />
      {data.items.map(it => <span key={it} className="chip">{it}</span>)}
    </div>
  );
  if (data.kind === 'note') return (
    <div className={`prop-card note ${className||''}`} style={base}>
      <span className="pin" style={{ background: data.pin }} />
      <div className="head">{data.head}</div>
      <p>{data.body}</p>
    </div>
  );
  return null;
}

/* ─── Hero scene ─── */
function HeroScene({ driftT, vw, onOpenAbout, onFrameHover, onPropHover, frameHover, onWorkClick }) {
  const dx = driftT * 320;
  const dy = driftT * 110;
  const dStyle = (xDir, yDir, deg) => ({ transform: `translate(${xDir*dx}px,${yDir*dy}px) rotate(${deg}deg)` });
  const fadeOp = Math.max(0, 1 - driftT * 1.5);
  const compact = vw <= 700;
  const frameInner = compact
    ? { w: 145, h: 112, pad: 8, label: 20 }
    : { w: 190, h: 148, pad: 10, label: 22 };

  return (
    <div className="hero-scene" style={{ pointerEvents: driftT < 0.25 ? 'auto' : 'none' }}>
      <div className="hero-title" style={{ transform: `translate(-50%, ${-driftT*55}px)`, opacity: Math.max(0, 1 - driftT*1.5) }}>
        <div className="l1">DIGITAL</div>
        <div className="l2">MARKETING</div>
      </div>

      {/* ── 上层：小件夹住标题 ── */}

      {/* 风铃 — 标题左侧 */}
      <div style={{ position:'absolute', display: compact ? 'none' : 'block', left:'30vw', top:'4vh', width:'clamp(58px,6.5vw,88px)', ...dStyle(-0.5,-0.15,4), opacity:fadeOp, cursor:'default' }}>
        <WindChime/>
      </div>

      {/* 黑板 — 标题右侧，与风铃呼应 */}
      <a href="mailto:bingyingz041@gmail.com?subject=Resume%20Request&body=Hi%20Bingying%2C%0A%0AI%27d%20love%20to%20see%20your%20resume.%20I%27m%20hiring%20for%20a%20%5Byour%20role%5D%20position%20at%20%5Bcompany%5D.%0A%0A"
        style={{ position:'absolute', display: compact ? 'none' : 'block', right:'22vw', top:'5vh', width:'clamp(118px,14vw,185px)', ...dStyle(0.5,-0.15,3), opacity:fadeOp, cursor:'pointer', textDecoration:'none' }}
        onMouseEnter={() => { sounds.hover(); onPropHover('chalk'); }} onMouseLeave={() => onPropHover(null)}>
        <ResumeChalkboard/>
      </a>

      {/* ── 中层：大件锚定左右 ── */}

      {/* 相框 — 左侧中部 */}
      <div style={{ position:'absolute', left: compact ? '3vw' : '4vw', top: compact ? '27vh' : '30vh', width: compact ? '160px' : 'clamp(170px,19vw,235px)', ...dStyle(-1,-0.3,-4), opacity:fadeOp, cursor:'pointer' }}
        onClick={onOpenAbout}
        onMouseEnter={() => { onFrameHover(true); sounds.hover(); }} onMouseLeave={() => onFrameHover(false)}>
        <div style={{ position:'relative', display:'inline-block' }}>
          <div style={{
            background:'#0f100d', padding: frameInner.pad + 'px',
            transform: 'rotate(-4deg)',
            display:'inline-block',
            transition: 'transform 200ms ease, box-shadow 200ms ease',
            boxShadow: frameHover ? '0 10px 28px rgba(0,0,0,0.45)' : '0 3px 10px rgba(0,0,0,0.25)',
          }}
          className={frameHover ? 'frame-hov' : ''}>
            <div style={{ width: frameInner.w + 'px', height: frameInner.h + 'px', overflow:'hidden', border:'2px solid #0f100d' }}>
              <img src="IMG_3633.JPG" alt="Bing" style={{ width:'100%', height:'160%', objectFit:'cover', objectPosition:'top', marginTop:'-10%',
                transition: 'transform 300ms ease', transform: frameHover ? 'scale(1.04)' : 'scale(1)' }}/>
            </div>
          </div>
          <div style={{
            marginTop: '10px',
            fontFamily: 'var(--marker)',
            fontSize: frameInner.label + 'px',
            color: 'var(--ink)',
            transform: 'rotate(-2deg)',
            textAlign: 'center',
            letterSpacing: '0.02em',
            opacity: 0.9,
          }}>about me?</div>
        </div>
      </div>

      {/* 书架 — 右侧中部，与相框对称 */}
      <div style={{ position:'absolute', right: compact ? '4vw' : '2vw', top: compact ? '25vh' : '24vh', width: compact ? '112px' : 'clamp(130px,16vw,205px)', ...dStyle(1,-0.3,-3), opacity:fadeOp, cursor:'pointer' }}>
        <Bookshelf/>
      </div>

      {/* ── 下层：地面感 ── */}

      {/* 黑胶 — 左下锚点 */}
      <div style={{ position:'absolute', left: compact ? '6vw' : '4vw', bottom: compact ? '8vh' : '7vh', width: compact ? '126px' : 'clamp(148px,17vw,225px)', ...dStyle(-1,0.35,-3), opacity:fadeOp, cursor:'pointer' }}
        onMouseEnter={() => { sounds.hover(); onPropHover('vinyl'); }} onMouseLeave={() => onPropHover(null)}>
        <VinylPlayer onPlayChange={v => { onPropHover(v ? 'vinyl' : null); }}/>
      </div>

      {/* 植物 — 中间过渡，角色脚边 */}
      <div style={{ position:'absolute', display: compact ? 'none' : 'block', left:'27vw', bottom:'9vh', width:'clamp(52px,6vw,80px)', ...dStyle(-0.5,0.2,5), opacity:fadeOp, cursor:'default' }}>
        <GrowingPlant/>
      </div>

      {/* 书桌 — 右下锚点 */}
      <div style={{ position:'absolute', right: compact ? '2vw' : '3vw', bottom: compact ? '2vh' : '4vh', width: compact ? '142px' : 'clamp(188px,25vw,340px)', ...dStyle(1,0.4,-2), opacity:fadeOp, cursor:'pointer' }}
        onMouseEnter={() => onPropHover('desk')} onMouseLeave={() => onPropHover(null)}>
        <DeskScene onWorkClick={onWorkClick}/>
      </div>


    </div>
  );
}

/* ─── Character — fixed, centered, scroll-driven morph ─── */
function Character({ scrollY, vh, vw, sectionIdx, frameHover, propHover }) {
  const [blink, setBlink]       = useState(false);
  const [reaction, setReaction] = useState(null);
  const [mouse, setMouse]       = useState({ x: -1, y: -1 });
  const [hover, setHover]       = useState(false);
  const [illos, setIllos]       = useState([]);
  const reactionTimer  = useRef(null);
  const illoTimer      = useRef(null);
  const rafPupil       = useRef(null);
  const reactionTick   = useRef(0);
  const lastReactionKey = useRef(null);

  useEffect(() => {
    let t;
    const schedule = () => {
      t = setTimeout(() => {
        setBlink(true);
        setTimeout(() => setBlink(false), 140);
        schedule();
      }, 2400 + Math.random() * 3200);
    };
    schedule();
    return () => clearTimeout(t);
  }, []);

  useEffect(() => {
    const onMove = (e) => {
      if (rafPupil.current) return;
      rafPupil.current = requestAnimationFrame(() => {
        // only update state when in hero — avoids re-renders during scroll
        if (!rafPupil._isHero) { rafPupil.current = null; return; }
        setMouse({ x: e.clientX, y: e.clientY });
        rafPupil.current = null;
      });
    };
    window.addEventListener('mousemove', onMove, { passive: true });
    return () => { window.removeEventListener('mousemove', onMove); cancelAnimationFrame(rafPupil.current); };
  }, []);


  /* transition thresholds */
  const TRANS_START = vh * 1.0;
  const TRANS_END   = vh * 2.2;
  const rawT = clamp((scrollY - TRANS_START) / (TRANS_END - TRANS_START), 0, 1);
  const t    = ease(rawT);

  const isHero = rawT < 0.05;
  rafPupil._isHero = isHero;

  /* size
     hero: full-body 2:3 figure, scaled down on narrow screens
     post: wider, container height = top 55% of natural height (waist-up crop via overflow:hidden) */
  const heroW = vw <= 700 ? Math.min(280, Math.round(vw * 0.72)) : 320;
  const heroH = Math.round(heroW * 1.5);
  const postW    = Math.min(360, Math.round(vw * 0.32));
  const postNatH = Math.round(postW * 1.5);    // full proportional height
  const postVisH = Math.round(postNatH * 0.55); // visible portion (overflow-hidden)

  const curW     = Math.round(lerp(heroW, postW, t));
  const curContH = Math.round(lerp(heroH, postVisH, t)); // container height

  /* always centered */
  const curLeft = Math.round(vw / 2 - curW / 2);

  /* top: hero = feet at viewport bottom; post = bottom of visible crop ~20px above fold */
  const heroTop = vh - heroH;
  const postTop = vh - postVisH - 20;
  const curTop  = Math.round(lerp(heroTop, postTop, t));

  /* expression overlay must match the FULL rendered img size (overflows container, gets clipped) */
  const fullImgH = Math.round(curW * 1.5);

  /* pupil tracking — map mouse → SVG offset (max ±12 units) */
  const faceCX = curLeft + curW / 2;
  const faceCY = curTop  + Math.round(fullImgH * 0.276); // eyes ~27.6% down full img
  const MAX_P  = 12;
  const pdx = mouse.x < 0 ? 0 : clamp((mouse.x - faceCX) / (vw  * 0.5), -1, 1);
  const pdy = mouse.y < 0 ? 0 : clamp((mouse.y - faceCY) / (vh  * 0.4), -1, 1);
  const px  = Math.round(pdx * MAX_P);
  const py  = Math.round(pdy * MAX_P);

  const showMood    = rawT < 0.25;  // mood overlays hero-only; blink always runs
  const signOpacity = ease(clamp((rawT - 0.50) / 0.38, 0, 1));

  const mood = reaction ? reaction.mood : null;

  const onCharClick = () => {
    if (!isHero) return;
    sounds.pop();
    const pool = REACTIONS.filter(r => r.key !== lastReactionKey.current);
    const r = pool[Math.floor(Math.random() * pool.length)];
    lastReactionKey.current = r.key;
    reactionTick.current += 1;
    setReaction({ ...r, _tick: reactionTick.current });
    clearTimeout(reactionTimer.current);
    reactionTimer.current = setTimeout(() => setReaction(null), 2400);
    // spawn illustrations
    const id = Date.now();
    const spread = [
      { dx: -curW / 2 - 20, dy: 10,  rot: -12, delay: 0   },
      { dx:  curW / 2 - 30, dy: -15, rot:  9,  delay: 100 },
    ];
    setIllos(spread.map((p, i) => ({
      id: id + i, type: r.illo,
      left: vw / 2 + p.dx,
      top:  curTop + curContH / 2 + p.dy,
      rot: p.rot, delay: p.delay,
    })));
    clearTimeout(illoTimer.current);
    illoTimer.current = setTimeout(() => setIllos([]), 2600);
  };

  const chap = CHAPTERS[clamp(sectionIdx, 0, CHAPTERS.length - 1)];

  /* sign: anchor bottom so sign straddles the arm level
     arms in SVG ≈ 47% of full height → arm viewport y = curTop + fullImgH * 0.47 */
  const armVY   = curTop + Math.round(fullImgH * 0.52);
  const signH   = 130; // approximate rendered card height
  const signBot = Math.max(Math.round(vh - armVY - signH * 0.35), 20);

  return (
    <>
      {/* ── Art container — overflow:hidden crops the bottom naturally ── */}
      <div
        style={{
          position: 'fixed',
          left: 0, top: 0,
          transform: `translate(${curLeft}px, ${curTop}px)`,
          width:    curW     + 'px',
          height:   curContH + 'px',
          overflow: 'hidden',
          zIndex:   10,
          pointerEvents: isHero ? 'auto' : 'none',
          cursor:   isHero ? 'pointer' : 'default',
          filter:   'drop-shadow(0 20px 14px rgba(0,0,0,0.20))',
          willChange: 'transform',
        }}
        onClick={onCharClick}
        onMouseEnter={() => { isHero && setHover(true); sounds.hover(); }}
        onMouseLeave={() => setHover(false)}
      >
        <div className="character-art" style={{ position:'absolute', top:0, left:0, width:'100%', transformOrigin:'50% 88%' }}>
          {/* single asset — always soya-rig.svg */}
          <img src="assets/soya-rig.svg" alt="" style={{ display:'block', width:'100%', height:'auto' }}/>

          {/* expression overlay — always rendered so blink works in all phases */}
          <svg
            className="expr"
            viewBox="0 0 1024 1536"
            xmlns="http://www.w3.org/2000/svg"
            style={{ position:'absolute', top:0, left:0, width: curW+'px', height: fullImgH+'px', pointerEvents:'none' }}
          >
            {/* moving pupils — hidden during blink */}
            <g opacity={blink ? 0 : 1}>
              {/* eye white */}
              <ellipse cx="444" cy="418" rx="38" ry="40" fill="#fff"/>
              <ellipse cx="594" cy="418" rx="38" ry="40" fill="#fff"/>
              {/* black pupil, tracks mouse only when tracking=true */}
              <circle cx={444 + px} cy={418 + py} r="20" fill="#0A0A08"/>
              <circle cx={594 + px} cy={418 + py} r="20" fill="#0A0A08"/>
            </g>

            {/* hover — smile + cheek blush */}
            <g opacity={(hover && !blink && !mood) ? 1 : 0}>
              <ellipse cx="519" cy="504" rx="46" ry="18" fill="#FBDCBF"/>
              <path d="M 484,496 Q 519,544 554,496" stroke="#0F100D" strokeWidth="9" fill="none" strokeLinecap="round"/>
              <ellipse cx="390" cy="480" rx="44" ry="24" fill="#F4A8B8" opacity="0.55"/>
              <ellipse cx="648" cy="480" rx="44" ry="24" fill="#F4A8B8" opacity="0.55"/>
            </g>

            {/* blink — always active */}
            <g opacity={blink ? 1 : 0}>
              <ellipse cx="428" cy="423" rx="62" ry="64" fill="#FBDCBF"/>
              <ellipse cx="610" cy="423" rx="62" ry="64" fill="#FBDCBF"/>
              <path d="M 374,430 Q 428,452 482,430" stroke="#0F100D" strokeWidth="8" fill="none" strokeLinecap="round"/>
              <path d="M 556,430 Q 610,452 664,430" stroke="#0F100D" strokeWidth="8" fill="none" strokeLinecap="round"/>
            </g>
            {/* mood overlays — hero only */}
            <g opacity={(showMood && mood === 'heart') ? 1 : 0}>
              <ellipse cx="428" cy="423" rx="62" ry="64" fill="#FBDCBF"/>
              <ellipse cx="610" cy="423" rx="62" ry="64" fill="#FBDCBF"/>
              <path d="M 428,460 C 388,420 388,388 410,388 C 420,388 428,398 428,410 C 428,398 436,388 446,388 C 468,388 468,420 428,460 Z" fill="#e1497a" stroke="#0F100D" strokeWidth="3"/>
              <path d="M 610,460 C 570,420 570,388 592,388 C 602,388 610,398 610,410 C 610,398 618,388 628,388 C 650,388 650,420 610,460 Z" fill="#e1497a" stroke="#0F100D" strokeWidth="3"/>
            </g>
            <g opacity={(showMood && (mood === 'sparkle' || mood === 'blush')) ? 1 : 0}>
              {/* cover original mouth */}
              <ellipse cx="519" cy="504" rx="46" ry="18" fill="#FBDCBF"/>
              {/* simple line smile */}
              <path d="M 484,496 Q 519,544 554,496" stroke="#0F100D" strokeWidth="9" fill="none" strokeLinecap="round"/>
            </g>
            <g opacity={(showMood && mood === 'sparkle') ? 1 : 0}>
              <path d="M 280,330 l 5,-14 5,14 14,5 -14,5 -5,14 -5,-14 -14,-5 z" fill="#f0c64b"/>
              <path d="M 760,330 l 4,-12 4,12 12,4 -12,4 -4,12 -4,-12 -12,-4 z" fill="#f0c64b"/>
              <path d="M 240,560 l 3,-10 3,10 10,3 -10,3 -3,10 -3,-10 -10,-3 z" fill="#6db8f0"/>
              <path d="M 790,560 l 3,-10 3,10 10,3 -10,3 -3,10 -3,-10 -10,-3 z" fill="#e1497a"/>
            </g>
            <g opacity={(showMood && mood === 'blush') ? 1 : 0}>
              <ellipse cx="393" cy="476" rx="40" ry="22" fill="#F4A8B8" opacity="0.65"/>
              <ellipse cx="647" cy="476" rx="40" ry="22" fill="#F4A8B8" opacity="0.65"/>
            </g>
          </svg>
        </div>

      </div>

      {/* ── Reaction bubble — fixed, above character head, never clipped ── */}
      {reaction && isHero && (
        <div className="reaction-bubble" key={`rb-${reaction._tick}`} style={{
          position: 'fixed',
          bottom: (vh - curTop - 40) + 'px',
          left: '50%',
        }}>
          <span className="text">{reaction.text}</span>
        </div>
      )}

      {/* ── Frame-hover: "learn more" bubble + radiating lines ── */}
      {frameHover && isHero && (
        <>
          {/* speech bubble */}
          <div className="frame-bubble" style={{
            position: 'fixed',
            bottom: (vh - curTop - 40) + 'px',
            left: '50%',
            transform: 'translateX(-50%)',
            zIndex: 18,
          }}>
            learn more about me ✦
          </div>
          {/* radiating lines around character */}
          <svg className="char-rays" style={{
            position: 'fixed',
            left: curLeft - 40 + 'px',
            top:  curTop  - 40 + 'px',
            width:  curW  + 80 + 'px',
            height: curContH + 80 + 'px',
            pointerEvents: 'none',
            zIndex: 8,
            overflow: 'visible',
          }}>
            {[0,45,90,135,180,225,270,315].map((deg, i) => {
              const rad = deg * Math.PI / 180;
              const cx = curW / 2 + 40, cy = curContH / 2 + 40;
              const r1 = 180, r2 = 230;
              return <line key={i}
                x1={cx + Math.cos(rad) * r1} y1={cy + Math.sin(rad) * r1}
                x2={cx + Math.cos(rad) * r2} y2={cy + Math.sin(rad) * r2}
                stroke="var(--ink)" strokeWidth="3" strokeLinecap="round"
                className="char-ray"
                style={{ animationDelay: i * 40 + 'ms' }}
              />;
            })}
          </svg>
        </>
      )}

      {/* ── Prop hover reaction — character comments on hovered prop ── */}
      {propHover && isHero && (() => {
        const msgs = {
          vinyl: '♪ classical time~',
          desk:  'check my work highlights~',
          chalk: 'check my resume!',
        };
        const msg = msgs[propHover];
        if (!msg) return null;
        return (
          <div className="frame-bubble" style={{
            position: 'fixed',
            bottom: (vh - curTop - 40) + 'px',
            left: '50%',
            transform: 'translateX(-50%)',
            zIndex: 18,
            background: 'var(--paper)',
          }}>
            {msg}
          </div>
        );
      })()}

      {/* ── Scroll hint at character's feet ── */}
      <div style={{
        position: 'fixed', bottom: '20px', left: '50%',
        transform: 'translateX(-50%)',
        zIndex: 12, opacity: Math.max(0, 1 - rawT * 8),
        pointerEvents: 'none',
        display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '5px',
      }}>
        <span style={{ fontFamily:'var(--mono)', fontSize:'9px', letterSpacing:'0.22em', color:'var(--ink)', opacity:0.45 }}>SCROLL</span>
        <div className="scroll-arrow"/>
      </div>

      {/* ── Floating illustrations ── */}
      {illos.map(il => (
        <div key={il.id} className="illo-float" style={{
          position: 'fixed',
          left: il.left + 'px',
          top:  il.top  + 'px',
          '--rot': il.rot + 'deg',
          animationDelay: il.delay + 'ms',
          zIndex: 20,
          pointerEvents: 'none',
        }}>
          <IlloSVG type={il.type}/>
        </div>
      ))}

      {/* ── Shirt trapezoid — small clothing peek below sign ── */}
      <div style={{
        position: 'fixed',
        bottom: Math.max(signBot - 52, 0) + 'px',
        left: '50%',
        transform: 'translateX(-50%)',
        width: Math.round(curW * 0.32) + 'px',
        height: '52px',
        background: '#fff',
        clipPath: 'polygon(22% 0%, 78% 0%, 90% 100%, 10% 100%)',
        zIndex: 9,
        opacity: signOpacity,
        pointerEvents: 'none',
      }}/>

      {/* ── Held sign — separate fixed element, always fully visible ── */}
      <div style={{
        position: 'fixed',
        bottom: signBot + 'px',
        left: '50%',
        transform: 'translateX(-50%) rotate(-3deg)',
        zIndex: 11, opacity: signOpacity, pointerEvents: 'none',
      }}>
        <div className="section-card-held">
          <span className="grip l"/><span className="grip r"/>
          <span className="kicker">{chap.chapter}</span>
          <span className="title">{chap.label}</span>
        </div>
      </div>
    </>
  );
}

/* ─── Per-section card layouts — 4 variants ─── */
/*
  Each section has a "left column" and "right column" flanking the centered character.
  The character occupies ~460px in the center, so each column gets the rest.
  Layouts differ in which cards go left vs right and their arrangement.
*/
function SectionCards({ chapter, idx }) {
  const [c0, c1, c2, c3] = chapter.cards;
  const v = idx % 4;

  if (v === 0) return (
    <>
      <div className="sc-col sc-left">
        <PropCard data={c0} className="enter-left"/>
        <PropCard data={c1} className="enter-left"/>
      </div>
      <div className="sc-col sc-right">
        <PropCard data={c2} className="enter-right"/>
        <PropCard data={c3} className="enter-right"/>
      </div>
    </>
  );

  if (v === 1) return (
    <>
      <div className="sc-col sc-left">
        <PropCard data={c1} className="enter-left"/>
        <PropCard data={c3} className="enter-left"/>
      </div>
      <div className="sc-col sc-right">
        <PropCard data={c0} className="enter-right"/>
        <PropCard data={c2} className="enter-right"/>
      </div>
    </>
  );

  if (v === 2) return (
    <>
      <div className="sc-col sc-left sc-left--wide">
        <PropCard data={c2 && c2.kind === 'note' ? c2 : c0} className="enter-left"/>
        <PropCard data={c3} className="enter-left"/>
      </div>
      <div className="sc-col sc-right">
        <PropCard data={c1} className="enter-right"/>
      </div>
    </>
  );

  /* v === 3 — case study: big metric left, note right */
  return (
    <>
      <div className="sc-col sc-left">
        <PropCard data={c1} className="enter-left metric-hero"/>
        <PropCard data={c0} className="enter-left"/>
      </div>
      <div className="sc-col sc-right">
        <PropCard data={c2 && c2.kind === 'note' ? c2 : c2} className="enter-right"/>
        <PropCard data={c3} className="enter-right"/>
      </div>
    </>
  );
}

/* ─── SEO & GEO showcase section ─── */
function SEOGEOSection() {
  return (
    <section className="seo-section">

      {/* ── Chapter header ── */}
      <div className="seo-header">
        <div className="seo-eyebrow">CASE 05</div>
        <h2 className="seo-title">SEO &amp; GEO</h2>
        <p className="seo-subtitle">
          Search engine optimisation · Generative engine optimisation · AI-assisted audits
        </p>
      </div>

      {/* ── Healthcare example screenshots ── */}
      <div className="seo-screenshots">
        <div className="seo-shot seo-shot-1">
          <div className="seo-shot-label">Organic keyword growth</div>
          <img src="SEO%26GEO/Health%20care%201.png" alt="Organic keywords 6,580 — healthcare client" loading="lazy"/>
        </div>
        <div className="seo-shot seo-shot-2">
          <div className="seo-shot-label">New users by channel</div>
          <img src="SEO%26GEO/health%20care%202.png" alt="89K organic search new users — healthcare client" loading="lazy"/>
        </div>
        <div className="seo-shot seo-shot-3">
          <div className="seo-shot-label">Top search queries</div>
          <img src="SEO%26GEO/health%20care%203.png" alt="Top queries in Search Console — healthcare client" loading="lazy"/>
        </div>
      </div>

      {/* ── AI agent description ── */}
      <div className="seo-agent-row">
        <div className="seo-agent-pill">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/>
          </svg>
          AI agent · audit &amp; report generation
        </div>
        <p className="seo-agent-desc">
          I run AI agents that crawl, audit, and generate full SEO &amp; GEO reports —<br/>
          technical issues, content gaps, AI search visibility, and recommendations in one pass.
        </p>
      </div>

      {/* ── Dashboard iframe ── */}
      <div className="seo-dashboard-wrap">
        {/* hand-drawn annotation */}
        <div className="seo-hand-note">
          <svg className="seo-arrow-svg" viewBox="0 0 80 50" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M 4,8 C 20,6 48,4 72,30" stroke="#0f100d" strokeWidth="2.5" strokeLinecap="round" fill="none"
              style={{ strokeDasharray: 90, strokeDashoffset: 0 }}/>
            <path d="M 68,22 L 72,30 L 62,28" stroke="#0f100d" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
          </svg>
          <span className="seo-hand-text">see the example report — scroll &amp; click!</span>
        </div>

        <div className="seo-iframe-container">
          <iframe
            src="SEO%26GEO/Origin%20Energy/index.html"
            title="Origin Energy SEO &amp; GEO Audit Dashboard"
            className="seo-iframe"
            scrolling="yes"
          />
        </div>

        {/* bottom hand-drawn hint */}
        <div className="seo-hand-bottom">
          <span className="seo-hand-text seo-hand-text--sm">↑ fully interactive — built with Claude Code</span>
        </div>
      </div>

    </section>
  );
}

/* ─── Click particle burst (green theme) ─── */
function ClickParticles() {
  const [bursts, setBursts] = useState([]);

  useEffect(() => {
    const onClick = (e) => {
      const shapes = ['★', '✦', '+', '●', '♡'];
      const greens = ['#a6c8a0', '#7ab87a', '#c8e6c4', '#5a9c5a', '#d4edda'];
      const id = Date.now() + Math.random();
      const particles = Array.from({ length: 6 }, (_, i) => ({
        id: i,
        shape: shapes[Math.floor(Math.random() * shapes.length)],
        color: greens[Math.floor(Math.random() * greens.length)],
        dx: (Math.random() - 0.5) * 80,
        dy: -(20 + Math.random() * 60),
        size: 10 + Math.random() * 12,
        rot: (Math.random() - 0.5) * 120,
      }));
      setBursts(b => [...b, { id, x: e.clientX, y: e.clientY, particles }]);
      setTimeout(() => setBursts(b => b.filter(bst => bst.id !== id)), 700);
    };
    window.addEventListener('click', onClick);
    return () => window.removeEventListener('click', onClick);
  }, []);

  return (
    <div style={{ position:'fixed', inset:0, pointerEvents:'none', zIndex:9999 }}>
      {bursts.map(burst => burst.particles.map(p => (
        <span key={burst.id + '-' + p.id} style={{
          position: 'absolute',
          left: burst.x,
          top: burst.y,
          fontSize: p.size + 'px',
          color: p.color,
          fontWeight: 'bold',
          lineHeight: 1,
          transform: 'translate(-50%,-50%)',
          animation: 'cp-fly 0.65s ease-out forwards',
          '--cp-dx': p.dx + 'px',
          '--cp-dy': p.dy + 'px',
          '--cp-rot': p.rot + 'deg',
        }}>{p.shape}</span>
      )))}
    </div>
  );
}

/* ─── The Human section ─── */
function Polaroid({ src, rot, mt, size }) {
  const [hov, setHov] = useState(false);
  const w = size || 150;
  const h = Math.round(w * 0.78);
  return (
    /* outer: fixed rotation + layout, never changes size so no sibling flicker */
    <div
      onMouseEnter={() => setHov(true)}
      onMouseLeave={() => setHov(false)}
      style={{
        transform: `rotate(${rot}deg)`,
        marginTop: mt || 0,
        width: w + 'px',
        flexShrink: 0,
        zIndex: hov ? 10 : 1,
        position: 'relative',
        cursor: 'pointer',
      }}
    >
      {/* inner: scale only, so the hover hit-box stays the same size */}
      <div style={{
        background: '#fdfaf0',
        padding: '8px 8px 28px',
        boxShadow: hov ? '8px 12px 28px rgba(0,0,0,0.55)' : '4px 6px 18px rgba(0,0,0,0.38)',
        transform: `scale(${hov ? 1.12 : 1})`,
        transformOrigin: '50% 50%',
        transition: 'transform 0.22s cubic-bezier(0.22,1,0.36,1), box-shadow 0.22s ease',
        position: 'relative',
      }}>
        <div style={{
          position: 'absolute', top: '-10px', left: '50%',
          transform: 'translateX(-50%) rotate(-1.5deg)',
          width: '52px', height: '14px',
          background: 'rgba(240,198,75,0.82)',
          border: '1px solid rgba(0,0,0,0.18)',
        }}/>
        <img src={src} loading="lazy" decoding="async" style={{ width: '100%', height: h + 'px', objectFit: 'cover', display: 'block' }}/>
      </div>
    </div>
  );
}

function HumanSection() {
  /* Three columns — each activity takes one column, no center gap needed.
     We break out of cs-inner and do a custom 3-col layout so content fills the width
     and nothing sits behind the character (character floats on top). */
  const col = {
    display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '14px',
    flex: '1 1 0', minWidth: 0,
  };
  const title = {
    fontFamily: 'var(--display)', fontSize: 'clamp(15px,1.7vw,21px)', color: 'var(--ink)',
  };
  const note = {
    fontFamily: 'var(--mono)', fontSize: '10px', color: 'var(--ink)',
    opacity: 0.55, lineHeight: 1.75, marginTop: '5px', textAlign: 'center',
  };

  return (
    <div style={{
      width: '100%', height: '100%',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      gap: 'clamp(16px,3vw,48px)',
      padding: '52px clamp(20px,4vw,64px) 60px',
      boxSizing: 'border-box',
    }}>

      {/* ── Cake Socials ── */}
      <div style={col}>
        {/* row 1 */}
        <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end' }}>
          <Polaroid src="The%20Human/webp/cake.webp"           rot={-6} size={138}/>
          <Polaroid src="The%20Human/webp/cake-top2.webp"  rot={4}  size={138} mt={22}/>
        </div>
        {/* row 2 */}
        <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start' }}>
          <Polaroid src="The%20Human/webp/cake2.webp"           rot={4}  size={128}/>
          <Polaroid src="The%20Human/webp/cake-group.webp"    rot={-3} size={148} mt={-12}/>
        </div>
        <div style={{ textAlign: 'center', marginTop: '4px' }}>
          <div style={title}>Cake Socials</div>
          <div style={note}>Built a Melbourne cake community<br/>from 0 — ~800 members in 2 months.<br/>Partnered with 10+ local cake shops.</div>
        </div>
      </div>

      {/* ── Alumni Network ── */}
      <div style={{ ...col, marginTop: '-60px' }}>
        <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end' }}>
          <Polaroid src="The%20Human/webp/Alumni1.webp" rot={-4} size={148}/>
          <Polaroid src="The%20Human/webp/Alumni2.webp"    rot={5}  size={148} mt={20}/>
        </div>
        <div style={{ textAlign: 'center', marginTop: '4px' }}>
          <div style={title}>Alumni Network</div>
          <div style={note}>Helped organise events for<br/>Nottingham alumni in Australia.</div>
        </div>
      </div>

      {/* ── Dubbing Club ── */}
      <div style={col}>
        <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end' }}>
          <Polaroid src="The%20Human/webp/Dubbing.webp"                rot={3}  size={148}/>
          <Polaroid src="The%20Human/webp/Dubbing-competition.webp"  rot={-5} size={148} mt={16}/>
        </div>
        <div style={{ textAlign: 'center', marginTop: '4px' }}>
          <div style={title}>Dubbing Club</div>
          <div style={note}>Club president at university —<br/>ran events, competitions,<br/>and judging panels end-to-end.</div>
        </div>
      </div>

    </div>
  );
}

/* ─── Back to top button ─── */
function BackToTopBtn() {
  return (
    <button
      onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
      style={{
        position: 'absolute', bottom: '24px', right: '28px',
        fontFamily: 'var(--marker)', fontSize: '16px',
        color: 'var(--ink)', background: 'var(--paper)',
        border: '2.5px solid var(--ink)', borderRadius: '999px',
        padding: '5px 16px', cursor: 'pointer',
        boxShadow: '3px 3px 0 var(--ink)',
        transition: 'transform .15s, box-shadow .15s',
        zIndex: 20,
      }}
      onMouseEnter={e => { e.currentTarget.style.transform = 'translate(-1px,-1px)'; e.currentTarget.style.boxShadow = '4px 4px 0 var(--ink)'; }}
      onMouseLeave={e => { e.currentTarget.style.transform = ''; e.currentTarget.style.boxShadow = '3px 3px 0 var(--ink)'; }}
    >↑ home</button>
  );
}

/* ─── Work hub cards ─── */
function WorkCard({ data, dir }) {
  const live = !!data.href;
  return (
    <a href={live ? data.href : undefined} style={{
      display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
      padding: '12px 14px 10px',
      border: '2.5px solid var(--ink)', borderRadius: '12px',
      background: 'var(--paper)',
      boxShadow: live ? '4px 4px 0 var(--ink)' : '2px 2px 0 var(--ink)',
      textDecoration: 'none', color: 'var(--ink)',
      position: 'relative', overflow: 'hidden',
      minHeight: '0',
      transition: 'transform 0.15s, box-shadow 0.15s',
      animation: `${dir === 'left' ? 'slideL' : 'slideR'} 0.5s ease both`,
      opacity: live ? 1 : 0.38,
      filter: live ? 'none' : 'grayscale(0.4)',
      cursor: live ? 'pointer' : 'default',
      pointerEvents: live ? 'auto' : 'none',
    }}
    onMouseEnter={e => { if (live) { e.currentTarget.style.transform = 'translate(-2px,-3px)'; e.currentTarget.style.boxShadow = '6px 7px 0 var(--ink)'; }}}
    onMouseLeave={e => { e.currentTarget.style.transform = ''; e.currentTarget.style.boxShadow = live ? '4px 4px 0 var(--ink)' : '2px 2px 0 var(--ink)'; }}
    >
      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '4px', background: data.accent, borderRadius: '10px 10px 0 0' }}/>
      <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between', height: '100%' }}>
        <div style={{ fontFamily: 'var(--display)', fontSize: 'clamp(16px, 1.8vw, 24px)', lineHeight: 1, marginTop: '4px' }}>{data.title}</div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '6px' }}>
          <span style={{ fontSize: '8px', fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', padding: '2px 8px', borderRadius: '999px', background: data.accent, color: data.darkTag ? 'var(--ink)' : 'var(--paper)' }}>
            {live ? data.tag : 'coming soon'}
          </span>
          {live && <span style={{ fontSize: '16px', fontWeight: 700 }}>→</span>}
        </div>
      </div>
    </a>
  );
}

function WorkHubSection() {
  return (
    <>
      <div className="sc-col sc-left" style={{ alignItems: 'stretch' }}>
        {WORK_CARDS.slice(0, 3).map(c => <WorkCard key={c.num} data={c} dir="left"/>)}
      </div>

      {/* center column annotation — sits above the character */}
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start', paddingTop: '32px', gap: '8px', pointerEvents: 'none' }}>
        <svg width="48" height="40" viewBox="0 0 48 40" fill="none">
          <path d="M 6,6 C 16,4 38,6 42,28" stroke="var(--ink)" strokeWidth="2.2" strokeLinecap="round"/>
          <path d="M 38,22 L 42,28 L 35,27" stroke="var(--ink)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
        <span style={{ fontFamily: 'var(--marker)', fontSize: 'clamp(18px, 2vw, 24px)', color: 'var(--ink)', textAlign: 'center', lineHeight: 1.25, whiteSpace: 'pre-line' }}>
          {'click to explore\nmy work!'}
        </span>
      </div>

      <div className="sc-col sc-right" style={{ alignItems: 'stretch' }}>
        {WORK_CARDS.slice(3, 6).map(c => <WorkCard key={c.num} data={c} dir="right"/>)}
      </div>
    </>
  );
}

/* ─── App ─── */
function App() {
  const [scrollY,    setScrollY]    = useState(0);
  const [dims,       setDims]       = useState({ vh: window.innerHeight, vw: window.innerWidth });
  const [showAbout,  setShowAbout]  = useState(false);
  const [frameHover,    setFrameHover]    = useState(false);
  const [propHover,     setPropHover]     = useState(null);
  const [activeSection, setActiveSection] = useState(0);
  const sectionRefs = useRef([]);

  const goToSection = (idx) => {
    sounds.pop();
    const el = sectionRefs.current[idx];
    if (el) el.scrollIntoView({ behavior: 'smooth' });
  };
  const goToWork   = () => goToSection(0);
  const goToHuman  = () => goToSection(1);

  useEffect(() => {
    const onScroll = () => setScrollY(window.scrollY);
    const onResize = () => setDims({ vh: window.innerHeight, vw: window.innerWidth });
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onResize);
    onScroll();
    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onResize);
    };
  }, []);

  useEffect(() => {
    const obs = new IntersectionObserver(
      entries => entries.forEach(e => {
        if (e.isIntersecting) setActiveSection(parseInt(e.target.dataset.idx, 10));
      }),
      { threshold: 0.4 }
    );
    sectionRefs.current.forEach(el => el && obs.observe(el));
    return () => obs.disconnect();
  }, []);

  const { vh, vw } = dims;

  /* hero drift starts halfway through transition window */
  const driftT = ease(clamp((scrollY - vh * 0.5) / (vh * 1.1), 0, 1));

  /* character width in post phase — match section layout gutter */
  const charW = Math.min(360, Math.round(vw * 0.32));

  return (
    <div>
      <ClickParticles/>
      <AboutPage open={showAbout} onClose={() => setShowAbout(false)}/>
      <Nav onOpenAbout={() => { setShowAbout(true); sounds.pop(); }} onGoToWork={goToWork} onGoToHuman={goToHuman}/>

      {/* Fixed character — always in DOM */}
      <Character scrollY={scrollY} vh={vh} vw={vw} sectionIdx={activeSection} frameHover={frameHover} propHover={propHover}/>

      {/* ── Hero runway ── sticky stage + 2.3×vh so morph has room */}
      <div style={{ height: vh * 2.3 + 'px', position: 'relative' }}>
        <div style={{ position:'sticky', top:0, height: vh+'px', overflow:'hidden', zIndex:5 }}>
          <HeroScene driftT={driftT} vw={vw} onOpenAbout={() => { setShowAbout(true); sounds.pop(); }} onFrameHover={setFrameHover} onPropHover={setPropHover} frameHover={frameHover} onWorkClick={goToWork}/>
        </div>
      </div>

      {/* ── content sections ── */}
      <div className="sections-wrapper" style={{ '--char-w': charW + 'px' }}>
        {CHAPTERS.map((ch, i) => (
          <section
            key={i}
            className={`content-section cs-${i % 4}`}
            data-idx={i}
            data-kind={ch.kind}
            ref={el => sectionRefs.current[i] = el}
            style={{ contain: 'layout style', willChange: 'opacity' }}
          >
            {ch.kind === 'workhub' ? (
              <>
                <div className="cs-inner"><WorkHubSection/></div>
                {/* scroll hint */}
                <div style={{
                  position: 'absolute', bottom: '28px', right: '28px',
                  display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '4px',
                  pointerEvents: 'none',
                }}>
                  <span style={{ fontFamily: 'var(--marker)', fontSize: '15px', color: 'var(--ink)', opacity: 0.55 }}>scroll down to explore more about me</span>
                  <span style={{ fontSize: '18px', color: 'var(--ink)', opacity: 0.4, animation: 'bounce 1.4s ease-in-out infinite', alignSelf: 'flex-end' }}>↓</span>
                </div>
              </>
            ) : ch.kind === 'human' ? (
              <>
                <HumanSection/>
                <BackToTopBtn/>
              </>
            ) : (
              <>
                <div className="cs-inner"><SectionCards chapter={ch} idx={i}/></div>
                <BackToTopBtn/>
              </>
            )}
          </section>
        ))}
      </div>

    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
