const THEMES = {
  stark: {
    id: 'ND-02',
    name: 'STARK',
    bg: '#FFFFFF',
    surface: '#F0F0F0',
    surface2: '#E0E0E0',
    fg: '#000000',
    fgDim: '#1F1F1F',
    muted: '#7A7A7A',
    line: '#000000',
    lineDim: 'rgba(0,0,0,0.14)',
    accent: '#FF2C24',
    accentInk: '#FFFFFF',
    grid: 'rgba(0,0,0,0.04)',
    chipBg: '#FFFFFF',
  },
  dark: {
    id: 'LAB-03',
    name: 'TERMINAL',
    bg: '#0A0A0A',
    surface: '#161614',
    surface2: '#222220',
    fg: '#E8E6E0',
    fgDim: '#B8B5AE',
    muted: '#6E6A62',
    line: '#E8E6E0',
    lineDim: 'rgba(232,230,224,0.18)',
    accent: '#FFB000',
    accentInk: '#0A0A0A',
    grid: 'rgba(232,230,224,0.04)',
    chipBg: 'rgba(232,230,224,0.06)',
  },
};

// Returns a theme variant with a black background and light text,
// preserving the accent. Used for the top bar and profile band so they
// read as solid black panels in every palette.
function invertTheme(theme) {
  return {
    ...theme,
    bg: '#000000',
    surface: '#0E0E0E',
    surface2: '#181816',
    fg: '#F2F0EA',
    fgDim: '#C8C5BE',
    muted: '#7A766E',
    line: '#F2F0EA',
    lineDim: 'rgba(242,240,234,0.16)',
    accentInk: '#000000',
    grid: 'rgba(242,240,234,0.05)',
    chipBg: 'rgba(242,240,234,0.06)',
  };
}

const PROFILE_PARAS = [
  "Software Engineering degree-apprenticeship student with 3.5 years of professional automation experience at PwC. Within my role I deliver Alteryx-based data automation solutions for both internal teams and external clients in a consultancy setting. Combining this with knowledge of REST APIs and DevOps, I have also been tasked with projects to streamline processes within my team, including building the discovery-to-production pipeline for how my team receives work.",
  "Alongside this, in my personal time I build full-stack projects spanning local AI and agentic systems, containerised microservices and experiment with both cloud-based and self-hosted infrastructure. Through the skills I have gathered I am able to combine real commercial delivery with hands-on engineering across Python, JavaScript, FastAPI, Docker, Kubernetes and CI/CD, as well as being able to communicate the intricacies of design and code comfortably with both technical and non-technical stakeholders.",
  "I am an avid user of AI agents within development. Through prolonged usage across different projects I have developed my own context-engineering practices for working with them, including structured documentation systems focused on transparency, memory and cost-effective safe use.",
];

const SKILLS = [
  { code: 'LANG', label: 'Languages', items: ['Python', 'JavaScript', 'HTML/CSS'] },
  { code: 'FRMK', label: 'Frameworks', items: ['FastAPI', 'Node.js', 'Express', 'Electron', 'Alteryx', 'REST', 'SSE'] },
  { code: 'INFRA', label: 'DevOps / Infra', items: ['Docker', 'Kubernetes', 'Rancher', 'GitLab CI/CD', 'Azure DevOps', 'Cloudflare'] },
  { code: 'AI', label: 'AI / Local', items: ['Ollama', 'LangChain', 'Faster-Whisper', 'Local LLMs', 'TTS'] },
  { code: 'AGENT', label: 'Agents / Context', items: ['Agentic workflows', 'Tool calling', 'Context engineering', 'Prompt design'] },
  { code: 'TEST', label: 'Testing', items: ['Pytest', 'JUnit', 'Jest', 'xUnit', 'Postman'] },
];

const PROJECTS = [
  {
    port: '01',
    rev: 'REV.A',
    kind: 'FINAL YEAR PROJECT',
    title: 'Localised Agentic\nHome Assistant',
    tagline: 'A privacy-focused, offline-first AI home assistant combining speech input, local LLM reasoning, text-to-speech and Zigbee smart-home control.',
    bullets: [
      'Built a local assistant pipeline using Faster-Whisper for speech recognition, Ollama with LangChain for localised agentic LLM inference, Supertonic text-to-speech and Zigbee2MQTT for device control, designed to run voice interactions without requiring an internet connection.',
      'Wrote bespoke tooling for device interaction alongside internet-dependent tools such as weather, news and article scraping.',
      'Implemented a central Python orchestrator managing voice flow, tool calling, text-to-speech, device control, pairing mode, UI state and graceful degradation of internal services.',
      'Developed FastAPI routes and Server-Sent Event streams powering live interfaces for system state, devices, logs, settings and setup.',
      'Built an Electron desktop wrapper to launch and manage external dependency subprocesses and to monitor backend state, local services and user interface.',
      'Wrote packaging scripts to ship the app as an executable installer, with a pipeline that automated repackaging when changes were made.',
    ],
    flow: ['VOICE', 'STT', 'LLM', 'TOOLS', 'ZIGBEE'],
    stack: ['Python', 'FastAPI', 'Ollama', 'LangChain', 'Whisper', 'Electron', 'SSE'],
    year: '2025',
  },
  {
    port: '02',
    rev: 'REV.B',
    kind: 'CLOUD MICROSERVICES PROJECT',
    title: 'QPC IP\nChecker',
    tagline: 'A multi-language IP analysis platform built as containerised microservices with automated testing, deployment and monitoring.',
    bullets: [
      'Designed and built seven services across Python, Java, Node.js, C#, PHP and frontend JavaScript with consistent input validation, structured JSON responses and automated unit/API tests.',
      'Containerised services with Docker and deployed to Rancher/Kubernetes using GitLab Container Registry images.',
      'Built GitLab CI/CD pipelines covering tests, image builds, deployment and rollback image tagging.',
      'Implemented a configurable Node.js reverse proxy and Kubernetes ingress to route traffic across services.',
      'Developed a FastAPI monitoring backend and Chart.js dashboard surfacing Kubernetes metrics, service latency and synthetic test history.',
    ],
    flow: ['CLIENT', 'INGRESS', '×7 SVC', 'METRICS'],
    stack: ['Docker', 'Kubernetes', 'Rancher', 'GitLab CI', 'FastAPI', 'Chart.js'],
    year: '2024',
  },
  {
    port: '03',
    rev: 'REV.C',
    kind: 'PERSONAL INFRASTRUCTURE',
    title: 'Self-Hosted VPN with\nEdge Monitoring & Zero Trust Access',
    tagline: "Production WireGuard VPN running on a Raspberry Pi using Cloudflare's edge for monitoring and Zero Trust access. Built to provide reliable, censorship-resistant connectivity while travelling.",
    bullets: [
      'Configured WireGuard on Raspberry Pi with per-device client configs and wrote a management CLI script to handle peer config lifecycle and VPN restarts without losing access when travelling.',
      "Built a public status page as a Cloudflare Worker backed by Workers KV, ingesting a 60-second heartbeat from the Pi and flipping to OFFLINE when the heartbeat goes stale or the WireGuard interface is down, the heartbeat is hosted on Cloudflare's edge so failure modes remain observable even when the Pi or home internet is unreachable.",
      'Replaced public-facing SSH to the Pi with a Cloudflare Tunnel gated by Cloudflare Access, configured to trust Cloudflare Access CA certificates for emergency access to the Pi while travelling.',
      "Enforced resilience to ISP and IP changes by pointing client configs at a DNS hostname rather than a fixed IP, writing an automated check running on the Pi every five minutes that detects IP changes and patches Cloudflare's DNS record via API, so ISP migrations or IP rotation does not mean the need to redistribute client configs.",
    ],
    flow: ['DEVICE', 'WG · UDP', 'PI'],
    stack: ['WireGuard', 'Raspberry Pi', 'Cloudflare Workers', 'Cloudflare KV', 'Cloudflare Access', 'systemd', 'Bash'],
    year: '2025-26',
    url: 'https://vpn.prsz.uk',
    urlLabel: 'vpn.prsz.uk · heartbeat',
  },
];

const EXPERIENCE = [
  {
    port: '04',
    company: 'PwC',
    role: 'Automation Specialist',
    range: '2022 → CURRENT',
    duration: '3.5y · full-time',
    bullets: [
      'Built Alteryx automation solutions for internal teams and clients, reducing manual effort and improving repeatable business processes.',
      'Designed a work-intake pipeline integrating Microsoft Forms with the Azure DevOps API to automatically create and route tickets onto a kanban board, replacing a manual logging step and streamlining how the team received and tracked work.',
      'Attended initial discovery calls in a business analyst capacity to help clients better understand what solutions my team were able to develop and to find additional problems they were having, enabling our team to provide the best possible solution.',
      'Communicated clearly with technical and non-technical stakeholders, supporting users, iterating on solutions from feedback and discussing scope and technical feasibility of imagined solutions.',
      'Acted as a mentor to younger apprentices, guiding them in how to navigate PwC as a firm and, in the capacity of SME, evaluating their work and teaching best practices in how to use Alteryx as a tool.',
    ],
    tags: ['Alteryx', 'Azure DevOps', 'REST APIs', 'Business analysis', 'Mentoring'],
  },
  {
    port: '05',
    company: 'Earlier employment',
    role: 'B-Line Removals (current part-time), Greens Pizza, Orchard, Show Inc., XLN Small Business Provider',
    roleHighlight: 'B-Line Removals (current part-time)',
    roleCase: 'normal',
    range: '2018 → CURRENT',
    duration: '6y+ · part-time',
    bullets: [
      'Removals, hospitality, sales, events. Customer-facing, time-pressured, physically demanding, team-based. Where I learned to talk to people, work fast and not drop things.',
    ],
    tags: ['Communication', 'Customer service', 'Reliability'],
  },
];

const EDUCATION = [
  {
    port: '06',
    grade: 'Expected 1:1',
    title: 'BEng Software Engineering with Digital Technology Partnership',
    institution: "Queen's University Belfast",
    detail: 'Cloud Computing · Service-Oriented Programming · Data Structures & Algorithms · Secure Software Development · Network Security · SE & Systems Development.',
    range: '2022-26',
  },
  {
    port: '07',
    grade: 'Distinction',
    title: 'Access Diploma, Computing for the Economy',
    institution: 'Belfast Metropolitan College',
    detail: 'One-year access programme: computing fundamentals, maths for computing, Python. Leading into the QUB / PwC apprenticeship.',
    range: '2021-22',
  },
];

// crosshair / reg mark
function RegMark({ size = 14, color, style = {} }) {
  return (
    <svg width={size} height={size} viewBox="0 0 14 14" style={{ display: 'block', ...style }}>
      <line x1="7" y1="0" x2="7" y2="14" stroke={color} strokeWidth="0.75" />
      <line x1="0" y1="7" x2="14" y2="7" stroke={color} strokeWidth="0.75" />
      <circle cx="7" cy="7" r="2.5" fill="none" stroke={color} strokeWidth="0.75" />
    </svg>
  );
}

// Corner brackets around a frame
function CornerBrackets({ color, inset = 0 }) {
  const len = 16;
  const stroke = 1;
  const wrap = { position: 'absolute', width: len, height: len, pointerEvents: 'none' };
  const lineStyle = { position: 'absolute', background: color };
  return (
    <>
      <div style={{ ...wrap, top: inset, left: inset }}>
        <div style={{ ...lineStyle, top: 0, left: 0, width: len, height: stroke }} />
        <div style={{ ...lineStyle, top: 0, left: 0, width: stroke, height: len }} />
      </div>
      <div style={{ ...wrap, top: inset, right: inset }}>
        <div style={{ ...lineStyle, top: 0, right: 0, width: len, height: stroke }} />
        <div style={{ ...lineStyle, top: 0, right: 0, width: stroke, height: len }} />
      </div>
      <div style={{ ...wrap, bottom: inset, left: inset }}>
        <div style={{ ...lineStyle, bottom: 0, left: 0, width: len, height: stroke }} />
        <div style={{ ...lineStyle, bottom: 0, left: 0, width: stroke, height: len }} />
      </div>
      <div style={{ ...wrap, bottom: inset, right: inset }}>
        <div style={{ ...lineStyle, bottom: 0, right: 0, width: len, height: stroke }} />
        <div style={{ ...lineStyle, bottom: 0, right: 0, width: stroke, height: len }} />
      </div>
    </>
  );
}

// Blinking cursor
function Cursor({ color, w = 10, h = 18, style = {} }) {
  return (
    <span
      style={{
        display: 'inline-block',
        width: w,
        height: h,
        background: color,
        marginLeft: 4,
        verticalAlign: 'text-bottom',
        animation: 'pjr-blink 1s steps(2,end) infinite',
        ...style,
      }}
    />
  );
}

// Reading-mode flag
function ReadingFlag({ theme, visible }) {
  return (
    <div style={{
      display: 'flex',
      alignItems: 'stretch',
      border: `1px solid ${theme.fg}`,
      fontFamily: '"Geist Mono", ui-monospace, monospace',
      fontSize: 10.5,
      letterSpacing: '0.18em',
      textTransform: 'uppercase',
      opacity: visible ? 1 : 0,
      transform: visible ? 'translateY(0)' : 'translateY(-6px)',
      transition: 'opacity 220ms ease-out, transform 220ms ease-out',
      pointerEvents: 'none',
      whiteSpace: 'nowrap',
    }}>
      <span style={{
        display: 'flex', alignItems: 'center',
        padding: '5px 9px',
        background: theme.accent,
        color: theme.accentInk,
        borderRight: `1px solid ${theme.fg}`,
        fontWeight: 600,
      }}>
        <span style={{
          width: 6, height: 6, borderRadius: '50%',
          background: theme.accentInk,
          marginRight: 7,
          animation: 'pjr-blink 1s steps(2,end) infinite',
        }} />
        PAUSED
      </span>
      <span style={{
        padding: '5px 10px', alignSelf: 'center',
        background: theme.fg, color: theme.bg,
      }}>
        anim off · reading mode
      </span>
    </div>
  );
}

// Mouse-tracked grid w dim base + hot grid masked to a radial gradient at the cursor. 
// Position vars updated via setProperty so React never re-renders on mousemove.
function MouseGrid({theme, cell = 32, radius = 150, paused = false }) {
  const wrapRef = React.useRef(null);
  const pausedRef = React.useRef(paused);
  React.useEffect(() => { pausedRef.current = paused; }, [paused]);

  React.useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const parent = el.parentElement;
    if (!parent) return;
    let raf = 0;
    let nx = 0, ny = 0;
    const flush = () => {
      raf = 0;
      parent.style.setProperty('--mx', nx + 'px');
      parent.style.setProperty('--my', ny + 'px');
      parent.style.setProperty('--mo', '1');
    };
    const onMove = (e) => {
      const rect = parent.getBoundingClientRect();
      nx = e.clientX - rect.left;
      ny = e.clientY - rect.top;
      if (!raf) raf = requestAnimationFrame(flush);
    };
    const onLeave = () => {
      parent.style.setProperty('--mo', '0');
    };
    parent.addEventListener('mousemove', onMove);
    parent.addEventListener('mouseleave', onLeave);
    return () => {
      parent.removeEventListener('mousemove', onMove);
      parent.removeEventListener('mouseleave', onLeave);
      if (raf) cancelAnimationFrame(raf);
    };
  }, []);

  const mask = `radial-gradient(circle ${radius}px at var(--mx, -9999px) var(--my, -9999px), rgba(0,0,0,1) 0%, rgba(0,0,0,0.5) 55%, rgba(0,0,0,0) 100%)`;

  return (
    <div
      ref={wrapRef}
      aria-hidden="true"
      style={{
        position: 'absolute', inset: 0,
        pointerEvents: 'none',
        zIndex: 0,
        overflow: 'hidden',
      }}
    >
      {/* dim base grid */}
      <div style={{
        position: 'absolute', inset: 0,
        backgroundImage: `linear-gradient(${theme.grid} 1px, transparent 1px), linear-gradient(90deg, ${theme.grid} 1px, transparent 1px)`,
        backgroundSize: `${cell}px ${cell}px`,
      }} />
      {/* hot grid — brighter, masked to a circle around the cursor */}
      <div style={{
        position: 'absolute', inset: 0,
        backgroundImage: `linear-gradient(${theme.accent} 1px, transparent 1px), linear-gradient(90deg, ${theme.accent} 1px, transparent 1px)`,
        backgroundSize: `${cell}px ${cell}px`,
        WebkitMaskImage: mask,
        maskImage: mask,
        opacity: paused ? 0 : 'var(--mo, 0)',
        transition: 'opacity 220ms ease-out',
      }} />
    </div>
  );
}

// Viewport width hook — used for conditional mobile layouts
function useViewport() {
  const [w, setW] = React.useState(() => (typeof window !== 'undefined' ? window.innerWidth : 1280));
  React.useEffect(() => {
    const onResize = () => setW(window.innerWidth);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return w;
}

// Section heading
// reveal on scroll-in
function useInView(ref, threshold = 0.12) {
  const [inView, setInView] = React.useState(false);
  React.useEffect(() => {
    if (!ref.current) return;
    const check = () => {
      const el = ref.current;
      if (!el) return;
      const rect = el.getBoundingClientRect();
      const vpH = window.innerHeight || document.documentElement.clientHeight;
      const visibleY = Math.max(0, Math.min(rect.bottom, vpH) - Math.max(rect.top, 0));
      const ratio = rect.height > 0 ? visibleY / rect.height : 0;
      const isIn = ratio >= threshold;
      setInView((prev) => (prev === isIn ? prev : isIn));
    };
    let raf = 0;
    const onScroll = () => {
      if (!raf) raf = requestAnimationFrame(() => { raf = 0; check(); });
    };
    check();
    requestAnimationFrame(check);
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
      if (raf) cancelAnimationFrame(raf);
    };
  }, []);
  return inView;
}

// Engineering tick ruler
function TickRuler({ theme, ticks = 64 }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', height: 14, gap: 6 }}>
      <RegMark color={theme.muted} size={10} />
      <div style={{ flex: 1, display: 'flex', alignItems: 'center', height: '100%' }}>
        {Array.from({ length: ticks }).map((_, i) => (
          <div key={i} style={{
            flex: 1,
            display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
            height: '100%',
          }}>
            <div style={{
              width: 1,
              height: i % 5 === 0 ? 12 : (i % 5 === 0 ? 12 : 5),
              background: theme.muted,
              opacity: i % 5 === 0 ? 0.85 : 0.45,
            }} />
          </div>
        ))}
      </div>
      <RegMark color={theme.muted} size={10} />
    </div>
  );
}

// Section heading
function SectionTab({ no, title, theme, meta, accent = false }) {
  const ref = React.useRef(null);
  const inView = useInView(ref);
  const chars = title.split('');
  return (
    <div ref={ref} style={{ marginBottom: 6, flex: 1, minWidth: 0 }}>
      {/* Tick ruler reveal */}
      <div style={{
        clipPath: inView ? 'inset(0 0% 0 0)' : 'inset(0 100% 0 0)',
        WebkitClipPath: inView ? 'inset(0 0% 0 0)' : 'inset(0 100% 0 0)',
        transition: 'clip-path 950ms cubic-bezier(0.2,0.7,0.15,1), -webkit-clip-path 950ms cubic-bezier(0.2,0.7,0.15,1)',
        marginBottom: 22,
        marginRight: 32,
      }}>
        <TickRuler theme={theme} />
      </div>

      {/* Meta row: §NN on the left, optional meta on the right */}
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
        marginBottom: 10,
        opacity: inView ? 1 : 0,
        transform: inView ? 'translateY(0)' : 'translateY(8px)',
        transition: 'opacity 600ms 220ms ease-out, transform 600ms 220ms cubic-bezier(0.2,0.7,0.2,1)',
        fontFamily: '"Geist Mono", ui-monospace, monospace',
        fontSize: 12,
        letterSpacing: '0.2em',
        textTransform: 'uppercase',
        color: theme.muted,
      }}>
        <span><span style={{ color: theme.accent }}>§</span>{no} / SECTION</span>
        {meta && <span>{meta}</span>}
      </div>

      {/* Big title — each character lifts in, staggered */}
      <h2 style={{
        margin: 0,
        fontFamily: '"Geist", system-ui, sans-serif',
        fontSize: 'clamp(44px, 6vw, 66px)',
        lineHeight: 0.95,
        letterSpacing: '-0.04em',
        fontWeight: 600,
        color: theme.fg,
        overflow: 'hidden',
        paddingBottom: '0.12em',
        whiteSpace: 'nowrap',
      }}>
        {chars.map((ch, i) => (
          <span
            key={i}
            style={{
              display: 'inline-block',
              opacity: inView ? 1 : 0,
              transform: inView ? 'translateY(0)' : 'translateY(0.85em)',
              transition: `opacity 500ms ${300 + i * 32}ms ease-out, transform 700ms ${300 + i * 32}ms cubic-bezier(0.2,0.7,0.15,1)`,
              whiteSpace: ch === ' ' ? 'pre' : 'normal',
            }}
          >
            {ch}
          </span>
        ))}
        <span
          style={{
            display: 'inline-block',
            color: theme.accent,
            opacity: inView ? 1 : 0,
            transform: inView ? 'translateY(0)' : 'translateY(0.85em)',
            transition: `opacity 500ms ${300 + chars.length * 32}ms ease-out, transform 700ms ${300 + chars.length * 32}ms cubic-bezier(0.2,0.7,0.15,1)`,
          }}
        >.</span>
      </h2>
    </div>
  );
}

// Data row in a spec sheet
function SpecRow({ k, v, theme, last }) {
  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: '90px 1fr',
      padding: '10px 0',
      borderBottom: last ? 'none' : `1px solid ${theme.lineDim}`,
      fontFamily: '"Geist Mono", ui-monospace, monospace',
      fontSize: 12,
      letterSpacing: '0.04em',
    }}>
      <div style={{ color: theme.muted, textTransform: 'uppercase' }}>{k}</div>
      <div style={{ color: theme.fg }}>{v}</div>
    </div>
  );
}

// Stack chip
function Chip({ children, theme }) {
  return (
    <span style={{
      display: 'inline-block',
      padding: '4px 10px',
      border: `1px solid ${theme.lineDim}`,
      background: theme.chipBg,
      fontFamily: '"Geist Mono", ui-monospace, monospace',
      fontSize: 10.5,
      letterSpacing: '0.08em',
      color: theme.fg,
      whiteSpace: 'nowrap',
    }}>{children}</span>
  );
}

// I/O flow strip
function FlowStrip({ steps, theme }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 0 }}>
      {steps.map((s, i) => (
        <React.Fragment key={i}>
          <div style={{
            border: `1px solid ${theme.line}`,
            padding: '6px 10px',
            fontFamily: '"Geist Mono", ui-monospace, monospace',
            fontSize: 10.5,
            letterSpacing: '0.1em',
            color: theme.fg,
            background: theme.surface,
          }}>{s}</div>
          {i < steps.length - 1 && (
            <div style={{
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              padding: '0 6px',
              color: theme.muted,
              fontSize: 12,
            }}>→</div>
          )}
        </React.Fragment>
      ))}
    </div>
  );
}

// Big "PORT XX" badge
function PortBadge({ port, theme, accent = false }) {
  return (
    <div style={{
      display: 'flex',
      alignItems: 'baseline',
      gap: 6,
      fontFamily: '"Geist Mono", ui-monospace, monospace',
      lineHeight: 1,
    }}>
      <span style={{
        fontSize: 10,
        letterSpacing: '0.18em',
        color: theme.muted,
        textTransform: 'uppercase',
      }}>port</span>
      <span style={{
        fontSize: 64,
        fontWeight: 500,
        letterSpacing: '-0.04em',
        color: accent ? theme.accent : theme.fg,
        fontFamily: '"Geist", system-ui, sans-serif',
      }}>{port}</span>
    </div>
  );
}

// Dot-matrix style strip (just letterspaced mono for now, no actual LED dots)
function DotMatrix({ children, theme, color }) {
  return (
    <span style={{
      fontFamily: '"Geist Mono", ui-monospace, monospace',
      fontSize: 11,
      letterSpacing: '0.22em',
      color: color || theme.fg,
      fontWeight: 500,
    }}>{children}</span>
  );
}

// Mode selector
function ModeSelector({ themeKey, onChange, theme }) {
  const opts = [
    { key: 'stark', id: 'ND-02', dot: '#FF2C24', bg: '#FFFFFF' },
    { key: 'dark',  id: 'LAB-03', dot: '#FFB000', bg: '#0B0B0B' },
  ];
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'stretch',
      border: `1px solid ${theme.line}`,
      background: theme.bg,
    }}>
      <div style={{
        padding: '6px 12px',
        borderRight: `1px solid ${theme.line}`,
        background: theme.fg, color: theme.bg,
        fontFamily: '"Geist Mono", ui-monospace, monospace',
        fontSize: 10, letterSpacing: '0.22em',
        display: 'flex', alignItems: 'center',
      }}>MODE</div>
      {opts.map((o, i) => {
        const active = themeKey === o.key;
        return (
          <button
            key={o.key}
            onClick={() => onChange(o.key)}
            title={o.id}
            style={{
              appearance: 'none',
              cursor: 'pointer',
              border: 'none',
              borderRight: i < opts.length - 1 ? `1px solid ${theme.lineDim}` : 'none',
              padding: '6px 10px',
              background: active ? theme.fg : 'transparent',
              color: active ? theme.bg : theme.fg,
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 10, letterSpacing: '0.18em',
              display: 'inline-flex', alignItems: 'center', gap: 8,
              transition: 'background 120ms, color 120ms',
            }}
          >
            <span style={{
              width: 10, height: 10, borderRadius: '50%',
              background: o.bg,
              border: `1.5px solid ${o.dot}`,
              boxShadow: active ? `inset 0 0 0 2px ${o.dot}` : 'none',
              display: 'inline-block',
            }} />
            {o.id}
          </button>
        );
      })}
    </div>
  );
}

// Top status bar
function TopBar({ theme, themeKey, onTheme }) {
  const vw = useViewport();
  const isMobile = vw < 768;
  const [t, setT] = React.useState(0);
  React.useEffect(() => {
    const id = setInterval(() => setT((x) => x + 1), 1000);
    return () => clearInterval(id);
  }, []);
  const hh = String(Math.floor(t / 3600)).padStart(2, '0');
  const mm = String(Math.floor((t % 3600) / 60)).padStart(2, '0');
  const ss = String(t % 60).padStart(2, '0');
  return (
    <div style={{
      position: 'relative',
      zIndex: 2,
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'space-between',
      flexWrap: 'wrap',
      gap: isMobile ? 10 : 0,
      padding: isMobile ? '10px 14px' : '12px 32px',
      borderBottom: `1px solid ${theme.line}`,
      background: '#000000',
      fontFamily: '"Geist Mono", ui-monospace, monospace',
      fontSize: isMobile ? 10 : 11,
      letterSpacing: '0.16em',
      textTransform: 'uppercase',
      color: theme.fg,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? 10 : 14 }}>
        <RegMark color={theme.fg} />
        <span style={{ fontWeight: 600 }}>PJR / {theme.id}</span>
        {!isMobile && <span style={{ color: theme.muted }}>· portfolio · rev.a</span>}
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? 10 : 24, flexWrap: 'wrap' }}>
        {!isMobile && <span style={{ color: theme.muted }}>UPTIME</span>}
        {!isMobile && <span>T+{hh}:{mm}:{ss}</span>}
        <ModeSelector themeKey={themeKey} onChange={onTheme} theme={theme} />
        <span style={{
          display: 'inline-flex', alignItems: 'center', gap: 6,
          padding: '2px 10px',
          background: theme.accent,
          color: theme.accentInk,
          letterSpacing: '0.2em',
        }}>
          <span style={{ width: 6, height: 6, borderRadius: '50%', background: theme.accentInk, animation: 'pjr-blink 1.2s steps(2,end) infinite' }} />
          SYS·OK
        </span>
      </div>
    </div>
  );
}

// Hero
function Hero({ theme }) {
  const vw = useViewport();
  const isMobile = vw < 768;
  return (
    <section style={{ position: 'relative', padding: 'clamp(28px, 5vw, 40px) clamp(16px, 4vw, 32px)' }}>
      <div style={{
        position: 'relative',
        display: 'grid',
        gridTemplateColumns: isMobile ? '1fr' : '1fr 360px',
        gap: isMobile ? 28 : 56,
        alignItems: isMobile ? 'stretch' : 'end',
      }}>
        <div>
          <div style={{
            display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap',
            fontFamily: '"Geist Mono", ui-monospace, monospace',
            fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: theme.muted,
            marginBottom: isMobile ? 20 : 32,
          }}>
            <span style={{ color: theme.accent, fontWeight: 600 }}>● ONLINE</span>
            <span>·</span>
            <span>UNIT 001 / OPERATOR</span>
          </div>

          <h1 style={{
            margin: 0,
            fontFamily: '"Geist", system-ui, sans-serif',
            fontWeight: 600,
            fontSize: 'clamp(56px, 16vw, 132px)',
            lineHeight: 0.9,
            letterSpacing: '-0.045em',
            color: theme.fg,
          }}>
            Philip<br />
            James<br />
            Ross<span style={{ color: theme.accent }}>.</span>
          </h1>

          <div style={{ marginTop: 36, maxWidth: 540 }}>
            <div style={{
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 13, letterSpacing: '0.1em', textTransform: 'uppercase',
              color: theme.fgDim, marginBottom: 12,
            }}>
              Software Engineer<span style={{ color: theme.accent }}> ·</span> AI &amp; automation specialist<span style={{ color: theme.accent }}> ·</span> belfast
            </div>
            <div style={{
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 14, color: theme.fgDim, lineHeight: 1.6,
            }}>
              Degree-apprentice · PwC × Queen&apos;s University Belfast.
              <Cursor color={theme.accent} w={9} h={16} />
            </div>
          </div>
        </div>

        {/* Right: spec block + contact */}
        <div style={{
          border: `1px solid ${theme.line}`,
          background: theme.surface,
          padding: '20px 22px',
          position: 'relative',
        }}>
          <CornerBrackets color={theme.accent} inset={-4} />
          <div style={{
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
            paddingBottom: 12, borderBottom: `1px solid ${theme.lineDim}`,
            marginBottom: 10,
          }}>
            <DotMatrix theme={theme} color={theme.muted}>SPEC SHEET</DotMatrix>
            <DotMatrix theme={theme} color={theme.muted}>PJR-001</DotMatrix>
          </div>
          <SpecRow theme={theme} k="model" v="Philip J. Ross" />
          <SpecRow theme={theme} k="role" v="Software Engineer · AI/Automation" />
          <SpecRow theme={theme} k="firm" v="PwC · QUB partnership" />
          <SpecRow theme={theme} k="email" v={<a href="mailto:philip.ross@proton.me" style={{ color: theme.accent, textDecoration: 'none' }}>philip.ross@proton.me</a>} />
          <SpecRow theme={theme} k="loc" v="Belfast, UK" />
          <SpecRow theme={theme} k="github" v={<a href="https://github.com/Wxste-afk" target="_blank" rel="noopener" style={{ color: theme.fg, textDecoration: 'none', borderBottom: `1px solid ${theme.lineDim}` }}>github.com/Wxste-afk</a>} />
          <SpecRow theme={theme} k="status" v={<span><span style={{ color: theme.accent }}>●</span> avail. q3 26</span>} last />
        </div>
      </div>
    </section>
  );
}

// Profile section
function Profile({ theme, onReadingEnter, onReadingLeave }) {
  const vw = useViewport();
  const isMobile = vw < 768;
  const totalChars = PROFILE_PARAS.reduce((a, p) => a + p.length, 0);
  const [paused, setPaused] = React.useState(false);
  const onEnter = () => { setPaused(true); onReadingEnter && onReadingEnter(); };
  const onLeave = () => { setPaused(false); onReadingLeave && onReadingLeave(); };
  return (
    <section
      onMouseEnter={onEnter}
      onMouseLeave={onLeave}
      style={{ padding: 'clamp(28px, 5vw, 40px) clamp(16px, 4vw, 32px)' }}
    >
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 32, marginBottom: 14, flexWrap: 'wrap' }}>
        <SectionTab no="01" title="Profile" theme={theme} />
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 12, paddingTop: 10 }}>
          <DotMatrix theme={theme} color={theme.muted}>{`──── ${totalChars} chars ────`}</DotMatrix>
          <ReadingFlag theme={theme} visible={paused} />
        </div>
      </div>
      <div style={{
        display: 'grid',
        gridTemplateColumns: isMobile ? '1fr' : '120px 1fr 200px',
        gap: isMobile ? 16 : 32,
      }}>
        {!isMobile && (
          <div style={{
            fontFamily: '"Geist Mono", ui-monospace, monospace',
            fontSize: 11, letterSpacing: '0.16em', color: theme.muted, textTransform: 'uppercase',
          }}>
            $ whoami<br />
            <span style={{ color: theme.accent }}>›</span> pjr
          </div>
        )}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
          {PROFILE_PARAS.map((para, i) => (
            <p key={i} style={{
              margin: 0,
              fontFamily: '"Geist", system-ui, sans-serif',
              fontSize: isMobile ? 16 : 19,
              lineHeight: 1.5,
              letterSpacing: '-0.008em',
              color: theme.fg,
              textWrap: 'pretty',
            }}>{para}</p>
          ))}
        </div>
        {!isMobile && (
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8 }}>
            <RegMark color={theme.muted} size={20} />
            <DotMatrix theme={theme} color={theme.muted}>F-01 / A4</DotMatrix>
          </div>
        )}
      </div>
    </section>
  );
}

// Skills spec-sheet table
function Skills({ theme }) {
  const vw = useViewport();
  const isMobile = vw < 768;
  return (
    <section style={{ padding: 'clamp(28px, 5vw, 40px) clamp(16px, 4vw, 32px)' }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 14, gap: 16, flexWrap: 'wrap' }}>
        <SectionTab no="02" title="Stack" theme={theme} />
        <DotMatrix theme={theme} color={theme.muted}>{SKILLS.length} GROUPS · {SKILLS.reduce((a, g) => a + g.items.length, 0)} ITEMS</DotMatrix>
      </div>
      <div style={{ border: `1px solid ${theme.line}` }}>
        {SKILLS.map((g, i) => (
          <div key={g.code} style={{
            display: 'grid',
            gridTemplateColumns: isMobile ? '1fr' : '92px 180px 1fr',
            borderBottom: i === SKILLS.length - 1 ? 'none' : `1px solid ${theme.lineDim}`,
            background: i % 2 === 0 ? theme.bg : theme.surface,
          }}>
            <div style={{
              padding: isMobile ? '10px 14px 4px' : '14px 16px',
              borderRight: isMobile ? 'none' : `1px solid ${theme.lineDim}`,
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 10.5,
              letterSpacing: '0.18em',
              color: theme.accent,
              fontWeight: 600,
              display: 'flex',
              alignItems: 'center',
            }}>{g.code}</div>
            <div style={{
              padding: isMobile ? '0 14px 6px' : '14px 16px',
              borderRight: isMobile ? 'none' : `1px solid ${theme.lineDim}`,
              fontFamily: '"Geist", system-ui, sans-serif',
              fontSize: isMobile ? 16 : 18,
              fontWeight: 500,
              color: theme.fg,
              display: 'flex',
              alignItems: 'center',
            }}>{g.label}</div>
            <div style={{
              padding: isMobile ? '0 14px 12px' : '14px 16px',
              display: 'flex', gap: 6, flexWrap: 'wrap',
              alignItems: 'center',
            }}>
              {g.items.map((it) => <Chip key={it} theme={theme}>{it}</Chip>)}
            </div>
          </div>
        ))}
      </div>
    </section>
  );
}

// Project card
function ProjectCard({ p, theme, idx }) {
  const vw = useViewport();
  const isMobile = vw < 768;
  return (
    <div style={{
      position: 'relative',
      border: `1px solid ${theme.line}`,
      background: theme.surface,
      padding: isMobile ? '18px 16px 20px' : '22px 24px 24px',
      display: 'flex',
      flexDirection: 'column',
      gap: 18,
    }}>
      <CornerBrackets color={theme.accent} inset={-4} />

      {/* header strip */}
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: 12,
        borderBottom: `1px solid ${theme.lineDim}`,
        paddingBottom: 14,
      }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 16, flexWrap: 'wrap' }}>
          <PortBadge port={p.port} theme={theme} accent />
          <div style={{
            fontFamily: '"Geist Mono", ui-monospace, monospace',
            fontSize: 10.5, letterSpacing: '0.18em', color: theme.muted,
          }}>{p.kind}</div>
        </div>
        <div style={{
          display: 'flex', gap: 14, alignItems: 'center',
          fontFamily: '"Geist Mono", ui-monospace, monospace',
          fontSize: 10.5, letterSpacing: '0.16em', color: theme.muted,
        }}>
          <span>{p.rev}</span>
          <span style={{ color: theme.fg }}>{p.year}</span>
        </div>
      </div>

      {/* title + tagline */}
      <div>
        <h3 style={{
          margin: 0,
          fontFamily: '"Geist", system-ui, sans-serif',
          fontSize: 'clamp(28px, 6vw, 44px)',
          lineHeight: 0.95,
          letterSpacing: '-0.035em',
          fontWeight: 600,
          color: theme.fg,
          whiteSpace: 'pre-line',
        }}>{p.title}</h3>
        <div style={{
          marginTop: 14,
          fontFamily: '"Geist Mono", ui-monospace, monospace',
          fontSize: 12.5, letterSpacing: '0.04em',
          color: theme.fgDim,
        }}>{p.tagline}</div>
      </div>

      {/* flow */}
      <FlowStrip steps={p.flow} theme={theme} />

      {/* bullets */}
      <ul style={{
        margin: 0, padding: 0, listStyle: 'none',
        display: 'flex', flexDirection: 'column', gap: 10,
      }}>
        {p.bullets.map((b, i) => (
          <li key={i} style={{
            display: 'grid',
            gridTemplateColumns: '18px 1fr',
            gap: 8,
            fontFamily: '"Geist", system-ui, sans-serif',
            fontSize: 14.5,
            lineHeight: 1.55,
            color: theme.fgDim,
            textWrap: 'pretty',
          }}>
            <span style={{
              color: theme.accent,
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 11,
              letterSpacing: '0.1em',
              paddingTop: 4,
            }}>{String(i + 1).padStart(2, '0')}</span>
            <span>{b}</span>
          </li>
        ))}
      </ul>

      {/* stack + optional link */}
      <div style={{
        marginTop: 'auto',
        paddingTop: 14,
        borderTop: `1px dashed ${theme.lineDim}`,
        display: 'flex', flexWrap: 'wrap', gap: 10,
        alignItems: 'center', justifyContent: 'space-between',
      }}>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          {p.stack.map((s) => <Chip key={s} theme={theme}>{s}</Chip>)}
        </div>
        {p.url && (
          <a href={p.url} target="_blank" rel="noopener" style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            fontFamily: '"Geist Mono", ui-monospace, monospace',
            fontSize: 11,
            letterSpacing: '0.14em',
            textTransform: 'uppercase',
            color: theme.fg,
            textDecoration: 'none',
            padding: '4px 8px',
            border: `1px solid ${theme.line}`,
            background: theme.accent,
          }}>
            <span style={{ color: theme.accentInk }}>↗</span>
            <span style={{ color: theme.accentInk }}>{p.urlLabel || p.url.replace(/^https?:\/\//, '')}</span>
          </a>
        )}
      </div>
    </div>
  );
}

function Projects({ theme }) {
  const vw = useViewport();
  const isMobile = vw < 768;
  return (
    <section style={{ padding: 'clamp(28px, 5vw, 40px) clamp(16px, 4vw, 32px)' }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 14, gap: 16, flexWrap: 'wrap' }}>
        <SectionTab no="03" title="Projects" theme={theme} accent />
        <DotMatrix theme={theme} color={theme.muted}>{PROJECTS.length} UNITS · CAT.A</DotMatrix>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 16 }}>
        {PROJECTS.slice(0, 2).map((p, i) => <ProjectCard key={p.port} p={p} theme={theme} idx={i} />)}
      </div>
      <div style={{ marginTop: 16 }}>
        <ProjectCard p={PROJECTS[2]} theme={theme} idx={2} />
      </div>
    </section>
  );
}

// Experience card
function ExpCard({ e, theme }) {
  const vw = useViewport();
  const isMobile = vw < 768;
  return (
    <div style={{
      position: 'relative',
      border: `1px solid ${theme.line}`,
      background: theme.surface,
      padding: isMobile ? '18px 16px' : '22px 24px 22px',
      display: 'flex',
      flexDirection: 'column',
      gap: 16,
    }}>
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: 12,
        borderBottom: `1px solid ${theme.lineDim}`,
        paddingBottom: 12,
      }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 16, flexWrap: 'wrap' }}>
          <PortBadge port={e.port} theme={theme} accent />
          <div>
            <div style={{
              fontFamily: '"Geist", system-ui, sans-serif',
              fontSize: 26, fontWeight: 600, letterSpacing: '-0.02em',
              color: theme.fg, lineHeight: 1,
            }}>{e.company}</div>
            <div style={{
              marginTop: 6,
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 11.5,
              letterSpacing: e.roleCase === 'normal' ? '0.04em' : '0.14em',
              color: theme.muted,
              textTransform: e.roleCase === 'normal' ? 'none' : 'uppercase',
              lineHeight: 1.4,
              maxWidth: 640,
            }}>{e.roleHighlight && e.role.indexOf(e.roleHighlight) >= 0 ? (() => {
              const idx = e.role.indexOf(e.roleHighlight);
              return (
                <React.Fragment>
                  {e.role.slice(0, idx)}
                  <span style={{ color: theme.fg, fontWeight: 600 }}>{e.roleHighlight}</span>
                  {e.role.slice(idx + e.roleHighlight.length)}
                </React.Fragment>
              );
            })() : e.role}</div>
          </div>
        </div>
        <div style={{ textAlign: 'right' }}>
          <div style={{
            fontFamily: '"Geist Mono", ui-monospace, monospace',
            fontSize: 11, letterSpacing: '0.16em', color: theme.fg,
          }}>{e.range}</div>
          <div style={{
            marginTop: 4,
            fontFamily: '"Geist Mono", ui-monospace, monospace',
            fontSize: 11, letterSpacing: '0.16em', color: theme.accent,
          }}>{e.duration}</div>
        </div>
      </div>
      <ul style={{
        margin: 0, padding: 0, listStyle: 'none',
        display: 'flex', flexDirection: 'column', gap: 10,
      }}>
        {e.bullets.map((b, i) => (
          <li key={i} style={{
            display: 'grid',
            gridTemplateColumns: '18px 1fr',
            gap: 8,
            fontFamily: '"Geist", system-ui, sans-serif',
            fontSize: 14.5,
            lineHeight: 1.55,
            color: theme.fgDim,
            textWrap: 'pretty',
          }}>
            <span style={{
              color: theme.accent,
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 11,
              letterSpacing: '0.1em',
              paddingTop: 4,
            }}>{String(i + 1).padStart(2, '0')}</span>
            <span>{b}</span>
          </li>
        ))}
      </ul>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
        {e.tags.map((t) => <Chip key={t} theme={theme}>{t}</Chip>)}
      </div>
    </div>
  );
}

function Experience({ theme }) {
  return (
    <section style={{ padding: 'clamp(28px, 5vw, 40px) clamp(16px, 4vw, 32px)' }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 14, gap: 16, flexWrap: 'wrap' }}>
        <SectionTab no="04" title="Experience" theme={theme} />
        <DotMatrix theme={theme} color={theme.muted}>{EXPERIENCE.length} UNITS · 6Y SPAN</DotMatrix>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
        {EXPERIENCE.map((e) => <ExpCard key={e.port} e={e} theme={theme} />)}
      </div>
    </section>
  );
}

// Education card
function EduCard({ e, theme }) {
  return (
    <div style={{
      border: `1px solid ${theme.line}`,
      background: theme.surface,
      padding: '22px 24px',
      display: 'flex', flexDirection: 'column', gap: 14,
    }}>
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
        borderBottom: `1px solid ${theme.lineDim}`,
        paddingBottom: 12,
      }}>
        <PortBadge port={e.port} theme={theme} accent />
        <div style={{
          padding: '4px 10px',
          background: theme.accent, color: theme.accentInk,
          fontFamily: '"Geist Mono", ui-monospace, monospace',
          fontSize: 10.5, letterSpacing: '0.16em', textTransform: 'uppercase',
        }}>{e.grade}</div>
      </div>
      <div>
        <div style={{
          fontFamily: '"Geist Mono", ui-monospace, monospace',
          fontSize: 11, letterSpacing: '0.16em', color: theme.muted,
          textTransform: 'uppercase', marginBottom: 6,
        }}>{e.range} · {e.institution}</div>
        <h4 style={{
          margin: 0,
          fontFamily: '"Geist", system-ui, sans-serif',
          fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', lineHeight: 1.1,
          color: theme.fg,
        }}>{e.title}</h4>
      </div>
      <p style={{
        margin: 0,
        fontFamily: '"Geist", system-ui, sans-serif',
        fontSize: 13.5, lineHeight: 1.55, color: theme.fgDim,
        textWrap: 'pretty',
      }}>{e.detail}</p>
    </div>
  );
}

function Education({ theme }) {
  const vw = useViewport();
  const isMobile = vw < 768;
  return (
    <section style={{ padding: 'clamp(28px, 5vw, 40px) clamp(16px, 4vw, 32px)' }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 14, gap: 16, flexWrap: 'wrap' }}>
        <SectionTab no="05" title="Education" theme={theme} />
        <DotMatrix theme={theme} color={theme.muted}>2021 / 2026</DotMatrix>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 16 }}>
        {EDUCATION.map((e) => <EduCard key={e.port} e={e} theme={theme} />)}
      </div>
    </section>
  );
}

// Contact / footer
function Footer({ theme }) {
  return (
    <section style={{ padding: 'clamp(36px, 6vw, 54px) clamp(16px, 4vw, 32px) clamp(28px, 5vw, 40px)', position: 'relative' }}>
      <div style={{ position: 'relative' }}>
        <div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'baseline', marginBottom: 24 }}>
          <DotMatrix theme={theme} color={theme.muted}>END OF DOC</DotMatrix>
        </div>

        <h2 style={{
          margin: 0,
          fontFamily: '"Geist", system-ui, sans-serif',
          fontSize: 'clamp(44px, 11vw, 88px)', fontWeight: 600, letterSpacing: '-0.04em', lineHeight: 0.95,
          color: theme.fg,
        }}>
          Let&apos;s build<br />
          something<span style={{ color: theme.accent }}>.</span>
          <Cursor color={theme.accent} w={10} h={18} />
        </h2>

        <div style={{ marginTop: 36, display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(200px, 100%), 1fr))', gap: 24 }}>
          <div>
            <div style={{
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 10.5, letterSpacing: '0.2em', color: theme.muted, textTransform: 'uppercase', marginBottom: 6,
            }}>email</div>
            <a href="mailto:philip.ross@proton.me" style={{
              fontFamily: '"Geist", system-ui, sans-serif',
              fontSize: 20, fontWeight: 500, color: theme.fg, textDecoration: 'none',
              borderBottom: `2px solid ${theme.accent}`,
            }}>philip.ross@proton.me</a>
          </div>
          <div>
            <div style={{
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 10.5, letterSpacing: '0.2em', color: theme.muted, textTransform: 'uppercase', marginBottom: 6,
            }}>location</div>
            <div style={{
              fontFamily: '"Geist", system-ui, sans-serif',
              fontSize: 20, fontWeight: 500, color: theme.fg,
            }}>Belfast, UK</div>
          </div>
          <div>
            <div style={{
              fontFamily: '"Geist Mono", ui-monospace, monospace',
              fontSize: 10.5, letterSpacing: '0.2em', color: theme.muted, textTransform: 'uppercase', marginBottom: 6,
            }}>elsewhere</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontFamily: '"Geist", system-ui, sans-serif', fontSize: 17, fontWeight: 500 }}>
              <a href="https://github.com/Wxste-afk" target="_blank" rel="noopener" style={{ color: theme.fg, textDecoration: 'none' }}>↗ github</a>
              <a href="https://www.linkedin.com/in/phil-ross-0b277b318/" target="_blank" rel="noopener" style={{ color: theme.fg, textDecoration: 'none' }}>↗ linkedin</a>
            </div>
          </div>
        </div>

        <div style={{
          marginTop: 56,
          paddingTop: 18,
          borderTop: `1px solid ${theme.line}`,
          display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12,
          fontFamily: '"Geist Mono", ui-monospace, monospace',
          fontSize: 10.5, letterSpacing: '0.18em', color: theme.muted, textTransform: 'uppercase',
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
            <RegMark color={theme.muted} />
            <span>© philip j. ross · {new Date().getFullYear()}</span>
          </div>
          <div style={{ display: 'flex', gap: 18, flexWrap: 'wrap' }}>
            <span>FILE / index.html</span>
            <span>BUILD / {theme.id}</span>
            <span style={{ color: theme.accent }}>● READY</span>
          </div>
        </div>
      </div>
    </section>
  );
}

// Main Portfolio
function Portfolio({ themeKey: themeKeyProp, controlled = false, onThemeChange }) {
  const norm = (k) => (k === 'stark' || k === 'dark') ? k : 'stark';
  const [internalKey, setInternalKey] = React.useState(() => {
    if (controlled) return norm(themeKeyProp);
    try { return norm(localStorage.getItem('pjr.theme') || themeKeyProp); } catch (_) { return norm(themeKeyProp); }
  });
  const themeKey = controlled ? norm(themeKeyProp) : internalKey;
  const setThemeKey = (k) => {
    const nk = norm(k);
    if (controlled) { onThemeChange && onThemeChange(nk); return; }
    setInternalKey(nk);
    try { localStorage.setItem('pjr.theme', nk); } catch (_) {}
  };
  const theme = THEMES[themeKey];
  const [readingMode, setReadingMode] = React.useState(false);
  return (
    <div style={{
      width: '100%',
      background: theme.bg,
      color: theme.fg,
      fontFamily: '"Geist", system-ui, sans-serif',
      position: 'relative',
      transition: 'background 200ms, color 200ms',
    }}>
      <TopBar theme={invertTheme(theme)} themeKey={themeKey} onTheme={setThemeKey} />
      <MouseGrid theme={theme} paused={readingMode} />
      <div style={{ position: 'relative', zIndex: 1 }}>
        <Hero theme={theme} />
        <Profile
          theme={theme}
          onReadingEnter={() => setReadingMode(true)}
          onReadingLeave={() => setReadingMode(false)}
        />
        <Skills theme={theme} />
        <Projects theme={theme} />
        <Experience theme={theme} />
        <Education theme={theme} />
        <Footer theme={theme} />
      </div>
    </div>
  );
}

Object.assign(window, { Portfolio, THEMES });
