Colophon

How this site is built.

A short tour of the stack, the design decisions, and the AI bits behind the scenes. The whole thing is open-source — fork, copy, learn from it, ship your own.

The stack

Jekyll 3.10Static site generator (pinned by github-pages gem)
Pure CSSCustom properties, grid, clamp(). No Tailwind, no PostCSS.
Vanilla JavaScriptNo framework, no bundler. ES2017+.
GitHub PagesHosting, free, builds on push.
Cloudflare WorkersChatbot RAG backend at the edge.
TensorFlow.jsIn-browser sentiment model in the Live Lab.

Design decisions

Dark by default, no apologies

The site ships dark. The light theme exists for users who prefer it but the design is built for the dark palette first. prefers-color-scheme is intentionally ignored on first load — only an explicit click switches.

Tokens all the way down

Every colour, font, and easing curve is a CSS custom property in :root. The accent picker swaps eight tokens; the font picker swaps three. Light theme is a single [data-theme="light"] override on the same tokens — no rule duplication.

One CSS file, one JS file

No bundler, no code-splitting. The CSS is hand-written and lives in assets/css/main.css. The JS is split into a few focused files (main.js, theme.js, accent.js, fonts.js, chatbot.js, terminal.js, spotlight.js, card-tilt.js, agent-visualiser.js, ml-demo.js, project-filter.js) — all loaded with defer, none required for first paint.

No flash of anything

Critical CSS (tokens + nav + hero shell) is inlined in the head. The full stylesheet loads non-blocking with media="print" + onload swap. A small inline script reads the saved theme, accent key, and font key from localStorage and applies them before first paint, so you never see the default-then-swap flash.

The AI features

Talk to Paul (chatbot)

A retrieval-augmented chatbot. The build script (scripts/build-knowledge-base.js) chunks _data/*.yml + blog posts into ~32 semantic chunks, each optionally embedded with @cf/baai/bge-base-en-v1.5. On every message: sanitise → rate-limit per IP → check for prompt-injection patterns → classify intent → retrieve the top-5 relevant chunks (cosine similarity if embeddings are present, BM25-lite keyword overlap otherwise) → build a focused prompt with only those chunks → call the LLM (Llama 3.1 8B via Workers AI by default, or Claude Haiku 4.5 if configured). The "How I found this answer" panel under each reply exposes the retrieval — same pattern Perplexity uses.

Live Lab (TF.js sentiment)

A real CNN running in your browser. The sentiment_cnn_v1 model from the TensorFlow.js model zoo, lazy-loaded only when the Live Lab section enters the viewport. Tokenises with the model's metadata vocab, runs inference on WebGL, reports timing in milliseconds. Total cold payload ~1.8 MB, cached after first visit. No data leaves your device.

Agent visualiser

A hand-rolled SVG flow showing how a research agent decomposes a task — Input → Planning → Tool Selection → Execution → Memory → Synthesis → Output. Auto-plays on viewport entry, pauses when scrolled out, supports play/pause/step/reset and 1×/2× speed.

Terminal

A CLI-style portfolio explorer. Type paul --help to start. Pure frontend — all data comes from _data/*.yml serialised into a JSON blob at build time. Up/Down history, Tab completion, Ctrl-L to clear, and a matrix easter egg.

Performance & privacy

Open source

Source: github.com/mcneillium/mcneillium.github.io. The chatbot worker code is in workers/portfolio-bot/. Deployment guide: docs/chatbot-deployment.md.