How this site is built

My portfolio coded from scratch exactly to my liking.

Hand-written HTML and CSS. Every accessibility feature described below is running on the page you are reading right now.

Source
Flat HTML, one stylesheet, one script
Dependencies
None
Hosting
GitHub Pages, static
JavaScript
~120 lines, progressive
Started
2025, still maintained

The stack, such as it is

Every page is a flat .html file at the repository root. They share one styles.css and one main.js, loaded with defer. There is no bundler, no template engine, and no generated output. What is in the repository is what the browser receives.

I made this choice deliberately. I have used static site generators and frameworks in the past, and I always feel held back. The simplicity of the stack allows for maximum control and flexibility, along with a reduced risk of dependency-related issues. Especially now with the help of modern large language model (LLM) coding capabilities, I can quickly make broad changes across the site and go back on my own to tweak the details. I would only being to suffer with the architecture of the website when the pages start to multiply, see Limits.

Accessibility, and why each piece is there

The theming system

Colour lives entirely in CSS custom properties. There are two independent axes, and keeping them independent is the whole idea:

Light mode needed more than an inverted background, and it took the longest of anything here to get right. Neon on white is unreadable, since the accents sit at roughly 1.3:1 against it, so so every accent is re-chosen for light mode rather than lightened. The site green becomes a forest green, the cyan a deep teal, the amber a burnt orange, and so on down the list. Half-measures looked worse than committing to a different colour.

Each theme carries two values and the split is the load-bearing part. --accent is the fill (the nav bar, the filled buttons) with white lettering on it, so it is measured white-on-fill. --accent-ink is the same colour used as text, so it is measured against the page background. Both clear Web Content Accessibility Guidelines (WCAG) AA, and every ratio is recorded in the stylesheet beside the value rather than left to be re-derived later.

Measuring against the real page background rather than against pure white is what caught the one failure: the lime ink read fine on white and came to 4.08:1 against the actual page, which is a shade off white. It is darker now.

The panels are also more transparent in light mode than in dark, and the page behind them is a shade off white rather than pure white, so the cards read as layers instead of rectangles drawn on a sheet of paper. I did not decide any of that alone. I asked people who actually prefer light mode which versions were easier on their eyes, and went with what they picked rather than what looked good to me at midnight in a dark room. The two modes are meant to be equally finished, not one real design and one accessibility checkbox.

The JavaScript

About 120 lines, and every page works without it. It fills in the footer year, toggles colour mode and remembers the choice, opens the email dropdown, copies the address to the clipboard, expands a project card when you tap it, and flips the coin on a touch screen where there is no hover to trigger it. What it deliberately does not do is render anything. Every word on this site is in the HTML, not assembled by a script after the fact.

Limits

Being honest about what my current approach might be lacking.


Source on GitHub, or back to Work.