How this site is built

You are standing inside the artifact. Here is what holds it up.

Hand-written HTML and CSS. No framework, no build step, no dependency tree, no package manager. Every accessibility feature described below is running on the page you are reading right now.

Source
Flat HTML, one stylesheet, one script
Dependencies
None
Build step
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.

That is a deliberate choice rather than a limitation I have not yet outgrown. At this size, a build step would add a dependency tree to audit, a lockfile to keep current, and a failure mode between writing a change and seeing it. What it would buy in return — component reuse across nine pages — I currently pay for by editing the nav in nine places. That trade flips somewhere around a dozen pages; 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:

One subtlety worth recording, because it cost me an afternoon: a custom property that references another one resolves where it is declared, not where it is used. Writing --accent-soft: var(--accent) at :root would freeze it to the root accent and silently ignore the per-page theme. Everything derived from --accent therefore has to be declared inside the body.theme-* block, not at :root.

Light mode also needs more than an inverted background. Neon accents sit at roughly 1.3:1 against white, so each theme declares a separate --accent-ink — a darkened version used for text and links that clears WCAG AA — while the bright value stays for glows and borders where contrast does not apply.

The JavaScript

About 120 lines, and the page works without it. It does four things: fills the footer year, toggles colour mode and persists the choice, collapses the nav bar on downward scroll, and drives the email dropdown. The scroll handler is passive and coalesced through requestAnimationFrame; the cursor-glow handler returns early when prefers-reduced-motion matches, so the listener is never attached at all.

Limits, and when I would change my mind

Being honest about where this approach stops working is more useful than defending it.


Source on GitHub, or back to Work.