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
- Readability comes first. Body text is Inter at 17px with a 1.62 line height, picked for its large x-height. That is what keeps the smaller supporting lines legible at the size they are set. The neon accent is loud, so it stays on borders, headings and glows and never lands on a paragraph you actually have to read. Dates, roles and captions use a lighter grey that still clears contrast, rather than the faint grey most portfolios reach for.
-
The glow is a signal, not decoration. Cards and panels brighten when you
hover them, so whatever is under your cursor is obvious without anything moving or
jumping. It ramps up over about a second instead of snapping on, because an instant
switch reads as a flicker when you drag the cursor across a grid of cards. Getting that
to work took a real fix: a
box-shadowonly animates when both states have the same number of layers, so every resting state carries a spare transparent layer just so the transition has something to interpolate to. -
Navigation. The first thing you land on when you press Tab is a "Skip to
main content" link. It sits off-screen until it is focused, then slides into view. It is
moved out of the way with positioning rather than hidden with
display:none, because an element that is display-hidden cannot be focused at all, which would defeat the entire point of having it. -
Landmarks are named, not anonymous. Sections are tied to their own
heading with
aria-labelledby, so a screen reader reads out something closer to a table of contents ("Currently", "Selected Work", "Skills") instead of "region, region, region." - You can always see where you are on a keyboard. Tab through the site and whatever link or button you are on gets a visible outline. Browsers draw that ring by default, and plenty of sites switch it off because it looks untidy in a screenshot. Switching it off makes a site unusable without a mouse, so it stays.
- Motion is optional. If your system is set to reduce motion, the smooth scrolling, the section-jump highlight, the card reveal and the animated footer art all switch off. The footer art is removed outright rather than slowed down, because a looping GIF has no pause control, so not showing it is the only honest answer.
- Colour is never the only signal. Links carry an underline on hover and focus; the accent is decoration on top of structure, not a replacement for it.
-
Explicit
width/heightanddecoding="async"on images, so the layout does not shift as they load.
The theming system
Colour lives entirely in CSS custom properties. There are two independent axes, and keeping them independent is the whole idea:
-
Light or dark is set on the root element as a
data-modeattribute. A tiny inline script in every<head>applies the stored or system preference before first paint. If it ran in the deferred script instead, the page would flash dark before switching to light. -
The accent is a class on
<body>. Each project page carries its own: green for the site itself, amber for Knights Counsel, sky for the build-inference study, red for SimpleBank, gold for PocketProfessors, aqua for FitnessFunctions, lime for the Contact Manager.
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.
- The nav bar is duplicated in every file. Nine copies today. When the amount of pages outway the benefits of hand-written HTML, I will need to switch to a templating system or reorganize the naviagion bar.
- No automated accessibility testing. The keyboard pass is manual, which means it is only as reliable as my memory to run it.