/* global React, PROJECTS, STACK, TIMELINE, NOW_LIST, MARQUEE, PROFILE */ const { useEffect, useRef, useState } = React; /* ─── visuals (per-card) ───────────────────────────── */ function VisFlow() { return ( {[40, 80, 120, 160, 200].map((y, i) => ( ))} {[ [60, 70], [180, 110], [320, 80], [240, 170], [110, 190] ].map(([x, y], i) => ( ))} ); } function VisBars() { const heights = [40, 65, 35, 80, 55, 90, 45, 70, 30, 85, 50, 75]; return (
{heights.map((h, i) => ( ))}
); } function CardVis({ kind }) { if (kind === 'flow') return
; if (kind === 'bars') return ; if (kind === 'grid') return
; if (kind === 'rings') return
; return (
); } /* ─── reveal observer ──────────────────────────────── */ function useReveal() { useEffect(() => { const els = document.querySelectorAll('.reveal'); const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { threshold: 0.12 }); els.forEach(el => io.observe(el)); return () => io.disconnect(); }, []); } /* ─── card with cursor spotlight ───────────────────── */ function Card({ p }) { const ref = useRef(null); const onMove = (e) => { const r = ref.current.getBoundingClientRect(); ref.current.style.setProperty('--mx', (e.clientX - r.left) + 'px'); ref.current.style.setProperty('--my', (e.clientY - r.top) + 'px'); }; return (
{p.num} · {p.cat} {p.year} · {p.metric.v} {p.metric.l}

{p.title}

{p.desc}

{p.tags.map(t => {t})}
Case study
); } /* ─── nav ──────────────────────────────────────────── */ function Nav() { return ( ); } /* ─── status bar ───────────────────────────────────── */ function StatusBar() { const [t, setT] = useState(() => fmt()); useEffect(() => { const id = setInterval(() => setT(fmt()), 60_000); return () => clearInterval(id); }, []); function fmt() { const d = new Date(); const opts = { timeZone: PROFILE.status.timezone, hour: '2-digit', minute: '2-digit', hour12: false }; return new Intl.DateTimeFormat('en-GB', opts).format(d) + ' ' + PROFILE.status.tzLabel; } return (
{PROFILE.status.availability}
{PROFILE.status.version} / {PROFILE.status.city} · {t} / {PROFILE.status.build}
); } /* ─── hero ─────────────────────────────────────────── */ function Hero() { return (
{PROFILE.hero.badge}

{PROFILE.hero.h1[0]}
{PROFILE.hero.h1[1]} {PROFILE.hero.h1[2]}
{PROFILE.hero.h1[3]}

{PROFILE.hero.lede}

); } /* ─── marquee ──────────────────────────────────────── */ function Marquee() { const items = [...MARQUEE, ...MARQUEE]; return (
{items.map((m, i) => ( {m} ))}
); } /* ─── work ─────────────────────────────────────────── */ function Work() { return (
/ 001 · selected work

{PROFILE.sections.work[0]}{PROFILE.sections.work[1]}{PROFILE.sections.work[2]}

{PROJECTS.map(p => )}
); } /* ─── stack ────────────────────────────────────────── */ function Stack() { const groups = STACK.reduce((acc, s) => { (acc[s.cat] ||= []).push(s); return acc; }, {}); return (
/ 002 · stack

{PROFILE.sections.stack[0]}{PROFILE.sections.stack[1]}{PROFILE.sections.stack[2]}

{Object.entries(groups).map(([cat, items]) => (
— {cat}
{items.map(s => (
{s.name} {s.yrs}
{s.cat}
))}
))}
); } /* ─── timeline ─────────────────────────────────────── */ function Timeline() { return (
/ 003 · trajectory

{PROFILE.sections.timeline[0]}{PROFILE.sections.timeline[1]}{PROFILE.sections.timeline[2]}

{TIMELINE.map((t, i) => (
{t.yr}
{t.role}
{t.co}
{t.what}
))}
); } /* ─── about ────────────────────────────────────────── */ function About() { return (
/ 004 · about

{PROFILE.sections.about[0]}{PROFILE.sections.about[1]}{PROFILE.sections.about[2]}

{PROFILE.about.bio.map((para, i) =>

{para}

)}
); } /* ─── footer / contact ─────────────────────────────── */ function Footer() { return ( ); } /* ─── tweaks panel ─────────────────────────────────── */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accentMode": "moonlight", "displayFont": "geist", "italicFont": "instrument", "grain": true, "marquee": true, "density": "comfortable" }/*EDITMODE-END*/; function ApplyTweaks({ tweaks }) { useEffect(() => { const root = document.documentElement; const map = { moonlight: { glow: 'rgba(255,255,255,0.55)', tint: '255,255,255' }, arctic: { glow: 'rgba(200,225,255,0.55)', tint: '210,225,245' }, ember: { glow: 'rgba(255,225,200,0.50)', tint: '245,225,205' }, }; const c = map[tweaks.accentMode] || map.moonlight; root.style.setProperty('--glow', c.glow); root.style.setProperty('--tint', c.tint); const sans = { geist: "'Geist',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif", inter: "'Inter',-apple-system,sans-serif", neue: "'Neue Haas Grotesk Display Pro','Helvetica Neue',Helvetica,Arial,sans-serif", }[tweaks.displayFont] || "'Geist',sans-serif"; root.style.setProperty('--sans', sans); const it = { instrument: "'Instrument Serif',serif", cormorant: "'Cormorant Garamond',serif", none: "'Geist',sans-serif", }[tweaks.italicFont] || "'Instrument Serif',serif"; document.querySelectorAll('.it').forEach(el => { el.style.fontFamily = it; }); document.querySelector('.grain').style.display = tweaks.grain ? 'block' : 'none'; document.querySelector('.marquee').style.display = tweaks.marquee ? 'block' : 'none'; const pad = tweaks.density === 'compact' ? '60px' : tweaks.density === 'spacious' ? '160px' : '110px'; document.querySelectorAll('section.block').forEach(el => { el.style.paddingTop = pad; el.style.paddingBottom = pad; }); }, [tweaks]); return null; } function Tweaks() { const [tweaks, setTweak] = window.useTweaks(TWEAK_DEFAULTS); const T = window.TweaksPanel, S = window.TweakSection, R = window.TweakRadio, B = window.TweakToggle, Sel = window.TweakSelect; return ( <> setTweak('accentMode', v)} options={[{value:'moonlight',label:'Moonlight'},{value:'arctic',label:'Arctic'},{value:'ember',label:'Ember'}]} /> setTweak('displayFont', v)} options={[{value:'geist',label:'Geist (default)'},{value:'inter',label:'Inter'},{value:'neue',label:'Neue Haas / Helvetica'}]} /> setTweak('italicFont', v)} options={[{value:'instrument',label:'Instrument Serif'},{value:'cormorant',label:'Cormorant Garamond'},{value:'none',label:'No italic accent'}]} /> setTweak('grain', v)} /> setTweak('marquee', v)} /> setTweak('density', v)} options={[{value:'compact',label:'Compact'},{value:'comfortable',label:'Default'},{value:'spacious',label:'Spacious'}]} /> ); } /* ─── app ──────────────────────────────────────────── */ function App() { useReveal(); return ( <>