// newcomer-app.jsx — Minimal dark resume + hacker terminal drop-down.

const { useState, useEffect, useRef, useMemo } = React;

// ───────────────────────────────────────────────────────────────
// VISUAL FX (confetti / matrix / glitch / doggo)
// ───────────────────────────────────────────────────────────────
function burstConfetti() {
  const root = document.createElement('div');
  root.className = 'fx-confetti';
  document.body.appendChild(root);
  const COLORS = ['#5fd9e8', '#7be8f5', '#b8568f', '#e26eb0', '#e6e6ec', '#efb756'];
  const N = 90;
  for (let i = 0; i < N; i++) {
    const s = document.createElement('span');
    const dx = (Math.random() - 0.5) * window.innerWidth * 1.4 + 'px';
    s.style.left = (window.innerWidth / 2) + 'px';
    s.style.top = (window.innerHeight / 2) + 'px';
    s.style.background = COLORS[i % COLORS.length];
    s.style.setProperty('--dx', dx);
    s.style.transform = 'rotate(' + Math.random() * 360 + 'deg)';
    s.style.animationDelay = (Math.random() * 0.18) + 's';
    root.appendChild(s);
  }
  setTimeout(() => root.remove(), 2200);
}

function runMatrix(duration = 4200) {
  const wrap = document.createElement('div');
  wrap.className = 'fx-matrix';
  const canvas = document.createElement('canvas');
  wrap.appendChild(canvas);
  document.body.appendChild(wrap);
  const ctx = canvas.getContext('2d');
  function resize() {
    canvas.width = window.innerWidth * devicePixelRatio;
    canvas.height = window.innerHeight * devicePixelRatio;
    canvas.style.width = window.innerWidth + 'px';
    canvas.style.height = window.innerHeight + 'px';
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.scale(devicePixelRatio, devicePixelRatio);
  }
  resize();
  const charset = '01abcdefghijklmnopqrstuvwxyzNEWCOMER<>{}[]/\\$#@*+-=';
  const fontSize = 14;
  const cols = Math.floor(window.innerWidth / fontSize);
  const drops = new Array(cols).fill(0).map(() => Math.random() * -100);
  ctx.font = fontSize + 'px JetBrains Mono, monospace';
  let raf, t0 = performance.now();
  function frame(t) {
    ctx.fillStyle = 'rgba(6,6,10,0.18)';
    ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
    for (let i = 0; i < cols; i++) {
      const c = charset[Math.floor(Math.random() * charset.length)];
      const x = i * fontSize;
      const y = drops[i] * fontSize;
      // leading char brighter
      ctx.fillStyle = '#7be8f5';
      ctx.fillText(c, x, y);
      ctx.fillStyle = 'rgba(95,217,232,0.55)';
      ctx.fillText(c, x, y - fontSize);
      drops[i] += 1;
      if (y > window.innerHeight && Math.random() > 0.975) drops[i] = 0;
    }
    if (t - t0 < duration) raf = requestAnimationFrame(frame);
    else {
      wrap.style.transition = 'opacity 0.4s';
      wrap.style.opacity = '0';
      setTimeout(() => wrap.remove(), 420);
    }
  }
  raf = requestAnimationFrame(frame);
}

function flashGlitch() {
  const el = document.querySelector('[data-glitch-target]');
  if (!el) return;
  el.classList.add('go');
  setTimeout(() => el.classList.remove('go'), 420);
}

function spawnGlitchFault() {
  const band = document.createElement('span');
  band.className = 'fx-glitch-band';
  const width = Math.max(18, Math.floor(Math.random() * 60));
  const height = Math.max(1, Math.floor(Math.random() * 4));
  band.style.left = `${Math.random() * 72}%`;
  band.style.top = `${Math.random() * 94}%`;
  band.style.width = `${width}%`;
  band.style.height = `${height}px`;
  band.style.opacity = `${0.16 + Math.random() * 0.26}`;
  band.style.animationDelay = `${Math.random() * 0.12}s`;
  document.body.appendChild(band);
  setTimeout(() => band.remove(), 220);
}

function runDoggo() {
  const wrap = document.createElement('div');
  wrap.className = 'fx-dog';
  const sprite = document.createElement('span');
  sprite.className = 'fx-dog-sprite';
  const frameA = document.createElement('img');
  const frameB = document.createElement('img');
  frameA.className = 'dog-frame a right-run';
  frameB.className = 'dog-frame b right-run';
  frameA.src = 'assets/havana.webp';
  frameB.src = 'assets/bella.webp';
  frameA.alt = 'havana';
  frameB.alt = 'bella';
  frameA.setAttribute('aria-hidden', 'true');
  frameB.setAttribute('aria-hidden', 'true');
  sprite.appendChild(frameA);
  sprite.appendChild(frameB);
  wrap.appendChild(sprite);
  document.body.appendChild(wrap);
  setTimeout(() => wrap.remove(), 8000);
}

let IP_IDENTITY_CACHE = null;
let IP_IDENTITY_PROMISE = null;
const FALLBACK_IP_IDENTITY = {
  ip: '127.0.0.1',
  source: 'local fallback',
  location: 'unknown',
};

function getIpIdentity() {
  if (IP_IDENTITY_CACHE) return Promise.resolve(IP_IDENTITY_CACHE);
  if (IP_IDENTITY_PROMISE) return IP_IDENTITY_PROMISE;

  IP_IDENTITY_PROMISE = fetch('https://api.ipify.org?format=json')
    .then((resp) => {
      if (!resp.ok) throw new Error('ip lookup failed');
      return resp.json();
    })
    .then((data) => {
      IP_IDENTITY_CACHE = {
        ip: data.ip || FALLBACK_IP_IDENTITY.ip,
        source: 'ipify.org',
        location: 'unknown',
      };
      return IP_IDENTITY_CACHE;
    })
    .catch(() => {
      IP_IDENTITY_CACHE = FALLBACK_IP_IDENTITY;
      return IP_IDENTITY_CACHE;
    });

  return IP_IDENTITY_PROMISE;
}

function printDevtoolsArt() {
  const art = `
   .--._.--.
  ( o    o )      hi — i'm newcomer.
   \\  ~~  /       agentic engineer · consultant
    '-..-'        try the terminal:  press \`  then  help
   /      \\
  ( /  \\/ \\)
`;
  console.log('%c' + art, 'color:#5fd9e8; font-family: monospace; font-size: 12px; line-height: 1.4;');
}

// ───────────────────────────────────────────────────────────────
// RESUME CONTENT
// ───────────────────────────────────────────────────────────────
const RESUME = {
  name: 'R. Newcomer',
  role: 'B.S. Accounting',
  edu: 'BS, Accounting',
  linkedin: 'https://www.linkedin.com/in/rnewcomer/',
  linkedinHandle: '/in/rnewcomer',
  headline: 'Agentic Engineer',
  linkedinCover: 'assets/cover_art.png',
  linkedinAvatar: 'assets/headshot_RN.png',
  about: [
    'I work at the intersection of finance, accounting, fintech, and agentic engineering.',
    'Overall, I am focused on using AI agents and financial analysis to build faster, cleaner, and more scalable business processes.',
  ],
  experience: [
    {
      title: 'Agentic Engineer',
      org: 'Independent',
      tags: ['#1 Vibecoder in the Midwest 🏆'],
    },
  ],
  education: [
    {
      title: 'University of Southern Missisippi',
      org: 'B.S. Accounting',
      gpa: '3.7',
      achievements: ['99 percentile EFT Business 198/200', '98 percentile 915/1000'],
    },
  ],
  projects: [
    {
      id: 'agent-ledger',
      title: 'Agent Ledger',
      status: 'active',
      stack: ['AI agents', 'accounting ops', 'controls'],
      summary: 'A finance workflow lab for turning messy accounting tasks into repeatable agent-assisted processes.',
      commands: ['audit --inbox', 'reconcile --vendors', 'export --control-log'],
    },
    {
      id: 'pweb',
      title: 'Newcomer.dev',
      status: 'live',
      stack: ['React', 'canvas', 'terminal UI'],
      summary: 'This portfolio system: network background, resume surface, hidden terminal, and interactive project browser.',
      commands: ['open --profile', 'scan --projects', 'theme --cyber'],
    },
    {
      id: 'ops-stack',
      title: 'Ops Stack',
      status: 'prototype',
      stack: ['Xero', 'ERPNext', 'Cloudflare'],
      summary: 'A practical automation stack for connecting bookkeeping, reporting, and small business operating data.',
      commands: ['sync --ledger', 'route --forms', 'report --variance'],
    },
  ],
  articles: [
    {
      title: 'Building agentic finance workflows without losing controls',
      status: 'drafting',
      summary: 'A practical note on using agents around accounting work while keeping review, evidence, and approvals intact.',
    },
    {
      title: 'What accountants should expect from AI operators',
      status: 'notes',
      summary: 'A field guide for where AI operators help, where they still need supervision, and what changes in daily finance work.',
    },
  ],
  contact: {
    email: 'ryan@newcomer.dev',
    linkedin: 'https://www.linkedin.com/in/rnewcomer/',
    socials: [
      { label: 'GitHub', value: 'github.com/rnewcomer' },
      { label: 'Twitter / X', value: 'x.com/rnewcomer' },
      { label: 'Substack', value: 'coming soon' },
    ],
  },
  skills: {
    stack: ['Codex', 'ChatGPT', 'Gemini', 'Ollama'],
    tools: ['Xero', 'Erpnext', 'Cloudflare', 'Expo'],
  },
};

function NetworkBackground() {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    const nodes = [];
    const FRAME_INTERVAL = 1000 / 30;
    const MAX_LINKS_PER_NODE = 3;
    let raf = 0;
    let width = 0;
    let height = 0;
    let dpr = 1;
    let lastSpawn = 0;
    let lastFrame = 0;
    let nextId = 1;

    const rand = (min, max) => min + Math.random() * (max - min);
    const maxNodes = () => Math.max(18, Math.min(46, Math.floor((width * height) / 42000)));
    const linkDistance = () => Math.max(74, Math.min(132, width * 0.095));

    function makeNode(edgeSpawn = false) {
      const fromLeft = Math.random() > 0.5;
      const x = edgeSpawn ? (fromLeft ? -24 : width + 24) : rand(0, width);
      const y = rand(36, Math.max(72, height - 36));
      const speed = rand(0.12, 0.42);

      return {
        id: nextId++,
        x,
        y,
        vx: edgeSpawn ? (fromLeft ? speed : -speed) : rand(-speed, speed),
        vy: rand(-speed, speed),
        r: rand(1.4, 3.1),
        born: performance.now(),
        seed: Math.random() * 360,
        pulse: rand(0.45, 1.05),
      };
    }

    function resize() {
      dpr = Math.min(window.devicePixelRatio || 1, 1.35);
      width = window.innerWidth;
      height = window.innerHeight;
      canvas.width = Math.floor(width * dpr);
      canvas.height = Math.floor(height * dpr);
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

      while (nodes.length < maxNodes() * 0.62) nodes.push(makeNode(false));
      if (nodes.length > maxNodes()) nodes.length = maxNodes();
    }

    function findRoot(parent, id) {
      while (parent.get(id) !== id) {
        parent.set(id, parent.get(parent.get(id)));
        id = parent.get(id);
      }
      return id;
    }

    function join(parent, a, b) {
      const ra = findRoot(parent, a);
      const rb = findRoot(parent, b);
      if (ra !== rb) parent.set(rb, ra);
    }

    function componentColor(componentId, t, size) {
      const hue = (componentId * 47 + t * 0.012) % 360;
      const flash = 0.58 + Math.sin(t * 0.0022 + componentId) * 0.42;
      const sat = 68 + flash * 14;
      const light = size > 1 ? 55 + flash * 10 : 46 + flash * 6;
      return {
        hue,
        flash,
        node: `hsla(${hue}, ${sat}%, ${light}%, 0.78)`,
        edge: `hsla(${hue}, ${sat}%, ${light}%, ${0.10 + flash * 0.14})`,
        glow: `hsla(${hue}, ${sat}%, ${light}%, ${0.12 + flash * 0.16})`,
      };
    }

    function draw(t) {
      if (document.hidden) {
        lastFrame = t;
        raf = requestAnimationFrame(draw);
        return;
      }

      if (t - lastFrame < FRAME_INTERVAL) {
        raf = requestAnimationFrame(draw);
        return;
      }
      lastFrame = t;

      const target = maxNodes();
      if (t - lastSpawn > 1200 && nodes.length < target) {
        nodes.push(makeNode(true));
        lastSpawn = t;
      }

      ctx.clearRect(0, 0, width, height);
      ctx.globalCompositeOperation = 'lighter';

      const parent = new Map();
      const edges = [];
      const linkCounts = new Map();
      const distLimit = linkDistance();
      const distLimitSq = distLimit * distLimit;
      const maxEdges = target * 2.25;

      for (const node of nodes) parent.set(node.id, node.id);

      for (const node of nodes) {
        const drift = Math.sin(t * 0.00035 + node.seed) * 0.004;
        node.vx += Math.cos(t * 0.00025 + node.seed) * 0.002;
        node.vy += drift;
        node.vx = Math.max(-0.48, Math.min(0.48, node.vx));
        node.vy = Math.max(-0.36, Math.min(0.36, node.vy));
        node.x += node.vx;
        node.y += node.vy;

        if (node.x < -42) node.x = width + 42;
        if (node.x > width + 42) node.x = -42;
        if (node.y < 24 || node.y > height - 24) {
          node.vy *= -1;
          node.y = Math.max(24, Math.min(height - 24, node.y));
        }
      }

      for (let i = 0; i < nodes.length; i++) {
        for (let j = i + 1; j < nodes.length; j++) {
          const a = nodes[i];
          const b = nodes[j];
          const dx = a.x - b.x;
          const dy = a.y - b.y;
          const distSq = dx * dx + dy * dy;
          if (
            distSq < distLimitSq &&
            (linkCounts.get(a.id) || 0) < MAX_LINKS_PER_NODE &&
            (linkCounts.get(b.id) || 0) < MAX_LINKS_PER_NODE &&
            edges.length < maxEdges
          ) {
            const strength = 1 - Math.sqrt(distSq) / distLimit;
            edges.push([a, b, strength]);
            linkCounts.set(a.id, (linkCounts.get(a.id) || 0) + 1);
            linkCounts.set(b.id, (linkCounts.get(b.id) || 0) + 1);
            join(parent, a.id, b.id);
          }
        }
      }

      const componentSizes = new Map();
      for (const node of nodes) {
        const root = findRoot(parent, node.id);
        componentSizes.set(root, (componentSizes.get(root) || 0) + 1);
      }

      const colors = new Map();
      for (const [root, size] of componentSizes) {
        colors.set(root, componentColor(root, t, size));
      }

      for (const [a, b, strength] of edges) {
        const root = findRoot(parent, a.id);
        const color = colors.get(root);
        ctx.strokeStyle = color.edge;
        ctx.lineWidth = 0.45 + strength * 1.05;
        ctx.shadowBlur = 7 + strength * 10;
        ctx.shadowColor = color.glow;
        ctx.beginPath();
        ctx.moveTo(a.x, a.y);
        ctx.lineTo(b.x, b.y);
        ctx.stroke();
      }

      for (const node of nodes) {
        const root = findRoot(parent, node.id);
        const size = componentSizes.get(root);
        const color = colors.get(root);
        const soloTwinkle = size === 1 ? Math.sin(t * 0.0025 * node.pulse + node.seed) * 0.5 + 0.5 : color.flash;
        const radius = node.r + soloTwinkle * 1.25;

        ctx.fillStyle = color.node;
        ctx.shadowBlur = 6 + soloTwinkle * 10;
        ctx.shadowColor = color.glow;
        ctx.beginPath();
        ctx.arc(node.x, node.y, radius, 0, Math.PI * 2);
        ctx.fill();

        ctx.strokeStyle = `hsla(${color.hue}, 92%, 72%, ${0.14 + soloTwinkle * 0.26})`;
        ctx.lineWidth = 0.8;
        ctx.beginPath();
        ctx.arc(node.x, node.y, radius + 3 + soloTwinkle * 2.2, 0, Math.PI * 2);
        ctx.stroke();
      }

      if (nodes.length > target) nodes.splice(0, nodes.length - target);
      raf = requestAnimationFrame(draw);
    }

    resize();
    window.addEventListener('resize', resize);
    raf = requestAnimationFrame(draw);

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', resize);
    };
  }, []);

  return (
    <div className="network-layer" aria-hidden="true">
      <canvas className="network-canvas" ref={canvasRef} />
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// TERMINAL
// ───────────────────────────────────────────────────────────────
const BOOT_LINES = [
  { cls: 'l-dim', t: 'nc-os v1.0.0 · loading kernel ...' },
  { cls: 'l-dim', t: "type 'help' for a list of commands · 'exit' to close" },
];

function Terminal({ open, onClose, onSecretFired }) {
  const [log, setLog] = useState(BOOT_LINES);
  const [input, setInput] = useState('');
  const [ipIdentity, setIpIdentity] = useState(FALLBACK_IP_IDENTITY);
  const [history, setHistory] = useState([]);
  const [hIdx, setHIdx] = useState(-1);
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [loginState, setLoginState] = useState('idle');
  const [loginUser, setLoginUser] = useState('');
  const [isGlitching, setIsGlitching] = useState(false);
  const inputRef = useRef(null);
  const logRef = useRef(null);
  const glitchLoop = useRef(null);

  useEffect(() => {
    if (open) {
      setTimeout(() => inputRef.current && inputRef.current.focus(), 50);
    }
  }, [open]);

  useEffect(() => {
    let active = true;
    getIpIdentity().then((identity) => {
      if (active) setIpIdentity(identity);
    });
    return () => {
      active = false;
    };
  }, []);

  useEffect(() => {
    if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
  }, [log]);

  useEffect(() => {
    return () => {
      if (glitchLoop.current) {
        clearInterval(glitchLoop.current);
        glitchLoop.current = null;
      }
      document.body.classList.remove('glitch-active');
      const term = document.querySelector('.term');
      if (term) term.classList.remove('term-cursor');
      setIsGlitching(false);
    };
  }, []);

  const stopGlitching = () => {
    if (glitchLoop.current) {
      clearInterval(glitchLoop.current);
      glitchLoop.current = null;
    }
    document.body.classList.remove('glitch-active');
    document.body.style.removeProperty('--glitch-x');
    document.body.style.removeProperty('--glitch-y');
    document.body.style.removeProperty('--glitch-skew');
    document.body.style.removeProperty('--glitch-pulse');
    const term = document.querySelector('.term');
    if (term) {
      term.classList.remove('term-cursor');
      term.style.transform = '';
      term.style.filter = '';
    }
    document.querySelectorAll('.fx-glitch-band').forEach((el) => el.remove());
    setIsGlitching(false);
  };

  const append = (lines) => setLog((prev) => [...prev, ...(Array.isArray(lines) ? lines : [lines])]);

  const runGlitchFx = () => {
    if (isGlitching) {
      append({ cls: 'l-dim', t: 'glitch loop already running — type `sorry` to stop.' });
      return;
    }
    setIsGlitching(true);
    document.body.classList.add('glitch-active');
    document.body.style.setProperty('--glitch-x', '0px');
    document.body.style.setProperty('--glitch-y', '0px');
    const term = document.querySelector('.term');
    if (term) term.classList.add('term-cursor');
    flashGlitch();
    if (glitchLoop.current) clearInterval(glitchLoop.current);
    glitchLoop.current = setInterval(() => {
      const x = ((Math.random() - 0.5) * 7).toFixed(2);
      const y = ((Math.random() - 0.5) * 5).toFixed(2);
      const skew = ((Math.random() - 0.5) * 2.4).toFixed(2);
      const pulse = (Math.random() * 1.4 + 0.78).toFixed(2);
      document.body.style.setProperty('--glitch-x', `${x}px`);
      document.body.style.setProperty('--glitch-y', `${y}px`);
      document.body.style.setProperty('--glitch-skew', `${skew}deg`);
      document.body.style.setProperty('--glitch-pulse', pulse);
      if (term) {
        term.style.transform = `translate(${(x / 3)}px, ${(y / 3)}px) skew(${skew}deg)`;
      }
      if (Math.random() > 0.52) flashGlitch();
      if (Math.random() > 0.28) spawnGlitchFault();
    }, 90);
    append({ cls: 'l-dim', t: 'glitch persists — enter `sorry` to stop.' });
    append({ cls: 'l-dim', t: 'visual layer: active' });
    append({ cls: 'l-dim', t: 'cursor jitter: active' });
  };

  const COMMANDS = useMemo(() => ({
    help: () => append([
      { cls: 'l-sys', t: 'available commands:' },
      { cls: 'l-out', t: '  help            — this list' },
      { cls: 'l-out', t: '  whoami          — identity (IP first, personal details when logged in)' },
      { cls: 'l-out', t: '  ls              — list resume sections' },
      { cls: 'l-out', t: '  cat <section>   — read a section (about · experience · education · projects · articles · skills · contact)' },
      { cls: 'l-out', t: '  linkedin        — open LinkedIn profile' },
      { cls: 'l-out', t: '  neovim          — terminal portrait summary' },
      { cls: 'l-out', t: '  doggo           — run a doggo sprite animation' },
      { cls: 'l-out', t: '  codex           — estimated stats panel' },
      { cls: 'l-out', t: '  claude          — alias of `codex`' },
      { cls: 'l-out', t: '  clear           — clear the log' },
      { cls: 'l-out', t: '  exit            — close terminal' },
      { cls: 'l-dim', t: '  ── some commands are hidden. some you will discover by trying.' },
    ]),
    whoami: () => {
      const out = [
        { cls: 'l-sys', t: 'identity snapshot:' },
        { cls: 'l-out', t: `ip        · ${ipIdentity.ip}` },
        { cls: 'l-out', t: `source    · ${ipIdentity.source}` },
        { cls: 'l-out', t: `location  · ${ipIdentity.location}` },
      ];

      if (isLoggedIn) {
        out.push(
          { cls: 'l-out', t: 'operator  · R. Newcomer' },
          { cls: 'l-out', t: `role      · ${RESUME.role}` },
          { cls: 'l-out', t: 'focus     · LLM ops, workflow automation, finance tooling' },
          { cls: 'l-out', t: 'stack     · ' + RESUME.skills.stack.join(', ') },
          { cls: 'l-out', t: 'tools     · ' + RESUME.skills.tools.join(', ') },
          { cls: 'l-ok', t: 'access    · expanded terminal context enabled' },
        );
      } else {
        out.push(
          { cls: 'l-warn', t: 'access    · guest session (no private payload)' },
        );
      }

      append(out);
    },
    login: () => {
      if (isLoggedIn) {
        append({ cls: 'l-ok', t: 'already authenticated as ryan.' });
        return;
      }
      if (loginState !== 'idle') {
        append({ cls: 'l-warn', t: 'auth: already in progress' });
        return;
      }
      setLoginState('awaiting-user');
      append({ cls: 'l-sys', t: 'login: start with username' });
      append({ cls: 'l-out', t: 'username:' });
    },
    logout: () => {
      setIsLoggedIn(false);
      setLoginState('idle');
      setLoginUser('');
      append({ cls: 'l-sys', t: 'logout complete · session reset' });
    },
    ls: () => append([{ cls: 'l-out', t: 'about/  experience/  education/  projects/  articles/  skills/  contact/' }]),
    cat: (args) => {
      const k = (args[0] || '').toLowerCase().replace(/\/$/, '');
      if (!k) return append({ cls: 'l-err', t: 'usage: cat <about|experience|education|projects|articles|skills|contact>' });

      switch (k) {
        case 'about':
          return append([{ cls: 'l-sys', t: '── about ──' }, ...RESUME.about.map((p) => ({ cls: 'l-out', t: p }))]);
        case 'experience': {
          append({ cls: 'l-sys', t: '── experience ──' });
          RESUME.experience.forEach((e) => {
            append([
              { cls: 'l-out', t: `${e.title}  ·  ${e.org}` },
            ]);
            if (Array.isArray(e.tags) && e.tags.length) {
              append({ cls: 'l-dim', t: `         ${e.tags.join('  ·  ')}` });
            }
          });
          return;
        }
        case 'education': {
          append({ cls: 'l-sys', t: '── education ──' });
          RESUME.education.forEach((e) => {
            append([
              { cls: 'l-out', t: `${e.title}  ·  ${e.org}` },
              { cls: 'l-dim', t: `         GPA: ${e.gpa}` },
            ]);
            if (Array.isArray(e.achievements) && e.achievements.length) {
              append({ cls: 'l-dim', t: `         Achievements: ${e.achievements.join('  ·  ')}` });
            }
          });
          return;
        }
        case 'projects': {
          append({ cls: 'l-sys', t: '── projects ──' });
          RESUME.projects.forEach((p) => {
            append([
              { cls: 'l-out', t: `${p.title} · ${p.status}` },
              { cls: 'l-dim', t: p.summary },
              { cls: 'l-dim', t: p.stack.join('  ·  ') },
            ]);
          });
          return;
        }
        case 'articles': {
          append({ cls: 'l-sys', t: '── articles · work in progress ──' });
          RESUME.articles.forEach((a) => append([
            { cls: 'l-out', t: `${a.title} · ${a.status}` },
            { cls: 'l-dim', t: a.summary },
          ]));
          return;
        }
        case 'skills':
          return append([
            { cls: 'l-sys', t: '── skills ──' },
            { cls: 'l-out', t: 'stack    · ' + RESUME.skills.stack.join('  ') },
            { cls: 'l-out', t: 'tools    · ' + RESUME.skills.tools.join('  ') },
          ]);
        case 'contact':
        case 'links':
          return append([
            { cls: 'l-sys', t: '── contact ──' },
            { cls: 'l-out', t: `email    · ${RESUME.contact.email}` },
            { cls: 'l-dim', t: 'socials  · github/x/bluesky cards are staged but not linked yet' },
          ]);
        default:
          return append({ cls: 'l-err', t: `cat: ${k}: no such file or section` });
      }
    },
    linkedin: () => {
      append({ cls: 'l-ok', t: '→ opening LinkedIn profile card...' });
      window.open(RESUME.linkedin, '_blank', 'noopener');
    },
    clear: () => setLog([]),
    exit: () => onClose(),
    close: () => onClose(),

    // ── secrets ──
    confetti: () => {
      append({ cls: 'l-mag', t: '✦ EGG · confetti dispatched.' });
      burstConfetti();
      onSecretFired && onSecretFired('confetti');
    },
    matrix: () => {
      append({ cls: 'l-mag', t: '✦ EGG · entering the matrix · 4s ...' });
      runMatrix();
      onSecretFired && onSecretFired('matrix');
    },
    glitch: () => {
      append({ cls: 'l-mag', t: '✦ EGG · glitching page + cursor ...' });
      runGlitchFx();
      onSecretFired && onSecretFired('glitch');
    },
    doggo: () => {
      append({ cls: 'l-mag', t: '✦ EGG · doggo dispatched.' });
      runDoggo();
      onSecretFired && onSecretFired('doggo');
    },
    neovim: () => {
      const portrait = `
 /\\_/\\      /\\_/\\
( o.o )    ( ^.^ )
 > ^ <      > ^ <
/   \\      /   \\
(_____)    (_____)
`;
      append({ cls: 'l-block', t: portrait });
      append([
        { cls: 'l-sys', t: 'brief:' },
        { cls: 'l-out', t: 'session role     : agentic engineer / operator workflows' },
        { cls: 'l-out', t: 'focus areas     : accounting context, LLM routing, execution reliability' },
        { cls: 'l-out', t: 'delivery style  : practical automations with clear handoff paths' },
      ]);
      onSecretFired && onSecretFired('neovim');
    },
    codex: () => {
      const stats = `
Model execution panel
--------------------
Tokens used        : 12,743
Plan type          : tactical + executive
Model routing      : primary=gpt-5 / fallback=gpt-5-mini
Eval pass count    : 3
Latency profile    : stable · p95 under 900ms
Tool selection     : confidence weighted
Output quality    : good
`; 
      append({ cls: 'l-block', t: stats });
      onSecretFired && onSecretFired('codex');
    },
    claude: () => COMMANDS.codex(),
    coffee: () => append([
      { cls: 'l-mag', t: '✦ ☕ added 1 coffee to operator queue.' },
      { cls: 'l-dim', t: '   (estimated wait: 7-9 min · 1 ahead of you)' },
    ]),
    sudo: () => append({ cls: 'l-err', t: 'sudo: permission denied · operator is not in sudoers file.' }),
    rm: () => append({ cls: 'l-err', t: 'rm: refusing to remove · nice try.' }),
    hack: () => append([
      { cls: 'l-warn', t: 'INITIATING MAINFRAME HACK ...' },
      { cls: 'l-dim', t: 'just kidding. type `help`.' },
    ]),
    secrets: () => append([
      { cls: 'l-sys', t: 'hidden commands:' },
      { cls: 'l-dim', t: '  · `confetti`  — visual celebrations.' },
      { cls: 'l-dim', t: '  · `matrix`    — green character rain.' },
      { cls: 'l-dim', t: '  · `glitch`    — page + cursor distortion (persists until `sorry`).' },
      { cls: 'l-dim', t: '  · `doggo`     — sprite animation from `havana` + `bella`.' },
      { cls: 'l-dim', t: '  · `sorry`     — stop the active glitch loop.' },
      { cls: 'l-dim', t: '  · `neovim`    — ascii portrait + profile blurb.' },
      { cls: 'l-dim', t: '  · `codex`/`claude` — stats panel.' },
      { cls: 'l-dim', t: '── solve the puzzles. they\'re short.' },
    ]),
    eggs: function () { COMMANDS.secrets(); },
    about: () => COMMANDS.cat(['about']),
    experience: () => COMMANDS.cat(['experience']),
    education: () => COMMANDS.cat(['education']),
    projects: () => COMMANDS.cat(['projects']),
    articles: () => COMMANDS.cat(['articles']),
    skills: () => COMMANDS.cat(['skills']),
    contact: () => COMMANDS.cat(['contact']),
    date: () => append({ cls: 'l-out', t: new Date().toString() }),
    echo: (args) => append({ cls: 'l-out', t: args.join(' ') }),
    open: (args) => {
      if ((args[0] || '').toLowerCase() === 'linkedin') return COMMANDS.linkedin();
      return append({ cls: 'l-err', t: 'open: unknown target · try `linkedin`.' });
    },
  }), [isLoggedIn, ipIdentity, isGlitching, onClose, onSecretFired, loginState]);

  const submit = (raw) => {
    const text = raw.trim();
    if (!text) {
      append({ cls: 'l-cmd', t: '$' });
      return;
    }
    append({ cls: 'l-cmd', t: '$ ' + text });
    setHistory((h) => [...h, text]);
    setHIdx(-1);

    const [cmd, ...args] = text.split(/\s+/);
    const lc = cmd.toLowerCase();

    if (lc === 'sorry') {
      if (isGlitching) {
        stopGlitching();
        append({ cls: 'l-ok', t: 'stability restored · visual glitch effects cleared.' });
      } else {
        append({ cls: 'l-dim', t: 'system stable · no active glitch to clear.' });
      }
      return;
    }

    if (loginState === 'awaiting-user') {
      const username = lc;
      setLoginUser(username);
      if (username !== 'ryan') {
        setLoginState('idle');
        setLoginUser('');
        append([
          { cls: 'l-err', t: `invalid username: ${username}` },
          { cls: 'l-out', t: 'invalid credentials' },
        ]);
        return;
      }
      setLoginState('awaiting-password');
      append([
        { cls: 'l-out', t: `user ${username} accepted` },
        { cls: 'l-out', t: 'password:' },
      ]);
      return;
    }

    if (loginState === 'awaiting-password') {
      if (loginUser === 'ryan' && lc === 'newcomer') {
        setIsLoggedIn(true);
        setLoginState('idle');
        setLoginUser('');
        append([
          { cls: 'l-ok', t: 'authentication success.' },
          { cls: 'l-ok', t: 'welcome, R. Newcomer.' },
        ]);
      } else {
        setIsLoggedIn(false);
        setLoginState('idle');
        setLoginUser('');
        append([
          { cls: 'l-err', t: 'authentication denied.' },
          { cls: 'l-out', t: 'invalid credentials' },
        ]);
      }
      return;
    }

    const fn = COMMANDS[lc];
    if (fn) {
      fn(args);
      return;
    }
    append({ cls: 'l-err', t: `${cmd}: command not found · try \'help\'` });
  };

  const onKey = (e) => {
    if (e.key === 'Enter') {
      submit(input);
      setInput('');
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      if (!history.length) return;
      const nx = hIdx < 0 ? history.length - 1 : Math.max(0, hIdx - 1);
      setHIdx(nx);
      setInput(history[nx]);
    } else if (e.key === 'ArrowDown') {
      e.preventDefault();
      if (!history.length || hIdx < 0) return;
      const nx = hIdx + 1;
      if (nx >= history.length) {
        setHIdx(-1);
        setInput('');
      } else {
        setHIdx(nx);
        setInput(history[nx]);
      }
    } else if (e.key === 'Tab') {
      e.preventDefault();
      const prefix = input.toLowerCase();
      const opts = Object.keys(COMMANDS).filter((k) => k.startsWith(prefix));
      if (opts.length === 1) setInput(opts[0]);
      else if (opts.length > 1 && prefix) {
        append({ cls: 'l-dim', t: opts.join('   ') });
      }
    } else if (e.key === 'Escape') {
      onClose();
    } else if (e.key === 'l' && e.ctrlKey) {
      e.preventDefault();
      setLog([]);
    }
  };

  if (!open) return null;
  return (
    <div className="term-overlay">
      <div className={`term ${isGlitching ? 'term-cursor' : ''}`}>
        <div className="term-head">
          <div className="dots">
            <span className="d1" />
            <span className="d2" />
            <span className="d3" />
          </div>
          <div className="title">nc@local · /home/operator</div>
          <button className="x" onClick={onClose}>esc ✕</button>
        </div>
        <div className="term-body">
          <div className="term-log" ref={logRef}>
            {log.map((l, i) =>
              l.t.includes('\n')
                ? <pre key={i} className={l.cls}>{l.t}</pre>
                : <div key={i} className={l.cls}>{l.t}</div>
            )}
          </div>
          <div className="term-input-row">
            <span className="pm">nc<b>@</b>local <span style={{ color: 'var(--mute)' }}>~</span> $</span>
            <input
              ref={inputRef}
              className="term-input"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={onKey}
              placeholder="type a command · `help` to start"
              autoComplete="off"
              autoCorrect="off"
              spellCheck={false}
            />
          </div>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// LINKEDIN PREVIEW CARD
// ───────────────────────────────────────────────────────────────
function HeroBookmarks({ activePage, onNavigate }) {
  const tabs = [
    { id: 'home', label: 'home' },
    { id: 'articles', label: 'articles' },
    { id: 'contact', label: 'contact' },
  ];

  return (
    <nav className="hero-bookmarks" aria-label="page bookmarks">
      {tabs.map((tab) => (
        <button
          className={`hero-bookmark ${activePage === tab.id ? 'is-active' : ''}`}
          key={tab.id}
          onClick={() => onNavigate(tab.id)}
          type="button"
          aria-current={activePage === tab.id ? 'page' : undefined}
        >
          {tab.label}
        </button>
      ))}
    </nav>
  );
}

function LinkedInCard({ activePage, onNavigate }) {
  return (
    <div className="hero-card-shell" id="home">
      <HeroBookmarks activePage={activePage} onNavigate={onNavigate} />
      <a className="li-card" href={RESUME.linkedin} target="_blank" rel="noopener">
        <div className="li-cover" aria-hidden="true">
          {RESUME.linkedinCover ? <img className="li-cover-image" src={RESUME.linkedinCover} alt="cover" /> : null}
          <div className="li-cover-grid" />
        </div>
        <div className="li-avatar" aria-hidden="true">
          {RESUME.linkedinAvatar ? <img src={RESUME.linkedinAvatar} alt="R. Newcomer" /> : <span>RN</span>}
        </div>
        <div className="li-meta">
          <div className="li-name">{RESUME.name}
            <svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true"
                 style={{ marginLeft: 6, verticalAlign: '-1px' }}>
              <circle cx="6.5" cy="6.5" r="6" fill="currentColor" opacity="0.85"/>
              <path d="M3.5 6.5 L5.5 8.5 L9.5 4.5" stroke="#06060a" strokeWidth="1.4"
                    strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </div>
          <div className="li-headline">{RESUME.headline}</div>
          <div className="li-loc">
            B.S. Accounting · University of Southern Missisippi
            <span className="car" />
          </div>

          <div className="li-stats">
            <div className="li-stat">
              <span className="k">degree</span>
              <span className="v">{RESUME.role}</span>
            </div>
            <div className="li-stat">
              <span className="k">edu</span>
              <span className="v">{RESUME.education[0].title}</span>
            </div>
          </div>

          <div className="li-cta">
            <span className="li-cta-btn">View profile <span className="arr">↗</span></span>
            <span className="li-brand">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
                <path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5V8h3v11zM6.5 6.7a1.75 1.75 0 110-3.5 1.75 1.75 0 010 3.5zM19 19h-3v-5.6c0-1.4-.5-2.3-1.8-2.3-1 0-1.6.7-1.9 1.4-.1.2-.1.6-.1.9V19h-3V8h3v1.3a3 3 0 012.7-1.5c2 0 3.1 1.3 3.1 4V19z"/>
              </svg>
              in/rnewcomer
            </span>
          </div>
        </div>
      </a>
    </div>
  );
}

function ProjectsTui() {
  const [selected, setSelected] = useState(0);
  const project = RESUME.projects[selected];

  return (
    <div className="projects-tui">
      <div className="tui-sidebar" role="tablist" aria-label="projects">
        <div className="tui-prompt">projects://select</div>
        {RESUME.projects.map((item, i) => (
          <button
            className={`tui-menu-item ${i === selected ? 'is-selected' : ''}`}
            key={item.id}
            onClick={() => setSelected(i)}
            role="tab"
            aria-selected={i === selected}
          >
            <span className="tui-caret">{i === selected ? '>' : '$'}</span>
            <span>{item.title}</span>
            <span className="tui-state">{item.status}</span>
          </button>
        ))}
      </div>

      <div className="tui-detail" role="tabpanel">
        <div className="tui-window-bar">
          <span>{project.id}</span>
          <span>{project.status}</span>
        </div>
        <div className="tui-title">{project.title}</div>
        <p>{project.summary}</p>
        <div className="tui-stack">
          {project.stack.map((item) => <span key={item}>{item}</span>)}
        </div>
        <div className="tui-command-list">
          {project.commands.map((cmd) => (
            <div className="tui-command" key={cmd}>
              <span className="pm">nc@projects $</span>
              <span>{cmd}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function SkillsTicker() {
  const items = (() => {
    const maxLen = Math.max(RESUME.skills.stack.length, RESUME.skills.tools.length);
    const out = [];
    for (let i = 0; i < maxLen; i++) {
      if (RESUME.skills.stack[i]) out.push(RESUME.skills.stack[i]);
      if (RESUME.skills.tools[i]) out.push(RESUME.skills.tools[i]);
    }
    return out;
  })();
  const trackItems = [...Array(4)].flatMap(() => items);
  const trackRef = useRef(null);
  const rowRef = useRef(null);
  const rafRef = useRef(0);
  const cycleWidthRef = useRef(1);
  const offsetRef = useRef(0);
  const velocityRef = useRef(0);
  const draggingRef = useRef(false);
  const dragPointerRef = useRef({ x: 0, t: 0 });
  const [isDragging, setIsDragging] = useState(false);

  const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
  const BASE_SPEED = -0.16;
  const FRICTION = 0.985;
  const MAX_VELOCITY = 2.4;

  useEffect(() => {
    const syncWidth = () => {
      if (rowRef.current) {
        cycleWidthRef.current = Math.max(rowRef.current.getBoundingClientRect().width, 1);
      }
    };
    syncWidth();
    window.addEventListener('resize', syncWidth);

    const animate = () => {
      if (!draggingRef.current && trackRef.current) {
        const cycleWidth = cycleWidthRef.current;
        if (cycleWidth > 0) {
          offsetRef.current += BASE_SPEED + velocityRef.current;
          if (offsetRef.current <= -cycleWidth) offsetRef.current += cycleWidth;
          if (offsetRef.current > 0) offsetRef.current -= cycleWidth;

          if (Math.abs(velocityRef.current) > 0.01) velocityRef.current *= FRICTION;
          else velocityRef.current = 0;

          trackRef.current.style.transform = `translateX(${offsetRef.current}px)`;
        }
      }
      rafRef.current = requestAnimationFrame(animate);
    };
    rafRef.current = requestAnimationFrame(animate);

    return () => {
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      window.removeEventListener('resize', syncWidth);
    };
  }, []);

  const releaseSpin = (e) => {
    if (!draggingRef.current) return;
    draggingRef.current = false;
    setIsDragging(false);
    if (e?.pointerId && trackRef.current && trackRef.current.hasPointerCapture?.(e.pointerId)) {
      trackRef.current.releasePointerCapture(e.pointerId);
    }
  };

  const handlePointerDown = (e) => {
    if (e.button !== undefined && e.button !== 0) return;
    draggingRef.current = true;
    setIsDragging(true);
    dragPointerRef.current = { x: e.clientX, t: performance.now() };
    velocityRef.current = 0;
    if (trackRef.current && trackRef.current.setPointerCapture) {
      trackRef.current.setPointerCapture(e.pointerId);
    }
    e.preventDefault();
  };

  const handlePointerMove = (e) => {
    if (!draggingRef.current || !trackRef.current) return;
    const now = performance.now();
    const dx = e.clientX - dragPointerRef.current.x;
    const dt = Math.max(8, now - dragPointerRef.current.t);
    dragPointerRef.current = { x: e.clientX, t: now };
    offsetRef.current += dx;
    velocityRef.current = clamp((dx / dt) * 16, -MAX_VELOCITY, MAX_VELOCITY);
    trackRef.current.style.transform = `translateX(${offsetRef.current}px)`;
    e.preventDefault();
  };

  return (
      <div
        className={`skill-carousel ${isDragging ? 'is-dragging' : ''}`}
        aria-label="skills"
        role="application"
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={releaseSpin}
        onPointerCancel={releaseSpin}
        onPointerLeave={releaseSpin}
      >
      <div className="skill-carousel-track" ref={trackRef} aria-hidden="true">
        <div className="skill-track-row" ref={rowRef}>
          {trackItems.map((item, i) => (
            <span className="skill-pill" key={`track-a-${item}-${i}`}>
              {item}
            </span>
          ))}
        </div>
        <div className="skill-track-row" aria-hidden="true">
          {trackItems.map((item, i) => (
            <span className="skill-pill" key={`track-b-${item}-${i}`}>
              {item}
            </span>
          ))}
        </div>
      </div>
    </div>
  );
}

function HomePageContent() {
  return (
    <>
      <section className="section about-layout">
        <article className="about-main">
          <div className="sec-head">
            <h2>// about</h2>
            <span className="n">[01]</span>
            <span className="line" />
          </div>
          <div className="sec-body">
            <p>{RESUME.about[0]}</p>
            <p>{RESUME.about[1]}</p>
          </div>
        </article>

        <div className="about-side-stack">
          <article className="resume-panel">
            <div className="sec-head">
              <h2>// education</h2>
              <span className="n">[02]</span>
              <span className="line" />
            </div>
            <div className="sec-body">
              {RESUME.education.map((e, i) => (
                <div className="row" key={i}>
                  <div className="body">
                    <span className="title">{e.title} <em>· {e.org}</em></span>
                    <span className="meta">GPA {e.gpa}</span>
                    {Array.isArray(e.achievements) && e.achievements.length > 0 ? (
                      <span className="meta">{e.achievements.join('  ·  ')}</span>
                    ) : null}
                  </div>
                </div>
              ))}
            </div>
          </article>

          <article className="resume-panel">
            <div className="sec-head">
              <h2>// experience</h2>
              <span className="n">[03]</span>
              <span className="line" />
            </div>
            <div className="sec-body">
              {RESUME.experience.map((e, i) => (
                <div className="row" key={i}>
                  <div className="body">
                    <span className="title">{e.title} <em>· {e.org}</em></span>
                    {Array.isArray(e.tags) && e.tags.length ? (
                      <span className="meta">{e.tags.join('  ·  ')}</span>
                    ) : null}
                  </div>
                </div>
              ))}
            </div>
          </article>
        </div>
      </section>

      <section className="section projects-section" id="projects">
        <div className="sec-head">
          <h2>// projects</h2>
          <span className="n">[04]</span>
          <span className="line" />
        </div>
        <ProjectsTui />
      </section>
    </>
  );
}

function ArticlesPage({ onMissing }) {
  return (
    <section className="section page-view articles-page" id="articles">
      <div className="sec-head">
        <h2>// news + articles</h2>
        <span className="n">[01]</span>
        <span className="line" />
      </div>
      <div className="page-callout">
        <div className="hazard-tape" aria-hidden="true" />
        <span className="callout-kicker">work in progress</span>
        <h1>Work In Progress.</h1>
        <p>Articles are under construction. Drafts are staged here, but the full posts are not online yet.</p>
        <div className="construction-grid" aria-hidden="true">
          <span>STOP</span>
          <span>!</span>
          <span>WIP</span>
        </div>
      </div>
      <div className="article-list">
        {RESUME.articles.map((article) => (
          <article className="article-row" key={article.title}>
            <span className="article-status">{article.status}</span>
            <div className="article-copy">
              <h3>{article.title}</h3>
              <p>{article.summary}</p>
            </div>
            <button className="article-arrow" type="button" onClick={() => onMissing('article route unavailable')}>
              ↗
            </button>
          </article>
        ))}
      </div>
    </section>
  );
}

function ContactPage({ onMissing }) {
  return (
    <section className="section page-view contact-page" id="contact">
      <div className="sec-head">
        <h2>// contact</h2>
        <span className="n">[01]</span>
        <span className="line" />
      </div>

      <div className="contact-cta">
        <span className="callout-kicker">reach out</span>
        <h1>Building something in finance, accounting, fintech, or agentic operations?</h1>
        <p>Email is the fastest way to start. Send the problem, the context, and what you want working better.</p>
        <a className="contact-primary" href={`mailto:${RESUME.contact.email}`}>
          <span>email me</span>
          <strong>{RESUME.contact.email}</strong>
          <span className="arr">↗</span>
        </a>
      </div>

      <div className="social-grid">
        {RESUME.contact.socials.map((social) => (
          <button className="social-card" type="button" onClick={() => onMissing(`${social.label.toLowerCase()} route unavailable`)} key={social.label}>
            <span className="social-label">{social.label}</span>
            <span className="social-value">{social.value}</span>
            <span className="arr">↗</span>
          </button>
        ))}
      </div>
    </section>
  );
}

// ───────────────────────────────────────────────────────────────
// RESUME PAGE
// ───────────────────────────────────────────────────────────────
function Page({ onOpenTerm }) {
  const getInitialPage = () => {
    const hash = window.location.hash.replace('#', '');
    return ['articles', 'contact'].includes(hash) ? hash : 'home';
  };
  const [activePage, setActivePage] = useState(getInitialPage);
  const [glitchNotice, setGlitchNotice] = useState(null);
  const glitchNoticeTimer = useRef(0);

  useEffect(() => {
    const onLocationChange = () => setActivePage(getInitialPage());
    window.addEventListener('hashchange', onLocationChange);
    window.addEventListener('popstate', onLocationChange);
    return () => {
      window.removeEventListener('hashchange', onLocationChange);
      window.removeEventListener('popstate', onLocationChange);
    };
  }, []);

  const navigate = (page) => {
    setActivePage(page);
    window.history.pushState(null, '', page === 'home' ? window.location.pathname : `#${page}`);
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  const triggerMissing = (message) => {
    flashGlitch();
    document.body.classList.add('error-glitch-active');
    setGlitchNotice(`404 // ${message}`);
    if (glitchNoticeTimer.current) clearTimeout(glitchNoticeTimer.current);
    glitchNoticeTimer.current = setTimeout(() => {
      document.body.classList.remove('error-glitch-active');
      setGlitchNotice(null);
    }, 1400);
  };

  useEffect(() => {
    return () => {
      if (glitchNoticeTimer.current) clearTimeout(glitchNoticeTimer.current);
      document.body.classList.remove('error-glitch-active');
    };
  }, []);

  return (
    <>
      <div className="bar">
        <div className="bar-l">
          <span className="dot" />
          <span>NEWCOMER<b style={{ color: 'var(--mute)', letterSpacing: '0.18em' }}>.DEV</b></span>
          <span style={{ color: 'var(--dim)' }}>·</span>
          <span style={{ color: 'var(--cyan)' }}>online</span>
        </div>
        <div className="bar-r">
          <button
            className="k"
            onClick={onOpenTerm}
            style={{ background:'none', border:0, color:'var(--mute)', font:'inherit',
              letterSpacing:'0.22em', textTransform:'uppercase', cursor:'pointer' }}
          >
            terminal <kbd>`</kbd>
          </button>
        </div>
      </div>

      <main className="page" data-glitch-target>
        {glitchNotice ? <div className="glitch-404-alert" role="status">{glitchNotice}</div> : null}
        <section className="section hero-block">
          <div className="sec-body">
            <LinkedInCard activePage={activePage} onNavigate={navigate} />
            <SkillsTicker />
          </div>
        </section>

        {activePage === 'home' ? <HomePageContent /> : null}
        {activePage === 'articles' ? <ArticlesPage onMissing={triggerMissing} /> : null}
        {activePage === 'contact' ? <ContactPage onMissing={triggerMissing} /> : null}
      </main>

      <div className="foot">
        <span>R. Newcomer · {new Date().getFullYear()} · est. now</span>
      </div>
    </>
  );
}

// ───────────────────────────────────────────────────────────────
// APP
// ───────────────────────────────────────────────────────────────
function App() {
  const [open, setOpen] = useState(false);

  useEffect(() => {
    printDevtoolsArt();
    const onKey = (e) => {
      // backtick opens, escape closes (escape also handled inside terminal)
      if (e.key === '`' && !open) {
        // Don't capture when user is typing in an input/textarea
        const tag = (document.activeElement && document.activeElement.tagName) || '';
        if (tag === 'INPUT' || tag === 'TEXTAREA') return;
        e.preventDefault();
        setOpen(true);
      } else if (e.key === '`' && open) {
        e.preventDefault();
        setOpen(false);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open]);

  return (
    <>
      <NetworkBackground />
      <Page onOpenTerm={() => setOpen(true)} />
      <Terminal open={open} onClose={() => setOpen(false)} />
    </>
  );
}

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