/* global React, ReactDOM, useTweaks, TweaksPanel, TweakSection, TweakColor, TweakRadio, TweakSlider */
const { useState, useRef, useEffect, useLayoutEffect, useCallback } = React;

const EASE = [0.16, 1, 0.3, 1];
function animateEl(el, kf, opts) {
  const M = window.__motion;
  if (!M || !el) return;
  try { M.animate(el, kf, opts); } catch (e) {}
}

/* ---- one history entry (echo + output) — content ALWAYS ends visible ---- */
function Entry({ entry }) {
  const ref = useRef(null);
  useLayoutEffect(() => {
    const el = ref.current;
    if (!el) return;
    let cancelled = false;
    const targets = entry.kind === 'welcome'
      ? Array.from(el.querySelectorAll('.welcome > *'))
      : entry.kind === 'boot'
      ? Array.from(el.querySelectorAll('.bootlog > *'))
      : [el];
    const reveal = () => targets.forEach((tg) => { tg.style.opacity = '1'; tg.style.transform = 'none'; });
    const run = () => {
      const M = window.__motion;
      if (!M) { reveal(); return; }
      try {
        targets.forEach((tg) => { tg.style.opacity = '0'; });
        const opts = (entry.kind === 'welcome' || entry.kind === 'boot')
          ? { duration: entry.kind === 'boot' ? 0.32 : 0.5, delay: M.stagger(entry.kind === 'boot' ? 0.11 : 0.07), ease: EASE }
          : { duration: 0.4, ease: EASE };
        const anim = M.animate(targets, { opacity: [0, 1], y: [10, 0] }, opts);
        if (anim && anim.finished && anim.finished.then) anim.finished.then(reveal).catch(reveal);
      } catch (e) { reveal(); }
      setTimeout(() => { if (!cancelled) reveal(); }, 900); // hard fallback
    };
    if (window.__motion) run();
    else {
      window.addEventListener('gb-motion-ready', run, { once: true });
      setTimeout(() => { if (!window.__motion && !cancelled) reveal(); }, 1100);
    }
    return () => { cancelled = true; };
  }, []);

  return (
    <div className="entry" ref={ref}>
      {entry.command != null && (
        <div className="echo">
          <span className="prompt" aria-hidden="true">
            <span className="p-user">glauber@borges</span><span className="p-dim">:</span><span className="p-path">~</span><span className="p-dim">$</span>
          </span>
          <span className="echo-cmd">{entry.command}</span>
        </div>
      )}
      <div className="output" dangerouslySetInnerHTML={{ __html: entry.html }} />
    </div>
  );
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "#7ee787",
  "fontSize": 14
}/*EDITMODE-END*/;

let UID = 0;

const URL_PARAMS = new URLSearchParams(location.search);
const LS = (k, d) => { try { return localStorage.getItem(k) || d; } catch (e) { return d; } };
const INITIAL_LANG = (URL_PARAMS.get('lang') || LS('gb-lang', 'pt')) === 'en' ? 'en' : 'pt';
const INITIAL_THEME = (URL_PARAMS.get('theme') || LS('gb-theme', 'dark')) === 'light' ? 'light' : 'dark';
/* accent swatches need a darker counterpart to stay readable on the light theme */
const LIGHT_ACCENT = { '#7ee787': '#1f9d55', '#e3b341': '#a1670f', '#79c0ff': '#0969da', '#e6e8ec': '#3d403b' };

function Terminal() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [lang, setLang] = useState(INITIAL_LANG);
  const [theme, setThemeState] = useState(INITIAL_THEME);
  const [history, setHistory] = useState([]);
  const [input, setInput] = useState('');
  const [hIndex, setHIndex] = useState(-1);
  const bodyRef = useRef(null);
  const inputRef = useRef(null);

  // boot sequence → welcome (first load only)
  useEffect(() => {
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    setHistory([{ id: ++UID, command: null, kind: 'boot', html: window.GB_DATA.boot(lang) }]);
    const tmo = setTimeout(() => {
      setHistory((h) => [...h, { id: ++UID, command: null, kind: 'welcome', html: window.GB_DATA.welcome(lang) }]);
    }, reduce ? 60 : 1150);
    return () => clearTimeout(tmo);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // theme + language live in the shared site preference (site.js / localStorage)
  useEffect(() => {
    const onTheme = (e) => setThemeState(e.detail === 'light' ? 'light' : 'dark');
    const onLang = (e) => {
      const l = e.detail === 'en' ? 'en' : 'pt';
      setLang(l);
      setHistory([{ id: ++UID, command: null, kind: 'welcome', html: window.GB_DATA.welcome(l) }]);
    };
    window.addEventListener('gb-theme', onTheme);
    window.addEventListener('gb-lang', onLang);
    return () => { window.removeEventListener('gb-theme', onTheme); window.removeEventListener('gb-lang', onLang); };
  }, []);

  // apply tweaks to :root
  useEffect(() => {
    const r = document.documentElement;
    r.style.setProperty('--accent', theme === 'light' ? (LIGHT_ACCENT[t.accent] || t.accent) : t.accent);
    r.style.setProperty('--term-fs', t.fontSize + 'px');
  }, [t.accent, t.fontSize, theme]);

  useEffect(() => { document.documentElement.setAttribute('data-theme', theme); }, [theme]);

  // window entrance
  useEffect(() => {
    const win = document.querySelector('.window');
    if (win) {
      win.style.opacity = '0';
      animateEl(win, { opacity: [0, 1], y: [14, 0], scale: [0.992, 1] }, { duration: 0.6, ease: EASE });
    }
    const onReady = () => {
      const w = document.querySelector('.window');
      if (w && getComputedStyle(w).opacity === '0') {
        w.style.opacity = '1';
        animateEl(w, { opacity: [0, 1], y: [14, 0] }, { duration: 0.6, ease: EASE });
      }
    };
    if (!window.__motion) window.addEventListener('gb-motion-ready', onReady, { once: true });
    // safety: never leave window hidden
    setTimeout(() => { const w = document.querySelector('.window'); if (w) w.style.opacity = '1'; }, 900);
  }, []);

  // focus the terminal without scrolling the pitch out of view; page starts at the top
  useEffect(() => {
    window.scrollTo(0, 0);
    const focus = () => { try { inputRef.current?.focus({ preventScroll: true }); } catch (e) {} };
    focus();
    const t1 = setTimeout(focus, 120);
    const t2 = setTimeout(focus, 1300); // after boot → welcome
    // typing anywhere lands in the prompt
    const onKey = (e) => {
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      const el = document.activeElement;
      if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable)) return;
      if (e.key.length === 1 || e.key === 'Backspace' || e.key === 'Enter') focus();
    };
    window.addEventListener('keydown', onKey);
    return () => { clearTimeout(t1); clearTimeout(t2); window.removeEventListener('keydown', onKey); };
  }, []);

  // keep scrolled to bottom
  useEffect(() => {
    const b = bodyRef.current;
    if (b) b.scrollTop = b.scrollHeight;
  }, [history]);

  const COMMANDS = ['rescue', 'about', 'stack', 'projects', 'experience', 'education', 'contact', 'status', 'arch', 'help', 'clear'];
  const ALIASES = {
    skills: 'stack', tech: 'stack', work: 'experience', exp: 'experience',
    edu: 'education', studies: 'education', who: 'about', whoami: 'about',
    proj: 'projects', ls: 'projects', email: 'contact', me: 'about',
    fix: 'rescue', socorro: 'rescue', consultoria: 'rescue', consulting: 'rescue',
    audit: 'rescue', diagnose: 'rescue', diagnostico: 'rescue', cleanup: 'rescue',
    debug: 'rescue', services: 'rescue', servicos: 'rescue',
  };

  const run = useCallback((raw) => {
    const command = raw.trim();
    const c = command.toLowerCase();
    if (!c) {
      setHistory((h) => [...h, { id: ++UID, command: raw, kind: 'output', html: '' }]);
      return;
    }
    // language switch → reset to welcome in new lang
    if (c === 'en' || c === 'pt') {
      setLang(c);
      if (window.GBSite) window.GBSite.setLang(c);
      else { try { localStorage.setItem('gb-lang', c); } catch (e) {} }
      setHistory([{ id: ++UID, command: null, kind: 'welcome', html: window.GB_DATA.welcome(c) }]);
      return;
    }
    if (c === 'clear') { setHistory([]); return; }

    // interactive easter eggs (side effects)
    if (c === 'theme') {
      const next = theme === 'dark' ? 'light' : 'dark';
      if (window.GBSite) window.GBSite.setTheme(next);
      else { setThemeState(next); try { localStorage.setItem('gb-theme', next); } catch (e) {} }
      setHistory((h) => [...h, { id: ++UID, command: raw, kind: 'output',
        html: `<div class="block dim">${lang === 'en' ? 'theme →' : 'tema →'} ${next}</div>` }]);
      return;
    }
    if (c === 'matrix') {
      runMatrix();
      setHistory((h) => [...h, { id: ++UID, command: raw, kind: 'output',
        html: `<div class="block dim">${lang === 'en' ? 'Wake up, Glauber… (4s)' : 'Acorde, Glauber… (4s)'}</div>` }]);
      return;
    }

    // multiword / power commands
    const head = c.split(/\s+/)[0];
    if (head === 'curl' || head === 'wget' || head === 'http' || head === 'fetch') {
      const html = window.GB_DATA.api(lang, c);
      setHistory((h) => [...h, { id: ++UID, command: raw, kind: 'output', html }]);
      return;
    }
    if (head === 'select') {
      const html = window.GB_DATA.sql(lang, c);
      setHistory((h) => [...h, { id: ++UID, command: raw, kind: html.startsWith('<div class="err"') ? 'error' : 'output', html }]);
      return;
    }
    if (head === 'tree') {
      setHistory((h) => [...h, { id: ++UID, command: raw, kind: 'output', html: window.GB_DATA.tree(lang) }]);
      return;
    }

    const resolved = ALIASES[c] || c;
    let html;
    if (window.GB_DATA[resolved] && COMMANDS.includes(resolved)) {
      html = window.GB_DATA[resolved](lang);
    } else {
      html = miscOutput(c, lang);
    }
    setHistory((h) => [...h, { id: ++UID, command: raw, kind: html.startsWith('<div class="err"') ? 'error' : 'output', html }]);
  }, [lang, theme, setTweak]);

  // Konami code → secret entry + a celebratory accent cycle
  useEffect(() => {
    const seq = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
    let pos = 0;
    const onKey = (e) => {
      const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
      pos = (k === seq[pos]) ? pos + 1 : (k === seq[0] ? 1 : 0);
      if (pos === seq.length) {
        pos = 0;
        const en = document.documentElement.lang === 'en';
        setHistory((h) => [...h, { id: ++UID, command: null, kind: 'output',
          html: `<div class="block"><span class="ok">▲▲▼▼◄►◄► B A</span> — ${en
            ? 'Konami unlocked. 30 extra lives of high availability granted.'
            : 'Konami desbloqueado. 30 vidas extras de alta disponibilidade concedidas.'}</div>` }]);
        runMatrix(2600);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  const onKeyDown = (e) => {
    if (e.key === 'Enter') {
      run(input);
      setInput('');
      setHIndex(-1);
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      const cmds = history.filter((x) => x.command != null && x.command.trim()).map((x) => x.command);
      if (!cmds.length) return;
      const ni = Math.min(hIndex + 1, cmds.length - 1);
      setHIndex(ni);
      setInput(cmds[cmds.length - 1 - ni] || '');
    } else if (e.key === 'ArrowDown') {
      e.preventDefault();
      const cmds = history.filter((x) => x.command != null && x.command.trim()).map((x) => x.command);
      const ni = Math.max(hIndex - 1, -1);
      setHIndex(ni);
      setInput(ni === -1 ? '' : cmds[cmds.length - 1 - ni] || '');
    } else if (e.key === 'Tab') {
      e.preventDefault();
      const all = [...COMMANDS, 'help', 'clear', 'en', 'pt', 'curl', 'tree', 'status', 'arch', 'rescue'];
      const m = all.find((x) => x.startsWith(input.trim().toLowerCase()));
      if (m) setInput(m);
    } else if (e.key === 'l' && e.ctrlKey) {
      e.preventDefault();
      setHistory([]);
    }
  };

  // click anywhere focuses input; click on [data-cmd] runs that command
  const onBodyClick = (e) => {
    const cmdEl = e.target.closest('[data-cmd]');
    if (cmdEl) {
      const c = cmdEl.getAttribute('data-cmd');
      run(c);
      setInput('');
      setHIndex(-1);
      inputRef.current?.focus();
      return;
    }
    if (window.getSelection && String(window.getSelection())) return; // don't steal text selection
    inputRef.current?.focus();
  };

  return (
    <div className="stage">
      <div className="window" role="application" aria-label="Terminal — Glauber Borges">
        <div className="titlebar">
          <div className="lights" aria-hidden="true">
            <span className="dot d-r"></span><span className="dot d-y"></span><span className="dot d-g"></span>
          </div>
          <div className="title">glauber@borges <span className="t-dim">— zsh — 80×24</span></div>
          <div className="status"><span className="ok">●</span> {lang === 'en' ? 'online' : 'online'}</div>
        </div>

        <div className="body" ref={bodyRef} onClick={onBodyClick}>
          {history.map((entry) => <Entry key={entry.id} entry={entry} />)}

          <div className="inputline">
            <span className="prompt" aria-hidden="true">
              <span className="p-user">glauber@borges</span><span className="p-dim">:</span><span className="p-path">~</span><span className="p-dim">$</span>
            </span>
            <input
              ref={inputRef}
              className="cmd-input"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={onKeyDown}
              spellCheck="false"
              autoCapitalize="off"
              autoComplete="off"
              autoCorrect="off"
              aria-label={lang === 'en' ? 'Type a command' : 'Digite um comando'}
            />
            <span className="block-cursor" aria-hidden="true"></span>
            <span className="inp-spacer"></span>
          </div>
        </div>

        <div className="footer">
          <span>{lang === 'en'
            ? <>Type <b>help</b> · ↑ ↓ history · Tab autocomplete</>
            : <>Digite <b>help</b> · ↑ ↓ histórico · Tab autocompleta</>}</span>
          <span className="f-dim">{lang === 'en' ? 'clear to reset' : 'clear para limpar'}</span>
        </div>
      </div>

      <TweaksPanel>
        <TweakSection label={lang === 'en' ? 'Appearance' : 'Aparência'} />
        <TweakColor
          label={lang === 'en' ? 'Accent' : 'Cor de destaque'}
          value={t.accent}
          options={['#7ee787', '#e3b341', '#79c0ff', '#e6e8ec']}
          onChange={(v) => setTweak('accent', v)}
        />
        <TweakSlider
          label={lang === 'en' ? 'Font size' : 'Tamanho do texto'}
          value={t.fontSize}
          min={12} max={17} step={1} unit="px"
          onChange={(v) => setTweak('fontSize', v)}
        />
      </TweaksPanel>
    </div>
  );
}

/* misc / easter-egg commands */
function miscOutput(c, lang) {
  const en = lang === 'en';
  const date = new Date().toLocaleString(en ? 'en-US' : 'pt-BR');
  const blk = (s, cls) => `<div class="block${cls ? ' ' + cls : ''}">${s}</div>`;

  switch (c) {
    case 'whoami': return blk('glauber');
    case 'pwd': return blk('/home/glauber/payments');
    case 'date': return blk(date);
    case 'uname': return blk('devbox 6.10.0-backend x86_64 GNU/Coffee');
    case 'echo': return blk('echo $WHO → glauber', 'dim');
    case 'uptime':
      return blk(en
        ? `up 10+ years,  4 squads,  load average: high availability`
        : `no ar há 10+ anos,  4 squads,  load average: alta disponibilidade`, 'dim');

    case 'neofetch': case 'screenfetch': return neofetch(en);

    case 'sudo':
      return blk(en ? 'Nice try. glauber is not in the sudoers file. This incident will be reported. 👀'.replace(' 👀','')
                    : 'Boa tentativa. glauber não está no arquivo sudoers. Este incidente será reportado.', 'dim');
    case 'sudo su': case 'su':
      return blk(en ? 'Permission denied. The payments cluster does not negotiate.' : 'Permissão negada. O cluster de pagamentos não negocia.', 'dim');

    case 'vim': case 'vi': case 'nvim': case 'emacs':
      return blk(en ? `You opened ${c}. Now you live here. (try <span class="cmd" data-cmd="exit">exit</span>)` : `Você abriu ${c}. Agora você mora aqui. (tente <span class="cmd" data-cmd="exit">exit</span>)`, 'dim');
    case 'exit': case 'quit': case ':q': case ':wq': case ':q!':
      return blk(en ? "There is no escape from a good terminal. But there is a <span class='cmd' data-cmd='contact'>contact</span>." : 'Não há saída de um bom terminal. Mas há um <span class="cmd" data-cmd="contact">contato</span>.', 'dim');

    case 'coffee': case 'make coffee': case 'brew':
      return blk(en ? '☕  brewing… HTTP 418: I’m a teapot. Back-end still compiles though.'.replace('☕  ','> ')
                    : '> preparando café… HTTP 418: I’m a teapot. Mas o back-end compila mesmo assim.', 'dim');

    case 'git': case 'git status':
      return blk(`<span class="dim">On branch</span> <span class="ok">main</span>\n<span class="dim">${en ? 'Your career is ahead of origin/main by 10 years.' : 'Sua carreira está 10 anos à frente de origin/main.'}</span>`.replace('\n','<br/>'));
    case 'git blame':
      return blk(en ? 'It was probably the gateway. It’s always the gateway.' : 'Provavelmente foi o gateway. É sempre o gateway.', 'dim');

    case 'rm -rf /': case 'rm -rf': case 'rm -rf /*':
      return blk(en ? 'Refused. Production is sacred. 🛡 We don’t do that here.'.replace(' 🛡','')
                    : 'Recusado. Produção é sagrada. A gente não faz isso aqui.', 'dim');

    case 'ping':
      return blk(en ? '64 bytes from glauberborges.me: time=0.1ms — always reachable.' : '64 bytes de glauberborges.me: time=0.1ms — sempre disponível.', 'dim');
    case 'top': case 'htop': case 'ps':
      return blk([
        `<span class="dim">PID   COMMAND                 %CPU</span>`,
        `<span class="ok">1</span>     payments-core           42.0`,
        `<span class="ok">7</span>     side-projects           18.0`,
        `<span class="ok">15</span>    mentoring                12.0`,
        `<span class="ok">42</span>    learning (always)        ∞`,
      ].join('<br/>'));

    case 'joke': case 'fortune':
      return blk(randomFrom(en ? JOKES_EN : JOKES_PT), 'dim');
    case 'motd':
      return blk(en ? '“The best system is the one nobody notices is running.”' : '“O melhor sistema é aquele que ninguém percebe que está rodando.”', 'dim');

    case 'theme': return ''; // handled in run()
    case 'matrix': return ''; // handled in run()

    case 'hire': case 'hire glauber': case 'hire me':
      return blk(`${en ? 'Let’s talk. Reach out:' : 'Vamos conversar. Fale comigo:'} <a class="a" href="mailto:ola@glauberborges.me">ola@glauberborges.me</a>`);

    case 'sl': return blk(en ? 'You meant `ls`. The train has left the station anyway. 🚂'.replace(' 🚂','') : 'Você quis dizer `ls`. Mas o trem já partiu.', 'dim');
    case 'help me': case '?':
      return blk(en ? 'Aren’t we all. Try <span class="cmd" data-cmd="help">help</span>.' : 'Quem não, né. Tente <span class="cmd" data-cmd="help">help</span>.', 'dim');

    case 'secret': case 'eggs': case 'easter':
      return blk(en
        ? `psst — try: <span class="cmd" data-cmd="neofetch">neofetch</span> · <span class="cmd" data-cmd="matrix">matrix</span> · <span class="cmd" data-cmd="coffee">coffee</span> · <span class="cmd" data-cmd="top">top</span> · <span class="cmd" data-cmd="joke">joke</span> · <span class="cmd" data-cmd="sudo">sudo</span> · <span class="cmd" data-cmd="theme">theme</span> · and the Konami code ↑↑↓↓←→←→ b a`
        : `psiu — tente: <span class="cmd" data-cmd="neofetch">neofetch</span> · <span class="cmd" data-cmd="matrix">matrix</span> · <span class="cmd" data-cmd="coffee">coffee</span> · <span class="cmd" data-cmd="top">top</span> · <span class="cmd" data-cmd="joke">joke</span> · <span class="cmd" data-cmd="sudo">sudo</span> · <span class="cmd" data-cmd="theme">theme</span> · e o código Konami ↑↑↓↓←→←→ b a`, 'dim');

    default:
      return `<div class="err">${en ? 'command not found' : 'comando não encontrado'}: ${escapeHtml(c)}<br/><span class="dim">${en ? 'type' : 'digite'} <span class="cmd" data-cmd="help">help</span> ${en ? 'or' : 'ou'} <span class="cmd" data-cmd="secret">secret</span></span></div>`;
  }
}

const JOKES_PT = [
  'Existem 2 tipos de problema em sistemas distribuídos: 2. entrega exatamente uma vez 1. ordem das mensagens 2. entrega exatamente uma vez.',
  'Não é bug, é uma feature não documentada com alta disponibilidade.',
  'Cache invalidation, naming things, e contar pessoas em uma daily.',
  'O deploy passou em staging. — frase dita momentos antes do desastre.',
  'Funciona na minha máquina. ☑ — então mandamos sua máquina pra produção.'.replace(' ☑',''),
];
const JOKES_EN = [
  'There are 2 hard problems in distributed systems: 2. exactly-once delivery 1. message ordering 2. exactly-once delivery.',
  'It’s not a bug, it’s an undocumented feature with high availability.',
  'Cache invalidation, naming things, and counting people in a standup.',
  'It passed in staging. — words said moments before disaster.',
  'It works on my machine. — so we shipped my machine to production.',
];
function randomFrom(a) { return a[Math.floor(Math.random() * a.length)]; }

function neofetch(en) {
  const logo = [
    "        __        ",
    "   ___ / /  __ _  ",
    "  / _ `/ _ \\/  ' \\ ",
    "  \\_, /_.__/_/_/_/ ",
    " /___/   payments ",
  ].join('\n');
  const rows = [
    ['OS', 'devbox · Backend Linux'],
    ['Host', 'Tech Lead'],
    ['Uptime', en ? '10+ years' : '10+ anos'],
    ['Shell', 'zsh · php · laravel'],
    ['Kernel', 'microservices 6.x (AWS · Azure · K8s)'],
    ['Stack', 'PHP · Redis · RabbitMQ · Elasticsearch'],
    ['Editor', 'nvim · Cursor · Claude Code'],
    ['Locale', en ? 'remote · UTC-3' : 'remoto · UTC-3'],
  ].map(([k, v]) => `<span class="nf-k">${k}</span><span class="nf-v">${v}</span>`).join('');
  const bar = ['#ec6a5e','#f4bf4f','#61c554','#79c0ff','#b48ead','#56d4dd']
    .map((c) => `<span style="color:${c}">███</span>`).join('');
  return `<div class="block"><div class="nf">
  <pre class="nf-logo">${logo}</pre>
  <div class="nf-info">
    <div class="nf-id">glauber@borges</div>
    <div class="nf-rule">─────────────</div>
    ${rows}
    <div class="nf-bar">${bar}</div>
  </div>
</div></div>`;
}
function escapeHtml(s) { return s.replace(/[&<>"]/g, (m) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[m])); }

/* Matrix rain overlay — brief, tasteful, self-cleaning */
let __matrixRunning = false;
function runMatrix(duration) {
  if (__matrixRunning) return;
  __matrixRunning = true;
  duration = duration || 4000;
  const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const canvas = document.createElement('canvas');
  canvas.id = 'matrix-rain';
  document.body.appendChild(canvas);
  const ctx = canvas.getContext('2d');
  const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#7ee787';
  let w, h, cols, drops, fontSize = 16;
  const chars = 'アイウエオカキクケコ0123456789{}[]<>=+*/$#&;PHP'.split('');
  const resize = () => {
    w = canvas.width = window.innerWidth;
    h = canvas.height = window.innerHeight;
    cols = Math.floor(w / fontSize);
    drops = Array(cols).fill(0).map(() => Math.floor(Math.random() * -50));
  };
  resize();
  window.addEventListener('resize', resize);
  requestAnimationFrame(() => canvas.classList.add('on'));

  let raf;
  const draw = () => {
    ctx.fillStyle = 'rgba(8,8,10,0.10)';
    ctx.fillRect(0, 0, w, h);
    ctx.fillStyle = accent;
    ctx.font = fontSize + 'px JetBrains Mono, monospace';
    for (let i = 0; i < cols; i++) {
      const ch = chars[Math.floor(Math.random() * chars.length)];
      ctx.fillText(ch, i * fontSize, drops[i] * fontSize);
      if (drops[i] * fontSize > h && Math.random() > 0.975) drops[i] = 0;
      drops[i]++;
    }
    raf = requestAnimationFrame(draw);
  };
  if (reduce) { /* draw a single static frame */ draw(); cancelAnimationFrame(raf); }
  else raf = requestAnimationFrame(draw);

  setTimeout(() => {
    canvas.classList.remove('on');
    setTimeout(() => {
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', resize);
      canvas.remove();
      __matrixRunning = false;
    }, 450);
  }, duration);
}

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