// Frame 01 v2: ASCII Terminal — 带图作品网格 + 浮动 chatbot + 光标变形

if (typeof window.getSiteCursorStyle !== 'function') {
  window.getSiteCursorStyle = function getSiteCursorStyleFallback(cur, C) {
    return {
      position: 'fixed', pointerEvents: 'none', zIndex: 200, left: cur.x, top: cur.y,
      transform: 'translate(-50%,-50%)', opacity: cur.visible ? 1 : 0,
      width: 10, height: 10, borderRadius: '50%', background: (C && C.curDefault) || '#6fb36f',
    };
  };
}

// Low-fi ASCII-ish placeholder "images" — each work gets a unique pattern
const PATTERNS = [
  { bg: '#1a1410', fg: '#d97757', glyph: '▞▚' },
  { bg: '#10181a', fg: '#6fb3a8', glyph: '◢◣' },
  { bg: '#181018', fg: '#b87fd9', glyph: '░▒▓' },
  { bg: '#1a1a10', fg: '#d4c06f', glyph: '╱╲' },
  { bg: '#101a14', fg: '#6fd48e', glyph: '●○' },
  { bg: '#1a1010', fg: '#d96f6f', glyph: '▼▲' },
];

const SELECTED_WORKS_DOTS = [
  [2.08,50.08], [34.08,50.08], [58.049,50.08], [66.049,50.08], [74.049,50.08], [98.018,50.08], [130.018,50.08], [145.986,50.08], [177.986,50.08], [2.08,42.08], [10.08,42.08],
  [26.08,42.08], [34.08,42.08], [50.049,42.08], [82.049,42.08], [98.018,42.08], [122.018,42.08], [145.986,42.08], [169.986,42.08], [2.08,34.08], [18.08,34.08], [34.08,34.08],
  [50.049,34.08], [82.049,34.08], [98.018,34.08], [114.018,34.08], [145.986,34.08], [161.986,34.08], [2.08,26.08], [18.08,26.08], [34.08,26.08], [50.049,26.08], [82.049,26.08],
  [98.018,26.08], [106.018,26.08], [114.018,26.08], [122.018,26.08], [145.986,26.08], [153.986,26.08], [2.08,18.08], [34.08,18.08], [50.049,18.08], [82.049,18.08], [98.018,18.08],
  [130.018,18.08], [145.986,18.08], [161.986,18.08], [2.08,10.08], [34.08,10.08], [50.049,10.08], [82.049,10.08], [98.018,10.08], [130.018,10.08], [145.986,10.08], [169.986,10.08],
  [2.08,2.08], [34.08,2.08], [58.049,2.08], [66.049,2.08], [74.049,2.08], [98.018,2.08], [106.018,2.08], [114.018,2.08], [122.018,2.08], [145.986,2.08], [177.986,2.08],
];

const HERO_SLIDES = [
  { id: 'eye', en: 'Curiosity broadens my horizons', zh: '好奇心拓宽了我的视野', alt: 'ASCII eye' },
  { id: 'coffee', en: 'No ideas? Coffee, then pretend to think.', zh: '没灵感？喝杯咖啡，再假装思考。', alt: 'ASCII coffee cup' },
  { id: 'cat', en: 'Cats taught empathy.', zh: '猫教会了我共情。', alt: 'ASCII cat' },
];

// Fit silhouettes into one optical frame while preserving character proportions.
const HERO_ART_LAYOUT = Object.fromEntries(Object.entries(window.HERO_ASCII_ART).map(([id, text]) => {
  const rows = text.split('\n');
  const points = rows.flatMap((row, y) => Array.from(row).flatMap((glyph, x) => glyph === ' ' ? [] : [{x, y}]));
  const minX = Math.min(...points.map(p => p.x)), maxX = Math.max(...points.map(p => p.x));
  const minY = Math.min(...points.map(p => p.y)), maxY = Math.max(...points.map(p => p.y));
  const width = maxX - minX + 1, height = maxY - minY + 1;
  const scale = Math.min(64 / width, 27 / height);
  const w = Math.round(width * scale), h = Math.round(height * scale);
  const left = Math.floor((90 - w) / 2), top = Math.floor((27 - h) / 2);
  const grid = Array.from({length: 35}, () => Array(90).fill(' '));
  for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
    grid[top + y][left + x] = rows[minY + Math.min(height - 1, Math.floor(y / scale))][minX + Math.min(width - 1, Math.floor(x / scale))] || ' ';
  }
  return [id, {art: grid.map(row => row.join('')).join('\n'), bottom: top + h}];
}));

// Stable per-cell variation keeps motion independent of React renders and slide changes.
const HeroAsciiArt = React.memo(function HeroAsciiArt({ id, label, art }) {
  const characters = React.useMemo(() => {
    const ramp = '.super*%&#';
    return Array.from(art || window.HERO_ASCII_ART[id]).map((glyph, index) => {
      if (glyph === ' ' || glyph === '\n') return glyph;
      const random = seed => {
        const value = Math.sin((index + 1) * 127.1 + seed * 311.7) * 43758.5453;
        return value - Math.floor(value);
      };
      const density = Math.max(0, ramp.indexOf(glyph)) / (ramp.length - 1);
      return <span key={index} className="hero-ascii-cell" style={{
        '--drift-duration': `${3 + random(1) * 5}s`,
        '--drift-delay': `${-random(2) * 8}s`,
        '--flicker-duration': `${1.8 + random(3) * 4.6}s`,
        '--flicker-delay': `${-random(4) * 7}s`,
        '--drift-x': `${(random(5) - .5) * .46}em`,
        '--drift-y': `${(random(6) - .5) * .5}em`,
        '--shade-low': .14 + density * .2,
        '--shade-mid': .38 + density * .4,
        '--shade-high': .68 + density * .32,
      }}>{glyph}</span>;
    });
  }, [id, art]);
  return <pre className="hero-ascii-text" role="img" aria-label={label}>{characters}</pre>;
});

// One shared character field moves between silhouettes instead of cross-fading images.
function HeroMorphArt({ id, label, reduced }) {
  const [art, setArt] = React.useState(HERO_ART_LAYOUT[id].art);
  const current = React.useRef(art);
  React.useEffect(() => {
    const target = HERO_ART_LAYOUT[id].art;
    if (reduced || current.current === target) {
      current.current = target;
      setArt(target);
      return;
    }
    const points = text => text.split('\n').flatMap((row, y) =>
      Array.from(row).flatMap((glyph, x) => glyph === ' ' ? [] : [{x, y, glyph}])
    );
    const from = points(current.current), to = points(target);
    const count = Math.max(from.length, to.length);
    const glyphs = 'super.#%*&';
    let frame, start;
    const tick = now => {
      if (start === undefined) start = now;
      const progress = Math.min(1, (now - start) / 250);
      if (progress === 1) {
        current.current = target;
        setArt(target);
        return;
      }
      // Update on each animation frame so the 250ms eased morph stays fluid.
      {
        const grid = Array.from({length: 35}, () => Array(90).fill(' '));
        const ease = progress * progress * (3 - 2 * progress);
        const spread = Math.sin(Math.PI * progress);
        for (let i = 0; i < count; i++) {
          const a = from[Math.floor(i * from.length / count)];
          const b = to[Math.floor(i * to.length / count)];
          const phase = i * 2.39996;
          const x = Math.round(a.x + (b.x - a.x) * ease + Math.sin(phase + progress * 4) * spread * 5);
          const y = Math.round(a.y + (b.y - a.y) * ease + Math.cos(phase + progress * 3) * spread * 2.5);
          if (x >= 0 && x < 90 && y >= 0 && y < 35) {
            grid[y][x] = progress < .18 ? a.glyph : progress > .82 ? b.glyph : glyphs[(i + Math.floor(progress * 12)) % glyphs.length];
          }
        }
        const next = grid.map(row => row.join('')).join('\n');
        current.current = next;
        setArt(next);
      }
      frame = requestAnimationFrame(tick);
    };
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [id, reduced]);
  return <div className="hero-art hero-morph-art"><HeroAsciiArt id={id} label={label} art={art} /></div>;
}

function HeroMorphCaption({ text, reduced }) {
  const [display, setDisplay] = React.useState(text);
  const current = React.useRef(text);
  React.useEffect(() => {
    const before = current.current;
    if (reduced || before === text) {
      current.current = text;
      setDisplay(text);
      return;
    }
    const glyphs = 'super.#%*&';
    let frame, start;
    const tick = now => {
      if (start === undefined) start = now;
      const progress = Math.min(1, (now - start) / 250);
      const eased = progress * progress * (3 - 2 * progress);
      const length = Math.round(before.length + (text.length - before.length) * eased);
      const next = progress === 1 ? text : Array.from({length}, (_, index) => {
        const threshold = ((index * 17 + 7) % 31) / 31;
        if (eased > .25 + threshold * .7) return text[index] || '';
        if (eased < threshold * .2) return before[index] || '';
        return text[index] === ' ' ? ' ' : glyphs[(index + Math.floor(eased * 16)) % glyphs.length];
      }).join('');
      current.current = next;
      setDisplay(next);
      if (progress < 1) frame = requestAnimationFrame(tick);
    };
    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [text, reduced]);
  return <p className="hero-morph-caption" aria-label={text}><span aria-hidden="true">{display}</span></p>;
}

// Original pixel portrait: side-swept hair, round glasses and a collared shirt.
const HERO_PORTRAIT_PIXELS = [
  '      #####      ',
  '    ########     ',
  '   ###    ###    ',
  '   ##      ##    ',
  '   #        #    ',
  '  ## ### ### #   ',
  '  #  # # # # #   ',
  '  #  ### ### #   ',
  '   #    #   #    ',
  '   #        #    ',
  '    #  ### #     ',
  '     #    #      ',
  '     ##  ##      ',
  '   ## #  # ##    ',
  '  #   ####   #   ',
  ' #    #  #    #  ',
  ' #    #  #    #  ',
];
function HeroPortraitBadge() {
  const Surface = window.GlassSurface;
  return <Surface className="hero-portrait-badge">
    <svg className="hero-portrait-icon" width="38" height="38" viewBox="0 0 38 38" aria-hidden="true" focusable="false">
      {HERO_PORTRAIT_PIXELS.flatMap((row, y) => Array.from(row).map((pixel, x) =>
        pixel === '#' ? <rect key={`${x}-${y}`} x={2 + x * 2} y={2 + y * 2} width="1.65" height="1.65" rx=".25" fill="currentColor" /> : null
      ))}
    </svg>
  </Surface>;
}

function AsciiHeroSection({ lang, children }) {
  const section = React.useRef(null);
  const quickScroll = React.useRef(0);
  const [reduced, setReduced] = React.useState(() => window.matchMedia('(prefers-reduced-motion: reduce)').matches);
  React.useEffect(() => {
    const media = window.matchMedia('(prefers-reduced-motion: reduce)');
    const update = () => setReduced(media.matches);
    media.addEventListener('change', update);
    return () => media.removeEventListener('change', update);
  }, []);
  React.useEffect(() => {
    const root = section.current;
    if (reduced) return;
    const cards = Array.from(root.querySelectorAll('.hero-stack-card'));
    const copy = root.querySelector('.hero-stack-copy');
    let frame = 0;
    const clamp = n => Math.max(0, Math.min(1, n));
    const ease = n => n * n * (3 - 2 * n);
    const render = () => {
      frame = 0;
      root.style.setProperty('--hero-viewport-width', `${document.documentElement.clientWidth}px`);
      const vh = window.innerHeight;
      const progress = Math.max(0, -root.getBoundingClientRect().top / vh);
      const entry = ease(clamp(progress / .8));
      const cardTop = cards[0].offsetTop;
      const stackGap = window.innerWidth <= 720 ? 28 : 40;
      const lift = Math.max(0, cardTop - Math.max(90, (vh - cards[0].offsetHeight) / 2));
      cards.forEach((card, i) => {
        const exit = ease(clamp((progress - .8 - i * .8) / .8));
        const isLast = i === cards.length - 1;
        // Add a short upward pop as Work follows the releasing sticky stage.
        const lastExit = ease(clamp((progress - 3.05) / .55));
        const depth = Math.max(0, i - Math.max(0, (progress - .8) / .8));
        const y = -lift * entry - depth * stackGap - (isLast ? lastExit * vh * .65 : exit * (vh + card.offsetHeight));
        const scale = 1 - Math.min(3, depth) * .07;
        card.style.transform = `translate(-50%, ${y}px) scale(${scale}) rotate(${(isLast ? lastExit : exit) * -5}deg)`;
        card.style.opacity = '1';
        card.style.visibility = !isLast && exit >= 1 ? 'hidden' : 'visible';
      });
      // Only fade text when a card approaches its actual horizontal and vertical bounds.
      const textBounds = Array.from(copy.querySelectorAll('.hero-lead, .hero-description')).map(el => el.getBoundingClientRect());
      let copyOpacity = 1;
      cards.forEach(card => {
        if (card.style.visibility === 'hidden') return;
        const bounds = card.getBoundingClientRect();
        textBounds.forEach(text => {
          const horizontalOverlap = bounds.left < text.right && bounds.right > text.left;
          if (horizontalOverlap && bounds.bottom > text.top - 24 && bounds.top < text.bottom + 24) {
            copyOpacity = Math.min(copyOpacity, 1 - ease(clamp((text.bottom + 24 - bounds.top) / 48)));
          }
        });
      });
      copy.style.opacity = String(copyOpacity);
      copy.style.visibility = copyOpacity === 0 ? 'hidden' : 'visible';
      const arrow = root.querySelector('.hero-stack-arrow');
      const lastBounds = cards[cards.length - 1].getBoundingClientRect();
      const arrowOpacity = 1 - ease(clamp((32 - lastBounds.bottom) / 64));
      arrow.style.opacity = String(arrowOpacity);
      arrow.style.visibility = arrowOpacity === 0 ? 'hidden' : 'visible';

    };
    const schedule = () => { if (!frame) frame = requestAnimationFrame(render); };
    window.addEventListener('scroll', schedule, {passive: true});
    window.addEventListener('resize', schedule);
    render();
    return () => { cancelAnimationFrame(frame); window.removeEventListener('scroll', schedule); window.removeEventListener('resize', schedule); };
  }, [reduced]);
  React.useEffect(() => {
    const stop = () => { cancelAnimationFrame(quickScroll.current); quickScroll.current = 0; window.__heroQuickScrollActive = false; };
    window.addEventListener('wheel', stop, {passive: true});
    window.addEventListener('touchstart', stop, {passive: true});
    window.addEventListener('keydown', stop);
    return () => { stop(); window.removeEventListener('wheel', stop); window.removeEventListener('touchstart', stop); window.removeEventListener('keydown', stop); };
  }, []);
  const onArrowClick = event => {
    if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
    event.preventDefault();
    cancelAnimationFrame(quickScroll.current);
    const root = section.current;
    const works = document.getElementById('works');
    const startY = window.scrollY;
    const rootY = root.getBoundingClientRect().top + startY;
    const destination = works.getBoundingClientRect().top + startY - 80;
    if (reduced) { window.scrollTo(0, destination); history.replaceState(null, '', '#works'); return; }
    const vh = window.innerHeight;
    const stops = [.8, 1.6, 2.4, 3.05].map(p => rootY + p * vh).concat(destination).filter(y => y > startY + 1 && y <= destination);
    if (!stops.length) return;
    window.__heroQuickScrollActive = true;
    let phase = 0, phaseStart, from = startY;
    const tick = now => {
      if (phaseStart === undefined) phaseStart = now;
      const duration = phase === 0 && startY < rootY + vh * .5 ? 340 : phase === stops.length - 1 ? 320 : 210;
      const t = Math.min(1, (now - phaseStart) / duration);
      const eased = t * t * (3 - 2 * t);
      window.scrollTo(0, from + (stops[phase] - from) * eased);
      if (t === 1) {
        from = stops[phase++]; phaseStart = now;
        if (phase === stops.length) { quickScroll.current = 0; window.__heroQuickScrollActive = false; history.replaceState(null, '', '#works'); return; }
      }
      quickScroll.current = requestAnimationFrame(tick);
    };
    quickScroll.current = requestAnimationFrame(tick);
  };
  const slides = [HERO_SLIDES[2], HERO_SLIDES[1], HERO_SLIDES[0]];
  return (
    <section ref={section} id="about" className={`home-hero hero-stack-section${reduced ? ' is-reduced' : ''}`}>
      <div className="hero-stack-sticky">
        <div className="hero-copy hero-stack-copy">{children}</div>
        <a className="hero-scroll-cue hero-stack-arrow" href="#works" onClick={onArrowClick} aria-label={lang === 'en' ? 'View selected work' : '查看作品'}>
          <svg className="hero-scroll-dots" width="37" height="53" viewBox="0 0 37 53" aria-hidden="true">
            {[[18.08,2.08],[18.08,10.08],[18.08,18.08],[18.08,26.08],[18.08,34.08],[2.08,34.08],[10.08,42.08],[18.08,50.08],[26.08,42.08],[34.08,34.08]].map(([cx,cy]) => <circle key={`${cx}-${cy}`} cx={cx} cy={cy} r="2.08" fill="currentColor" />)}
          </svg>
        </a>
        <div className="hero-card-stack" aria-label={lang === 'en' ? 'Creative inspirations' : '创作灵感'}>
          {slides.map((slide, index) => <article key={slide.id} className={`hero-stack-card hero-stack-card--${slide.id}`} style={{zIndex: 4 - index, '--stack-depth': index}}>
            <p className="hero-card-caption">{slide[lang]}</p>
            <div className="hero-card-art"><HeroAsciiArt id={slide.id} label={slide.alt} art={HERO_ART_LAYOUT[slide.id].art} /></div>
          </article>)}
          <article className="hero-stack-card hero-stack-card--placeholder" style={{zIndex: 1, '--stack-depth': 3}} aria-label={lang === 'en' ? 'Illustration placeholder' : '插图占位'}>
            <span className="hero-card-placeholder" aria-hidden="true">super.#%*&</span>
            <p className="hero-card-caption">{lang === 'en' ? 'More to explore.' : '还有更多，待探索。'}</p>
          </article>
        </div>
      </div>
    </section>
  );
}

function HeroDecodeText({ text, className, delay = 0, duration = 760 }) {
  const [rendered, setRendered] = React.useState(text);

  React.useEffect(() => {
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      setRendered(text);
      return undefined;
    }
    const glyphs = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789<>/\\[]{}*+~';
    const characters = Array.from(text);
    const codeTailLength = Math.max(4, Math.round(characters.length * .22));
    let frame = 0;
    let start = 0;
    let lastPaint = -Infinity;

    const tick = (now) => {
      if (!start) start = now;
      const elapsed = now - start;
      const visibleProgress = Math.max(0, Math.min(1, (elapsed - delay) / duration));
      if (elapsed - lastPaint >= 42 || visibleProgress === 1) {
        // Resolve the actual copy while the line is still heavily blurred, so the
        // final sharp frame never exposes a single scrambled glyph or tail.
        const resolveLine = elapsed - delay >= duration * .42 || visibleProgress === 1;
        const tailProgress = Math.max(0, Math.min(1, (elapsed - delay - duration * .04) / (duration * .34)));
        const visibleTailLength = Math.ceil(codeTailLength * (1 - tailProgress));
        const scrambled = characters.map((character) => {
          if (/\s/.test(character)) return character;
          if (resolveLine) return character;
          return glyphs[Math.floor(Math.random() * glyphs.length)];
        }).join('') + Array.from({ length: visibleTailLength }, () => glyphs[Math.floor(Math.random() * glyphs.length)]).join('');
        setRendered(scrambled);
        lastPaint = elapsed;
      }
      if (visibleProgress < 1) frame = window.requestAnimationFrame(tick);
    };

    frame = window.requestAnimationFrame(tick);
    return () => window.cancelAnimationFrame(frame);
  }, [text, delay, duration]);

  return <span
    className={`hero-decode ${className || ''}`}
    aria-label={text}
    style={{
      '--hero-decode-duration': `${duration}ms`,
      '--hero-decode-delay': `${delay}ms`,
    }}>
    {rendered}
  </span>;
}

function HeroRollingWords({ words, reducedText, interval = 2000 }) {
  const [reduceMotion, setReduceMotion] = React.useState(() => (
    window.matchMedia('(prefers-reduced-motion: reduce)').matches
  ));
  const [rotation, setRotation] = React.useState({ current: 0, previous: null, active: false });

  React.useEffect(() => {
    const media = window.matchMedia('(prefers-reduced-motion: reduce)');
    const onChange = (event) => setReduceMotion(event.matches);
    media.addEventListener?.('change', onChange);
    return () => media.removeEventListener?.('change', onChange);
  }, []);

  React.useEffect(() => {
    if (reduceMotion || words.length < 2) return undefined;
    let frame = 0;
    const timer = window.setInterval(() => {
      setRotation((state) => ({
        current: (state.current + 1) % words.length,
        previous: state.current,
        active: false,
      }));
      frame = window.requestAnimationFrame(() => {
        frame = window.requestAnimationFrame(() => {
          setRotation((state) => ({ ...state, active: true }));
        });
      });
    }, interval);

    return () => {
      window.clearInterval(timer);
      window.cancelAnimationFrame(frame);
    };
  }, [reduceMotion, words.length, interval]);

  if (reduceMotion) {
    return <span className="hero-rolling-static">{reducedText}</span>;
  }

  const longestWord = words.reduce((longest, word) => (
    word.length > longest.length ? word : longest
  ), words[0] || '');

  return (
    <span className="hero-rolling-words" aria-hidden="true">
      <span className="hero-rolling-sizer">{longestWord}</span>
      {rotation.previous !== null && (
        <span className={`hero-rolling-word hero-rolling-word-out${rotation.active ? ' is-active' : ''}`}>
          {words[rotation.previous]}
        </span>
      )}
      <span className={`hero-rolling-word hero-rolling-word-in${rotation.previous !== null ? ' is-next' : ''}${rotation.active ? ' is-active' : ''}`}>
        {words[rotation.current]}
      </span>
    </span>
  );
}

function WorkCover({ n, img, t, cover, decorative = false }) {
  if (cover && cover.src) {
    return cover.type === 'video'
      ? <video className="work-upload-cover" src={cover.src} aria-label={decorative ? undefined : t} autoPlay muted loop playsInline preload="metadata" />
      : <img className="work-upload-cover" src={cover.src} alt={decorative ? '' : t} draggable={false} />;
  }
  if (n === '02') return <div className="pimax-cover" role={decorative ? undefined : 'img'} aria-label={decorative ? undefined : t}>
    <div className="pimax-phone"><img src="assets/home/phone.webp" alt="" draggable={false} /></div>
    <img className="pimax-screen" src="assets/home/pimax-screen.webp" alt="" draggable={false} />
    <div className="pimax-screen-bottom"><img src="assets/home/pimax-screen.webp" alt="" draggable={false} /></div>
  </div>;
  if (n === '01') return <img className="huolala-cover" src={img} alt={decorative ? '' : t} draggable={false} />;
  return <div className="generic-work-cover" role={decorative ? undefined : 'img'} aria-label={decorative ? undefined : t}>
    <span>{String(t || 'New case').slice(0, 2)}</span>
  </div>;
}

function AsciiTile({ pat, t, img, n, cover }) {
  const [hover, setHover] = React.useState(false);
  const move = event => {
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches || event.pointerType === 'touch') return;
    const node = event.currentTarget;
    const rect = node.getBoundingClientRect();
    const x = (event.clientX - rect.left) / rect.width - .5;
    const y = (event.clientY - rect.top) / rect.height - .5;
    node.style.setProperty('--card-x', `${x * 14}px`);
    node.style.setProperty('--card-y', `${y * 10}px`);
    node.style.setProperty('--card-rx', `${y * -2.4}deg`);
    node.style.setProperty('--card-ry', `${x * 3}deg`);
  };
  return <div className={`work-tile${hover ? ' is-glass-hovered' : ''}`}
    onPointerEnter={event => { if (event.pointerType !== 'touch') setHover(true); }}
    onPointerMove={move}
    onPointerLeave={event => {
      setHover(false);
      for (const prop of ['x', 'y', 'rx', 'ry']) event.currentTarget.style.removeProperty(`--card-${prop}`);
    }}
    style={{
    boxShadow: hover ? 'var(--shadow-card-hover)' : 'none',
    transform: `perspective(1000px) translate3d(var(--card-x, 0px), var(--card-y, 0px), 0) rotateX(var(--card-rx, 0deg)) rotateY(var(--card-ry, 0deg)) scale(${hover ? 1.018 : 1})`,
    transition: hover ? 'transform 120ms cubic-bezier(.2,.8,.2,1), box-shadow .25s ease' : 'transform 420ms cubic-bezier(.16,1,.3,1), box-shadow .25s ease',

    display: 'block', background: pat.bg, borderRadius: 30,
    aspectRatio: '521/605', position: 'relative', overflow: 'hidden',
  }}>
    <WorkCover n={n} img={img} t={t} cover={cover} />
  </div>;
}

function PlayMedia({ item }) {
  return <div className="play-media home-scroll-item">
    {item.type === 'video'
      ? <video src={item.src} aria-label={item.alt || 'Play media'} autoPlay muted loop playsInline preload="metadata" />
      : <img src={item.src} alt={item.alt || ''} loading="lazy" decoding="async" />}
  </div>;
}

function AsciiTerminal() {
  const SiteTopbar = window.SiteTopbar;
  const GlassSurface = window.GlassSurface;
  const [cur, setCur] = React.useState({ x: 0, y: 0, mode: 'default', visible: false });
  const [theme, setTheme] = React.useState(() => {
    return window.getAsciiInitialTheme ? window.getAsciiInitialTheme() : 'light';
  });
  const showChatEntry = false; // 暂时隐藏 AI 入口，恢复时设为 true。
  const [chatOpen, setChatOpen] = React.useState(false);
  const [lang, setLang] = React.useState(() => {
    try { return localStorage.getItem('ascii-lang') === 'en' ? 'en' : 'zh'; } catch { return 'zh'; }
  });
  const [leavingCase, setLeavingCase] = React.useState(null);
  const [returnedFromCase, setReturnedFromCase] = React.useState(() => {
    try {
      const returned = sessionStorage.getItem('ascii-case-return-transition') === '1';
      sessionStorage.removeItem('ascii-case-return-transition');
      return returned;
    } catch (_) { return false; }
  });
  const [cmsContent, setCmsContent] = React.useState(null);
  const dark = theme === 'dark';
  const C = window.getAsciiThemePalette(dark);

  React.useEffect(() => {
    try { localStorage.setItem('ascii-theme', theme); } catch {}
    document.documentElement.dataset.theme = theme;
  }, [theme]);

  React.useEffect(() => {
    if (!window.watchAsciiAutomaticTheme) return undefined;
    return window.watchAsciiAutomaticTheme(setTheme);
  }, []);

  React.useEffect(() => {
    try { localStorage.setItem('ascii-lang', lang); } catch {}
  }, [lang]);

  React.useEffect(() => {
    let active = true;
    const loadContent = () => fetch(window.__STATIC_CONTENT__ ? '/data/site-content.json' : '/api/content', { cache: window.__STATIC_CONTENT__ ? 'default' : 'no-store' })
      .then(response => response.ok ? response.json() : Promise.reject(new Error('Content unavailable')))
      .then(content => { if (active) setCmsContent(content); })
      .catch(() => {});
    loadContent();
    const timer = window.__STATIC_CONTENT__ ? null : window.setInterval(loadContent, 3000);
    const channel = !window.__STATIC_CONTENT__ && 'BroadcastChannel' in window ? new BroadcastChannel('portfolio-content') : null;
    if (channel) channel.onmessage = loadContent;
    return () => {
      active = false;
      window.clearInterval(timer);
      if (channel) channel.close();
    };
  }, []);

  React.useEffect(() => {
    const root = rootRef.current;
    if (!root || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return undefined;
    let frame = 0;
    const motion = new Map();
    const animateScrollMotion = () => {
      frame = 0;
      let needsAnotherFrame = false;
      const viewport = Math.max(window.innerHeight || 0, 1);
      root.querySelectorAll('.home-scroll-item').forEach((node) => {
        const state = motion.get(node) || { y: 18, scale: .96, opacity: .42 };
        const rect = node.getBoundingClientRect();
        const progress = Math.max(0, Math.min(1, (viewport * .93 - rect.top) / (viewport * .62)));
        const targetY = (1 - progress) * 20;
        const targetScale = .96 + progress * .04;
        const targetOpacity = .42 + progress * .58;
        state.y += (targetY - state.y) * .115;
        state.scale += (targetScale - state.scale) * .115;
        state.opacity += (targetOpacity - state.opacity) * .115;
        motion.set(node, state);
        node.style.setProperty('--home-scroll-y', `${state.y.toFixed(2)}px`);
        node.style.setProperty('--home-scroll-scale', state.scale.toFixed(4));
        node.style.setProperty('--home-scroll-opacity', state.opacity.toFixed(3));
        if (Math.abs(targetY - state.y) > .04 || Math.abs(targetScale - state.scale) > .0004 || Math.abs(targetOpacity - state.opacity) > .004) needsAnotherFrame = true;
      });
      if (needsAnotherFrame) frame = window.requestAnimationFrame(animateScrollMotion);
    };
    const requestUpdate = () => { if (!frame) frame = window.requestAnimationFrame(animateScrollMotion); };
    requestUpdate();
    window.addEventListener('scroll', requestUpdate, { passive: true });
    window.addEventListener('resize', requestUpdate);
    return () => {
      window.removeEventListener('scroll', requestUpdate);
      window.removeEventListener('resize', requestUpdate);
      if (frame) window.cancelAnimationFrame(frame);
    };
  }, []);

  React.useEffect(() => {
    if (!chatOpen) return;
    const onKey = (e) => {
      if (e.key === 'Escape') setChatOpen(false);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [chatOpen]);

  React.useEffect(() => {
    if (!returnedFromCase) return;
    const timer = window.setTimeout(() => setReturnedFromCase(false), 620);
    return () => window.clearTimeout(timer);
  }, [returnedFromCase]);

  const rootRef = React.useRef(null);

  React.useEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    const onMove = (e) => {
      setCur(c => ({ ...c, x: e.clientX, y: e.clientY, visible: true, mode: e.target.closest('.work-tile') ? 'case' : (c.mode === 'case' ? 'link' : c.mode) }));
    };
    const onLeave = () => setCur(c => ({ ...c, visible: false }));
    el.addEventListener('mousemove', onMove);
    el.addEventListener('mouseleave', onLeave);
    return () => {
      el.removeEventListener('mousemove', onMove);
      el.removeEventListener('mouseleave', onLeave);
    };
  }, []);

  const setMode = (m) => setCur(c => ({ ...c, mode: m }));

  const s = {
    wrap: {
      background: C.bg,
      color: C.fg,
      fontFamily: 'var(--font-family-mono)',
      fontSize: 'var(--font-size-body)',
      lineHeight: 1.7,
      width: '100%',
      maxWidth: 1166,
      margin: '0 auto',
      minHeight: '100%',
      padding: '20px var(--page-gutter) 80px',
      position: 'relative',
      cursor: 'none',
      transition: 'background-color var(--duration-normal) var(--ease-out), color var(--duration-normal) var(--ease-out)',
    },
    intro: {
      display: 'block',
      gap: 32,
      marginBottom: 0,
      width: 'min(100%, 1120px)',
    },
    bigLine: {
      fontSize: 'clamp(32px, 3.7vw, 43px)', lineHeight: 1.42, color: C.bigFg,
      letterSpacing: -0.3, fontFamily: 'inherit',
      maxWidth: 'min(1120px, 100%)',
    },
    heroLead: {
      display: 'block',
      position: 'relative',
      width: 'fit-content',
      marginBottom: 0,
      fontFamily: 'var(--font-family-editorial)',
      fontWeight: 400,
      fontSize: 32,
      lineHeight: '34px',
      letterSpacing: 0.6,
      textTransform: 'none',
      color: C.bigFg,
    },
    heroBody: {
      display: 'block',
      fontSize: 18,
      fontWeight: 400,
      lineHeight: '19px',
      letterSpacing: '-0.3px',
      color: C.fg,
      maxWidth: 374,
    },
    dim: { color: C.dim },
    accent: { color: C.accent },
    sectionTitle: {
      color: C.faint, fontSize: 11, letterSpacing: 1.5,
      marginTop: 40, marginBottom: 16, textTransform: 'uppercase',
    },
    row: {
      display: 'grid', gridTemplateColumns: '60px 1fr 90px 80px',
      padding: '8px 0', color: C.fg,
    },
    link: {
      color: C.fg, textDecoration: 'none',
      paddingBottom: 1,
    },
    footer: {
      marginTop: 60, paddingTop: 16,
      color: C.faint, fontSize: 11,
      display: 'flex', justifyContent: 'center', textAlign: 'center',
    },
    prompt: { color: C.green, marginRight: 6 },
    // Chat 对话框（由底部按钮弹出）
    chatDock: {
      width: 320,
      maxWidth: '100%',
      background: C.chatBg,
      border: `1px solid ${C.line}`,
      fontSize: 12,
      color: C.chipText,
      boxShadow: dark ? '0 12px 40px rgba(0,0,0,0.55)' : '0 12px 40px rgba(60,50,30,0.2)',
      borderRadius: 2,
    },
    chatHead: {
      display: 'flex', justifyContent: 'space-between',
      padding: '8px 12px', borderBottom: `1px dashed ${C.line}`,
      color: C.faint, fontSize: 10, letterSpacing: 1.4, textTransform: 'uppercase',
    },
    chatBody: { padding: '12px 14px', lineHeight: 1.55 },
    chatInput: {
      borderTop: `1px dashed ${C.line}`, padding: '10px 12px',
      color: C.mute, display: 'flex', alignItems: 'center', gap: 6,
    },
  };

  const copy = {
    en: {
      heroLead: "Hi, I’m Super Lee.",
      heroBodyLines: [
        'AI-Native designer.',
        'I explore the fusion of AI and taste, shaping a distinct aesthetic for digital products.',
      ],
      creativePrefix: 'My creative drive comes from ',
      creativeWords: ['curiosity', 'reading', 'travel', 'reflection'],
      creativeReduced: 'curiosity, reading, travel, and reflection',
      creativeSuffix: '.',
      worksTitle: 'Work',
      writingTitle: 'writing',
      labTitle: 'lab',
      labBody: 'a place for half-baked ideas, generative toys, and tiny tools.',
      footerLeft: '@ 2026 · super lee',
      works: [
        { n: '01', t: 'Huolala', y: '2025', k: 'product', pat: PATTERNS[0], img: 'assets/works/01-harbor.jpg', href: 'work-huolala.html' },
        { n: '02', t: 'Pimax', y: '2025', k: 'product', pat: PATTERNS[1], img: 'assets/works/02-spatial.jpg', href: 'work-pimax.html' },
        { n: '03', t: 'AI OS Concept', y: '2024', k: 'concept', pat: PATTERNS[2], img: 'assets/works/03-zixiang.jpg' },
        { n: '04', t: 'OPPO', y: '2024', k: 'product', pat: PATTERNS[3], img: 'assets/works/04-quiet.jpg' },
      ],
    },
    zh: {
      heroLead: "Hi, I’m Super Lee.",
      heroBodyLines: [
        'AI-Native designer.',
        '我正在探索 AI 与品味的融合，塑造独特的数字产品审美。',
      ],
      creativePrefix: '我的创作动力，源于',
      creativeWords: ['好奇', '阅读', '旅行', '思考'],
      creativeReduced: '好奇、阅读、旅行、思考',
      creativeSuffix: '。',
      worksTitle: 'Work',
      writingTitle: 'writing',
      labTitle: 'lab',
      labBody: '一个放半成品想法、生成式玩具和微型工具的地方。',
      footerLeft: '@ 2026 · super lee',
      works: [
        { n: '01', t: '货拉拉', y: '2025', k: 'product', pat: PATTERNS[0], img: 'assets/works/01-harbor.jpg', href: 'work-huolala.html' },
        { n: '02', t: 'Pimax', y: '2025', k: 'product', pat: PATTERNS[1], img: 'assets/works/02-spatial.jpg', href: 'work-pimax.html' },
        { n: '03', t: 'ai os 概念探索', y: '2024', k: 'concept', pat: PATTERNS[2], img: 'assets/works/03-zixiang.jpg' },
        { n: '04', t: 'oppo', y: '2024', k: 'product', pat: PATTERNS[3], img: 'assets/works/04-quiet.jpg' },
      ],
    },
  };
  const text = copy[lang] || copy.en;
  const managedWorks = cmsContent && Array.isArray(cmsContent.works)
    ? cmsContent.works
    : ['02', '01'].map(id => ({ id }));
  const selectedWorks = managedWorks.map((managed, index) => {
    const id = managed.id;
    const work = text.works.find(item => item.n === id) || {};
    return {
      ...work,
      n: id,
      pat: work.pat || PATTERNS[index % PATTERNS.length],
      t: managed.title || work.t || 'New case',
      href: managed.href || work.href || '#',
      cover: managed.cover || null,
      img: `assets/home/${id === '02' ? 'pimax-screen' : 'huolala'}.webp`,
      description: managed.description || (id === '02'
        ? (lang === 'zh' ? '智能灯串的数字体验设计' : 'A smart light string experience.')
        : id === '01'
          ? (lang === 'zh' ? '让货运出行更简单的产品设计' : 'A simpler way to move goods.')
          : ''),
    };
  });
  const defaultPlayItems = [
    ['controller', 'Game controller study', 166], ['laptop', 'Website design study', 206],
    ['wallet', 'Digital wallet concept', 166], ['type', 'Experimental typography', 166],
    ['tiles', 'Colorful interface experiments', 166], ['dashboard', 'Dashboard interface', 166],
    ['gradient', 'Gradient and color exploration', 208], ['controls', 'Soft interface controls', 208],
    ['tiles', 'Colorful interface experiments', 166], ['dashboard', 'Dashboard interface', 166],
    ['gradient', 'Gradient and color exploration', 208], ['controls', 'Soft interface controls', 208],
  ].map(([src, alt, height]) => ({ src: `assets/play/${src}.webp`, alt, type: 'image', height }));
  const playItems = cmsContent && Array.isArray(cmsContent.play) && cmsContent.play.length
    ? cmsContent.play.slice(0, 12)
    : defaultPlayItems;
  const playColumns = [0, 1, 2, 3].map(column => playItems.filter((_, index) => index % 4 === column));

  const writings = [
    ['On the slow web', '6 min', '2025.03'],
    ['设计系统不是 UI kit', '9 min', '2024.11'],
    ['Designing for cursors', '4 min', '2024.06'],
  ];

  const cursorBlock = window.getSiteCursorStyle(cur, C, dark);

  const textProbe = {
    onMouseEnter: () => setMode('text'),
    onMouseLeave: () => setMode('default'),
  };
  const linkProbe = {
    onMouseEnter: () => setMode('link'),
    onMouseLeave: () => setMode('default'),
  };

  /** 同页 #锚点：显式滚动（#works / #writing / …） */
  const onNavClick = (href) => (e) => {
    if (!href.startsWith('#') || href.length < 2) return;
    const id = href.slice(1);
    const el = document.getElementById(id) || document.querySelector(`[id="${CSS.escape(id)}"]`);
    if (!el) return;
    e.preventDefault();
    el.scrollIntoView({ behavior: 'smooth', block: 'start' });
    try {
      window.history.replaceState(null, '', href);
    } catch (_) {}
  };

  // 作品详情保留原生链接语义；点击时才接管，留出一小段“卡片进入页面”的过渡。
  const onWorkClick = (href) => (e) => {
    if (!href || href === '#' || leavingCase) return;
    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
    e.preventDefault();
    setLeavingCase(href);
    try { sessionStorage.setItem('ascii-case-transition', '1'); } catch (_) {}
    window.setTimeout(() => { window.location.assign(href); }, 430);
  };

  return (
    <div ref={rootRef} style={s.wrap}>
      <style>{`
        @keyframes cursor-blink { 0%,50%{opacity:1} 50.01%,100%{opacity:0} }
        @keyframes cur-pulse { 0%,50%{opacity:1} 50.01%,100%{opacity:0.25} }
        @keyframes ascii-caret-blink { 0%, 48% { opacity: 1; } 50%, 100% { opacity: 0.22; } }
        @keyframes avatar-float-up {
          from { opacity: 0; transform: translate3d(-12px, 34px, 0) scale(.72); }
          to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
        }
        .hero-avatar {
          position: absolute;
          left: min(calc(100% + 18px), calc(100vw - 132px));
          bottom: 6px;
          z-index: 5;
          width: clamp(86px, 8.4vw, 104px);
          aspect-ratio: 1;
          border-radius: 50%;
          overflow: hidden;
          opacity: 0;
          pointer-events: none;
          transform: translate3d(-12px, 34px, 0) scale(.72);
          transform-origin: 34% 82%;
          will-change: transform, opacity;
          backface-visibility: hidden;
        }
        .hero-lead:hover .hero-avatar {
          opacity: 1;
          transform: translate3d(0, 0, 0) scale(1);
          animation: avatar-float-up 620ms cubic-bezier(.16,1,.3,1) both;
        }
        .hero-avatar img {
          display: block;
          width: 100%;
          height: 100%;
          object-fit: cover;
          object-position: center 34%;
        }
        @keyframes hero-decode-reveal {
          from { opacity: 0; filter: blur(8px); transform: translate3d(0, .82em, 0); }
          58% { opacity: .96; filter: blur(8px); transform: translate3d(0, .12em, 0); }
          to { opacity: 1; filter: blur(0); transform: translate3d(0, 0, 0); }
        }
        .hero-decode {
          display: block;
          width: fit-content;
          opacity: 0;
          white-space: nowrap;
          will-change: transform, filter, opacity;
          backface-visibility: hidden;
          animation: hero-decode-reveal var(--hero-decode-duration, 780ms) cubic-bezier(.16,1,.3,1) var(--hero-decode-delay, 0ms) both;
        }
        .hero-decode-body {
          max-width: 100%;
          white-space: normal;
          text-wrap: pretty;
        }
        .hero-creative-line {
          display: flex;
          align-items: baseline;
          flex-wrap: wrap;
          width: fit-content;
          max-width: 100%;
        }
        .hero-creative-prefix {
          display: inline-block;
          white-space: pre-wrap;
        }
        .hero-rolling-words {
          position: relative;
          display: inline-grid;
          overflow: hidden;
          vertical-align: bottom;
          color: ${C.bigFg};
          white-space: nowrap;
        }
        .hero-rolling-sizer {
          grid-area: 1 / 1;
          visibility: hidden;
          pointer-events: none;
        }
        .hero-rolling-word {
          grid-area: 1 / 1;
          transition-property: transform, opacity, filter;
          transition-duration: 420ms;
          transition-timing-function: cubic-bezier(.16,1,.3,1);
          will-change: transform, opacity, filter;
        }
        .hero-rolling-word-in.is-next {
          opacity: 0;
          filter: blur(5px);
          transform: translate3d(0, .72em, 0);
        }
        .hero-rolling-word-in.is-next.is-active {
          opacity: 1;
          filter: blur(0);
          transform: translate3d(0, 0, 0);
        }
        .hero-rolling-word-out {
          opacity: 1;
          filter: blur(0);
          transform: translate3d(0, 0, 0);
        }
        .hero-rolling-word-out.is-active {
          opacity: 0;
          filter: blur(4px);
          transform: translate3d(0, -.55em, 0);
        }
        .hero-visually-hidden {
          position: absolute;
          width: 1px;
          height: 1px;
          padding: 0;
          margin: -1px;
          overflow: hidden;
          clip: rect(0, 0, 0, 0);
          white-space: nowrap;
          border: 0;
        }
        /* 自定义光标：图片与链接触发区不显系统手型/箭头 */
        img, a, button { cursor: none !important; }
        #about, #works, #writing, #lab, #contact {
          scroll-margin-top: 80px;
        }
        .selected-works-dots {
          animation: selected-works-twinkle var(--dot-duration) ease-in-out var(--dot-delay) infinite alternate;
        }
        @keyframes selected-works-twinkle {
          from { opacity: var(--dot-min-opacity); }
          to { opacity: 1; }
        }
        @media (prefers-reduced-motion: reduce) {
          .selected-works-dots { animation: none; }
        }
        .work-bento {
          display: grid;
          grid-template-columns: repeat(2, minmax(0, 1fr));
          gap: 40px;
        }
        .work-card {
          min-width: 0;
          min-height: 0;
          isolation: isolate;
          transition: none;
        }
        .work-card.is-leaving {
          position: relative;
          z-index: 3;
          transform: scale(1.035);
          transition: transform 430ms cubic-bezier(.2,.8,.2,1), opacity 260ms ease;
        }
        .work-bento.is-transitioning .work-card:not(.is-leaving) {
          opacity: .38;
          transform: scale(.988);
          transition: transform 430ms cubic-bezier(.2,.8,.2,1), opacity 260ms ease;
        }
        @keyframes work-grid-return {
          from { opacity: 0; transform: translate3d(0, 16px, 0) scale(.984); filter: blur(2px); }
          to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); filter: blur(0); }
        }
        .work-bento.is-returning .work-card {
          animation: work-grid-return 540ms cubic-bezier(.16,1,.3,1) both;
        }
        .work-bento.is-returning .work-card:nth-child(2) { animation-delay: 45ms; }
        .work-bento.is-returning .work-card:nth-child(3) { animation-delay: 80ms; }
        .work-bento.is-returning .work-card:nth-child(4) { animation-delay: 115ms; }
        .case-transition-veil {
          position: fixed;
          inset: 0;
          z-index: 180;
          pointer-events: none;
          opacity: 0;
          background: ${C.bg};
          transition: opacity 300ms cubic-bezier(.22,.61,.36,1) 110ms;
        }
        .case-transition-veil.is-visible { opacity: 1; }
        .case-transition-veil::after {
          content: '';
          position: absolute;
          inset: 0;
          background: radial-gradient(circle at center, transparent 0 14%, ${C.bg} 68%);
          opacity: .78;
        }
        @media (prefers-reduced-motion: reduce) {
          .work-tile {
            transform: none !important;
            transition: box-shadow .2s ease !important;
          }
          .work-card, .work-card.is-leaving,
          .work-bento.is-transitioning .work-card:not(.is-leaving),
          .case-transition-veil { transition: none !important; transform: none !important; }
          .work-bento.is-returning .work-card { animation: none; }
          .hero-avatar,
          .hero-lead:hover .hero-avatar,
          .hero-decode {
            animation: none;
            transform: none;
            clip-path: none;
            opacity: 1;
            filter: none;
            transition: opacity .16s ease;
          }
          .hero-rolling-word { transition: none; }
        }
      `}</style>

      <SiteTopbar
        compactHero
        logo={<HeroPortraitBadge />}
        brand="Super lee"
        linkProbe={linkProbe}
        dark={dark}
        theme={theme}
        setTheme={setTheme}
        lang={lang}
        setLang={setLang}
        homeHrefPrefix=""
        anchorClickFactory={onNavClick}
      />

      <AsciiHeroSection lang={lang}>
        <div style={s.intro}>
          <div {...textProbe}>
            <div style={s.bigLine}>
              <span className="hero-lead" style={s.heroLead}>
                <HeroDecodeText key={`lead-${lang}`} text={text.heroLead} className="hero-decode-lead" delay={0} duration={520} />
                <HeroDecodeText key={`role-${lang}`} text={text.heroBodyLines[0]} className="hero-decode-role" delay={190} duration={520} />
              </span>
              <span className="hero-description" style={s.heroBody}>
                {text.heroBodyLines.slice(1).map((line, index) => <HeroDecodeText
                  key={`body-${lang}-${index}`}
                  text={line}
                  className="hero-decode-body"
                  delay={380 + index * 190}
                  duration={520}
                />)}

              </span>
            </div>
          </div>
        </div>
      </AsciiHeroSection>

      {/* Works — published cases, Pimax followed by Huolala */}
      <div
        id="works"
        className="home-section-heading home-scroll-item">
        <svg
          width="181"
          height="53"
          viewBox="0 0 181 53"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
          role="img"
          aria-label={text.worksTitle}
          style={{ display: 'block', width: 'min(180px, 42vw)', height: 'auto' }}
        >
          <defs>
            <filter id="selected-works-round-dots" x="-5%" y="-12%" width="110%" height="124%" colorInterpolationFilters="sRGB">
              <feGaussianBlur stdDeviation="1.15" />
              <feColorMatrix
                type="matrix"
                values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -8"
              />
            </filter>
          </defs>
          <path
            filter="url(#selected-works-round-dots)"
            style={{ display: 'none' }}
            d="M4.16016 52.1602H0V48H4.16016V52.1602ZM36.1602 52.1602H32V48H36.1602V52.1602ZM60.1289 52.1602H55.9688V48H60.1289V52.1602ZM68.1289 52.1602H63.9688V48H68.1289V52.1602ZM76.1289 52.1602H71.9688V48H76.1289V52.1602ZM100.098 52.1602H95.9375V48H100.098V52.1602ZM132.098 52.1602H127.938V48H132.098V52.1602ZM148.066 52.1602H143.906V48H148.066V52.1602ZM180.066 52.1602H175.906V48H180.066V52.1602ZM4.16016 44.1602H0V40H4.16016V44.1602ZM12.1602 44.1602H8V40H12.1602V44.1602ZM28.1602 44.1602H24V40H28.1602V44.1602ZM36.1602 44.1602H32V40H36.1602V44.1602ZM52.1289 44.1602H47.9688V40H52.1289V44.1602ZM84.1289 44.1602H79.9688V40H84.1289V44.1602ZM100.098 44.1602H95.9375V40H100.098V44.1602ZM124.098 44.1602H119.938V40H124.098V44.1602ZM148.066 44.1602H143.906V40H148.066V44.1602ZM172.066 44.1602H167.906V40H172.066V44.1602ZM4.16016 36.1602H0V32H4.16016V36.1602ZM20.1602 36.1602H16V32H20.1602V36.1602ZM36.1602 36.1602H32V32H36.1602V36.1602ZM52.1289 36.1602H47.9688V32H52.1289V36.1602ZM84.1289 36.1602H79.9688V32H84.1289V36.1602ZM100.098 36.1602H95.9375V32H100.098V36.1602ZM116.098 36.1602H111.938V32H116.098V36.1602ZM148.066 36.1602H143.906V32H148.066V36.1602ZM164.066 36.1602H159.906V32H164.066V36.1602ZM4.16016 28.1602H0V24H4.16016V28.1602ZM20.1602 28.1602H16V24H20.1602V28.1602ZM36.1602 28.1602H32V24H36.1602V28.1602ZM52.1289 28.1602H47.9688V24H52.1289V28.1602ZM84.1289 28.1602H79.9688V24H84.1289V28.1602ZM100.098 28.1602H95.9375V24H100.098V28.1602ZM108.098 28.1602H103.938V24H108.098V28.1602ZM116.098 28.1602H111.938V24H116.098V28.1602ZM124.098 28.1602H119.938V24H124.098V28.1602ZM148.066 28.1602H143.906V24H148.066V28.1602ZM156.066 28.1602H151.906V24H156.066V28.1602ZM4.16016 20.1602H0V16H4.16016V20.1602ZM36.1602 20.1602H32V16H36.1602V20.1602ZM52.1289 20.1602H47.9688V16H52.1289V20.1602ZM84.1289 20.1602H79.9688V16H84.1289V20.1602ZM100.098 20.1602H95.9375V16H100.098V20.1602ZM132.098 20.1602H127.938V16H132.098V20.1602ZM148.066 20.1602H143.906V16H148.066V20.1602ZM164.066 20.1602H159.906V16H164.066V20.1602ZM4.16016 12.1602H0V8H4.16016V12.1602ZM36.1602 12.1602H32V8H36.1602V12.1602ZM52.1289 12.1602H47.9688V8H52.1289V12.1602ZM84.1289 12.1602H79.9688V8H84.1289V12.1602ZM100.098 12.1602H95.9375V8H100.098V12.1602ZM132.098 12.1602H127.938V8H132.098V12.1602ZM148.066 12.1602H143.906V8H148.066V12.1602ZM172.066 12.1602H167.906V8H172.066V12.1602ZM4.16016 4.16016H0V0H4.16016V4.16016ZM36.1602 4.16016H32V0H36.1602V4.16016ZM60.1289 4.16016H55.9688V0H60.1289V4.16016ZM68.1289 4.16016H63.9688V0H68.1289V4.16016ZM76.1289 4.16016H71.9688V0H76.1289V4.16016ZM100.098 4.16016H95.9375V0H100.098V4.16016ZM108.098 4.16016H103.938V0H108.098V4.16016ZM116.098 4.16016H111.938V0H116.098V4.16016ZM124.098 4.16016H119.938V0H124.098V4.16016ZM148.066 4.16016H143.906V0H148.066V4.16016ZM180.066 4.16016H175.906V0H180.066V4.16016Z"
            fill={theme === 'dark' ? '#ffffff' : '#111111'}
          />
          {SELECTED_WORKS_DOTS.map(([cx, cy], index) => (
            <circle
              key={`${cx}-${cy}`}
              className="selected-works-dots"
              cx={cx}
              cy={cy}
              r="2.08"
              fill={theme === 'dark' ? '#ffffff' : '#111111'}
              style={{
                '--dot-duration': `${0.75 + ((index * 17) % 16) / 10}s`,
                '--dot-delay': `${-((index * 23) % 31) / 10}s`,
                '--dot-min-opacity': 0.16 + ((index * 11) % 6) * 0.08,
              }}
            />
          ))}
        </svg>
      </div>
      <div className={`work-bento${leavingCase ? ' is-transitioning' : ''}${returnedFromCase ? ' is-returning' : ''}`}>
        {selectedWorks.map(w => (
          <a
            key={w.n}
            className={`work-card home-scroll-item${leavingCase === w.href ? ' is-leaving' : ''}`}
            href={w.href || '#'}
            onClick={onWorkClick(w.href)}
            {...linkProbe}
            style={{ textDecoration: 'none', color: 'inherit', display: 'block', cursor: 'none' }}>
            <AsciiTile {...w} theme={theme} />
            <div className="work-caption"><h3>{w.t}</h3><p>{w.description}</p></div>
          </a>
        ))}
      </div>
      <section id="play" className="play-section" aria-labelledby="play-heading">
        <h2 id="play-heading" className="home-section-heading home-scroll-item">
          <img className="play-wordmark" src="assets/play/play.svg?v=monochrome-1" width="180" height="53" alt="Play" />
        </h2>
        <div className="play-grid">
          {playColumns.map((column, index) => (
            <div className="play-column" key={index}>
              {column.map((item, row) => (
                <PlayMedia key={`${item.src}-${row}`} item={item} />
              ))}
            </div>
          ))}
        </div>
      </section>
      <div className={`case-transition-veil${leavingCase ? ' is-visible' : ''}`} aria-hidden="true" />

      <div style={s.footer}>
        <span>{text.footerLeft}</span>
      </div>

      {/* 底部正中：简化图标，点击打开对话 */}
      {showChatEntry && <button
        type="button"
        aria-label="打开 ask-bot 对话"
        aria-expanded={chatOpen}
        {...linkProbe}
        onClick={() => setChatOpen(true)}
        style={{
          position: 'fixed',
          left: '50%',
          bottom: 28,
          transform: 'translateX(-50%)',
          zIndex: 95,
          width: 46,
          height: 46,
          borderRadius: '50%',
          border: `1px solid ${C.line}`,
          background: C.chatBg,
          color: C.green,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          padding: 0,
          boxShadow: dark ? '0 6px 24px rgba(0,0,0,0.45)' : '0 6px 20px rgba(60,50,30,0.12)',
        }}>
        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden>
          <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
        </svg>
      </button>}

      {showChatEntry && chatOpen && (
        <div
          role="presentation"
          style={{
            position: 'fixed',
            inset: 0,
            zIndex: 150,
            background: dark ? 'rgba(0,0,0,0.55)' : 'rgba(40,35,28,0.35)',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            padding: 20,
          }}
          onClick={() => setChatOpen(false)}>
          <div
            role="dialog"
            aria-modal="true"
            aria-labelledby="ask-bot-title"
            style={s.chatDock}
            onClick={(e) => e.stopPropagation()}>
            <div style={{ ...s.chatHead, alignItems: 'center' }}>
              <span id="ask-bot-title">ask-bot.sh</span>
              <span style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                <span>● online</span>
                <button
                  type="button"
                  aria-label="关闭"
                  {...linkProbe}
                  onClick={() => setChatOpen(false)}
                  style={{
                    background: 'transparent',
                    border: 'none',
                    color: C.mute,
                    fontSize: 18,
                    lineHeight: 1,
                    padding: '0 4px',
                    cursor: 'none',
                    fontFamily: 'inherit',
                  }}>
                  ×
                </button>
              </span>
            </div>
            <div style={s.chatBody}>
              <span style={s.prompt}>bot&gt;</span>
              <span> I'm SuperLee. This bot is trained on this site. Ask about projects, process, or just say hello.</span>
            </div>
            <div style={s.chatInput} {...textProbe}>
              <span style={{ color: C.green }}>you&gt;</span>
              <span>type a question…</span>
              <span style={{
                display: 'inline-block', width: 7, height: 12, background: C.green,
                animation: 'cursor-blink 1s steps(1) infinite', marginLeft: 2,
              }} />
            </div>
          </div>
        </div>
      )}

      {/* Custom block cursor */}
      <div style={{ ...cursorBlock, opacity: cur.mode === 'case' ? 0 : cursorBlock.opacity }} />
      <GlassSurface className="case-glass-cursor" borderRadius={39} distortionScale={-52} mapBlur={3.5} smoothRefraction
        style={{ position: 'fixed', left: cur.x, top: cur.y, width: 78, height: 78,
          padding: 0, borderRadius: '50%', pointerEvents: 'none', zIndex: 201,
          transform: 'translate(-50%, -50%)',
          opacity: cur.visible && cur.mode === 'case' ? 1 : 0 }} />
    </div>
  );
}

window.AsciiTerminal = AsciiTerminal;
