I wanted the site to be easy to inspect and hard to break. The implementation is deliberately simple: eighteen hand-authored HTML files, one 81 KB stylesheet, two scripts with nothing imported into either — 41 KB, and a few hundred bytes that set the reading mode before the first paint — four self-hosted font files, and an 8 KB index the search menu reads when it is opened. That is the whole supply chain, and every word of it is in view source.
I don’t dislike frameworks. This site simply didn’t need one. Its main job is to hold still and be read, so plain HTML, CSS, and JavaScript felt like the honest amount of machinery.
IThe rules I gave myself
These live at the top of codex.js. Writing them down kept me from quietly bending them when a
shortcut became tempting:
Nothing above the fold waits on an animation
The hero has one signature moment — the sentence arrives in monospace and settles into the display face, which is the footer line made literal. It lasts under a second, and the page is readable throughout it and identical to the no-JS render the moment it ends. It plays once per visit: come back to the home page in the same session and the sentence is already set.
The number in the markup is the true number
Every figure that counts up on scroll reads its target from the markup and animates toward it. There is no separate data value that could drift from the one that ships. Turn JavaScript off and the real numbers are simply there.
Transform and opacity only — never layout, never backdrop-filter
Motion is confined to the two properties the compositor can handle without touching layout or
paint. There is a documented budget of three simultaneous glass surfaces, because
backdrop-filter is the
most expensive thing on the page and the easiest to add a fourth of by accident.
Reduced motion is enforced twice, in CSS and in JS, independently
Not because one of them is unreliable, but because they fail differently. The comment in the stylesheet reads "both truths stated in CSS as well as in JS: if the script ever runs where motion is unwelcome, neither part appears."
Reveals fire once and are never re-triggered
An element that has appeared stays appeared. Scrolling back up a long case study does not replay anything, because the second viewing is navigation, not arrival.
IIWhat that bought me
| Property | Result | How |
|---|---|---|
| First contentful paint | 68 ms | Paint Timing API, median of twenty-one cold loads, local server |
| Own payload | 286 KB | Home page: HTML, CSS, JS and the four faces it loads; images excluded |
| Third-party payload | 0 KB | No script, font, style or beacon from any other origin |
| Runtime dependencies | 0 | Nothing imported into either script |
| Inline styles and scripts | 0 | None in the markup; the content policy allows neither, and a check fails any push that adds one |
| Horizontal overflow | 0 | Ten viewports, 320–2560 px, across all eighteen pages |
| Broken internal links | 0 / 532 | Every internal href and anchor resolved against the filesystem and the site’s 404 rules |
| Accessibility violations | 0 | axe-core 4.13, all eighteen pages, at 375 px and 1440 px, in Paper and in Dusk |
| Renders without JavaScript | Fully | All content, all real figures, settled headline |
IIIHow I made it with agents
You can inspect this repository, so this is the one place where I can show the method rather than just describe it. The production code it also informs is private. What follows is how I worked with coding agents on this site; I’m not pretending one portfolio proves more than that.
The loop is specification → generation → verification → correction → acceptance. The shorter version — prompt, output, ship — is how small mistakes become permanent ones.
What I specify
The rules in section I, plus the ones that govern content: preserve the editorial voice and the
visual system; no framework, no build step, no dependency; no fabricated metric and no career claim
the record does not carry; audit read-only before modifying anything. The commit history is the
evidence that this ordering is real rather than described — archive gate — commit the spec precedes
build three concepts, ship
none, and text and number
gates + before snapshot precedes the work it gates.
What the agents generate
On the most recent pass: a homepage rebuilt around what is being built rather than around an
argument about software; two sections renamed and their routes redirected; a new page for the four
conversations worth having; a native <details> fold that lets four case studies stay complete
without charging every reader six thousand words; attribution lines stating what I owned and what
I did not on each project; and one contact address replacing five identities. I didn’t type most
of that. I described the job, reviewed the result, and decided what stayed.
What the system verifies — not me reading it
axe-core across all eighteen pages at 375 px and 1440 px; a hundred and eighty page-and-viewport combinations checked for horizontal overflow; all 532 internal links and anchors resolved against the filesystem; the no-JS render compared against the scripted one; reduced motion checked in the stylesheet and in the script independently. Those checks return zero every time, and they are the reason I am willing to publish the numbers in section II at all.
They do not always return zero first. On this pass axe found a serious contrast failure on
twelve surfaces — a summary label I had dimmed a further 30% with
opacity after choosing a
colour that passed on its own — and a definition list I had written with the wrong container
element. Both are fixed above. Neither would have survived a careful reading; neither was caught by
one.
What the loop caught, and who caught it
The distinction matters more than the list. The audit found a homepage sentence claiming this site's essay contained a controlled trial that argued against it when the essay did not; a colophon that counted its own pages as sixteen when there were seventeen; a career sentence contradicted by the record two clicks away; scroll containers no keyboard could reach on a phone; and two font files that had been shipping to nobody for months. What the audit does not establish is who introduced any of them — I have not reconstructed that, and I am not going to guess in my own favour. I corrected the ones that were decisions rather than defects: dropping the analytics rather than gating it, leaving GitHub off because the repositories that matter are private and an empty list is not evidence, refusing to reconstruct a citation I have lost, and refusing to publish a measurement method that was never mine.
What this is not
One static document, eighteen pages, no users and no uptime. It is evidence of a working method on an artefact of that size, and it is not evidence that a production system can be built this way without engineers. The platform in the Vaulth case study needed an engineering constitution, eight CI jobs on every pull request and two full security reviews precisely because the loop described here does not scale on its own.
IVWhat still needs work
A page like this should include the awkward parts too. These are the ones still open:
I now have no idea who reads this. Google Analytics used to load on every page — 165 KB, larger than the HTML, CSS and JavaScript I wrote here put together, and set without asking anyone. That was hard to defend on a site whose flagship case study argues for consent over convenience and whose second product transmits nothing at all. So it is gone, and the content policy now permits no third-party origin at all. The cost is real and I am paying it knowingly: I have no traffic data, no referrer data, and no way to tell whether anyone reaches the end of a case study. If I want that back, it will be something cookieless that I can explain in one sentence — and until then the honest position is that I am flying blind on purpose.
There is still no templating, but the duplication is no longer typed by hand. It is the honest reason this site shipped for months with no canonical tags and no social preview metadata: the change was mechanical and touched every file, so it kept losing to work that touched one. Renaming two sections meant rewriting the navigation in eighteen places. The shared head, nav and footer now live once in a partials folder, and a small script copies them into every page between marker comments; a check on every push fails if a page drifts. The pages are still plain HTML that deploy exactly as written. It is a copier, not a generator, and whether it should become one is a trade I have not settled.
The oldest case studies still carry all the photographs. Three student projects from 2020–22 hold every screenshot on the site; the four products I would actually want judged are drawn as figures or described in prose. The student galleries are now folded and filed under earlier work, which changes the emphasis without fixing the underlying gap: there are no product screenshots yet, and this page will say so until there are.
VThe typefaces
Cinzel for display, EB Garamond for reading, JetBrains Mono for anything that labels rather than speaks. All three self-hosted, subset to Latin, preloaded, and cached immutably — four files, because EB Garamond ships a roman and an italic.
EB Garamond is served with a
size-adjust: 116%, which is
a fossil worth admitting to. This design started as two variants, and the other one set Newsreader;
EB Garamond's x-height measures 0.804 of Newsreader's at the same size, so the adjustment kept one
shared type scale instead of forking every token. Dusk then shipped everywhere, nothing ever set the
other variant again, and its two font files sat in the repository being downloaded by nobody until an
audit of this page found them. They are gone. The adjustment stays, because the scale is now built
around the adjusted size rather than the raw one — the correction outlived its reason and became
load-bearing, which is the usual way that happens. A later audit did trim it, from 124% to 116%, once
it was clear that a phone line had shrunk to about thirty characters.
The habit comes from somewhere specific: I typeset fiction with the same two faces, and the About page explains why that is either satisfying or a warning sign.
The tools are the footnote. The useful part was deciding which intentions needed to become rules.