Build write-up · 7 min read

How I built this site

Thirty-two pages of plain HTML, no framework and no build step, held to four written specs by ten checks that measure the rendered page. It is the closest thing to a production codebase I own, and it is how I work.

Role
Everything: the design, the code, the checks that hold them together
Built with
HTML and CSS, Node scripts, and one Chrome that measures every page
Size
32 pages, 3 token files, 4 specs, 32 scripts
Status
Live. Every merge deploys; there is no staging in front of it
The homepage of this site at laptop width: the wordmark and six section links across the top, the portrait in a circle on the left, and on the right the eyebrow Senior Product Designer over the title “Product design for software that got complicated.”, a two-line dek ending “Now I design how people and coding agents build together.” with “people and coding agents” in bold, and two buttons reading See selected work and Start a conversation. Under the pair, six facts in two rows of three, each with a small line icon in a pale disc, among them Agent boundaries, and a row of six client wordmarks along the bottom.

My approach

  1. 01Tokens

    • Three token files: color, type, and the page shell
    • Four specs that are the rule, not the record of one
    • A fourteen-step type scale and four fluid ramps between its steps
    Tokens first
  2. 02Pages

    • Hand-written HTML with the shell copied in, not included
    • A third of the markup is comments saying why
    • One generated file, and a check that rebuilds and diffs it
    The pages
  3. 03Checks

    • Ten checks; five of them render every page in a real browser
    • Contrast measured on the resting page and in every forced state
    • Every number the specs assert, recounted from the tree
    What the checks measure
  4. 04Ship

    • A merge is live in about a minute, with nothing in between
    • Guards that refuse rather than warn
    • AI wrote most of the scripts, to rules that came from failures I found
    Nothing ships on a feeling

The problem

A portfolio says design systems, accessibility and prototypes in real code. A site that is itself a template with a contrast failure in the hero says the opposite. It had to be built to the standard it claims for the work, or the claim was decoration.

That standard is not a look. It is three things I ask of a production codebase, and a personal site should get no less:

  • The rules are written down
  • The page is measured against them, not eyeballed
  • A change that breaks a rule is refused before a stranger sees it

Tokens first, and the specs are the rule

Three files hold every value the site is allowed to use. One holds the colors. One holds the type scale, with its leading and tracking. One holds the spacing and radii of the page shell. A component that needs a size takes one of the steps, and a color that is not in the palette is not a color.

Four normative documents sit over those files: typography, color, spacing and motion. When a spec and the stylesheet disagree, one of them is a bug. All four have been found stale at least once, so a check reads them.

  • The type scale is fourteen steps, and every rendered size on every page at four widths is one of them. Headings ride four fluid ramps between named steps. All four share one viewport window, so the ladder stays in order at every width
  • The color spec tabulates every ink on every ground with its measured ratio. The accent splits into two tokens, one for fills and one for text steps. The fill token clears a 2px rule but does not clear small type
  • Motion is six durations and one curve, and a duration that is not one of them is refused by the same token check

The pages, written by hand

Thirty-two pages, each a plain HTML file. The header, the navigation and the skip link are copied into every one rather than pulled from an include. So a change to the shell is a change to thirty-two files. The site knows it: a check counts how many pages load each stylesheet and holds the documentation to that number.

There is no build step. One file is generated: the Tailwind stylesheet the six oldest case studies still load. The pipeline rebuilds it and diffs the bytes against what is committed. It has to, because that build emits only the classes it has seen. A class the markup names that the build has never met is not an error. It is nothing at all, silently.

About a third of the markup is comments. They say why, not what: which alternative was tried, what it measured, and what sent it back. A rule with no reason attached is the first thing the next change deletes.

What the checks measure

Ten checks run on every merge. Five read the tree and finish in about a second. Each confirms one thing:

  • It can be deployed at all
  • Every token the specs name exists
  • Every number the documentation asserts still counts
  • What each page tells a machine matches the page
  • Every article's key ideas are still its headings

The other five open every page in Chrome and measure what rendered. The figures below are the sweep of 30 September 2026, at e10d336. They move with nearly every merge.

  • Every color a reader can see, at rest and in every reachable state: 11,530 measurements across the site, each against its floor. This one reads ink over artwork by the pixel. A caption on a photograph has no background color to compute from, and that caption once shipped at 3.88:1
  • Every hover, focus and script-applied state, forced: 2,005 state rules, each measured once per page that loads it
  • Every rendered type size at four widths against the fourteen steps: 47,792 of them
  • Every curved corner, for a border that follows only part of the curve: 29,216 elements
  • Every card on a ground, for an edge a reader can see against it: 374 raised surfaces, at a floor of 1.2:1

The prototype page renders its interesting surface from JavaScript in response to a press. For a while every check said the page passed while measuring a sixth of it.

A registry now names the states a reader can reach and the selectors that reach them. Each check presses into every one before measuring. A selector that matches nothing fails the run rather than quietly covering less. Its first pass found a live hover at 1.29:1.

The counts are watched as much as the verdicts. The state check once passed clean at 416 rules and at 617, and the gap was a third of the site going unmeasured. A run that comes back materially smaller means something stopped being measured.

Two things the site does for a reader that a check made honest

Every page of writing can read itself aloud, in the browser's own voice, marking the sentence and the word as it goes. It hands the synthesizer one sentence at a time. A single long utterance stops mid-word after fifteen seconds in Chrome, and in Safari it reports no position at all.

It marks the live text with the CSS Highlight API rather than wrapping words in spans. So the text a reader copies is the text that was there before they pressed the button.

Every page carries a canonical address, a share card rendered from the site's own type, and a schema.org graph. The graph is sixty-nine nodes across the site, all naming one Person.

Dates are stamped from the file's last commit rather than typed, so the sitemap and the article can never disagree about when a page changed. A check holds all of it, and holds the sitemap to listing every page exactly once.

Nothing ships on a feeling

A merge to the main branch is the live site about a minute later. There is no staging host in front of it any more. The branch and its checks are the gate. That is a reason to make the guards refuse rather than warn.

  • The deploy tars the repository, and one tracked symlink the runner cannot follow loses the whole thing. A check refuses any symlink and any tracked file the ignore rules match, before the upload and before the merge
  • Pull requests are squash-merged, so a branch and the main line diverge by construction. A script decides whether a force-push can lose work. Per file, it asks whether the branch's version exists anywhere in the main line's history. It exits 0, 1, or 2 for could not tell, and 2 is not a yes
  • The résumé is rendered from a script, to one Letter page at 1,056 pixels of 1,056. A tailored copy is a list of exact-match text swaps. A swap that matches zero times or twice refuses to render. A résumé that silently edited the wrong sentence is worse than one that did not render
  • A green run of the checks does not mean the site updated. The two workflows fail independently. Look for the deploy on the merge commit, and fetch with a cache-buster before deciding it failed

Since June, and before the change that wrote this sentence: 512 commits, 331 pull requests, and a written account under each of why. The account is the product as much as the pages are.

What AI actually did

An AI working in a terminal wrote most of the scripts, to rules I set. Nearly every rule came from a failure I found first:

  • The pixel-reading contrast check exists because a caption shipped at 3.88:1 and the ordinary check could not see it
  • The state registry exists because a page passed on a sixth of itself
  • The token check exists because the specs drifted from the stylesheet three times before anyone read them against it

What that changed is not who decides. It is how fast a bad idea can be shown to be bad. I would once have written a rule down and hoped to remember it. Now it is a script that refuses. The argument over whether it should exist happened once, in a comment, where the next person can read it.

The design is mine at every step: the scale, the palette, the order of the page, and every sentence. So is the judgment about what a check should hold the site to.

As in the other write-ups, the thinking happened out loud, with something that answered back, and I decided what was true.

It had a cost. The site is slower to change than a template would be. A change that moves a number has to move the sentence that states it. A change to a shared stylesheet waits for a nine-minute run. That is the price of checking a claim everywhere it is made, and for a site whose subject is that claim, it is the right price.