The Lab · 03 · Teardown

How this site is built

A designer's portfolio is a strange artifact: it is the one deliverable where the medium is also the sample. So here is the build, measured rather than described — what it costs, what it depends on, and what is still missing.

Every number on this page was measured, not estimated. The commands are printed beside the results so you can run them against the repository yourself. Where something is unmeasured, it says so.
The principle this artifact proves

A site that argues for evidence must be willing to be inspected itself.

Measured

The whole thing, in numbers

WhatMeasuredHow
Hand-written HTML pages44find . -name "*.html", excluding the book and print sources
Stylesheet, entire site313.2 KBstat -f "%z" styles.css → 320,685 bytes, unminified
Runtime dependencies, classic site0No root package.json, no node_modules, no bundler config
Third-party domains serving code2Google Analytics and Microsoft Clarity (heatmaps, added 2026-08-15; honours Do-Not-Track). Attention measurement (scroll depth, section dwell) is first-party: attention.js, 159 lines, no network of its own. Both load async and gate nothing on first paint. Google Fonts left on 2026-08-14 when the nine font files moved to this origin.
Interactive pattern demos9All in patterns/demos.js — 234 lines, 12.9 KB
Its imports, requires, fetches0grep -nE "import |require\(|fetch\(" → no matches
Open Graph share cards33All exactly 1200×630, verified with sips -g pixelWidth
Pages carrying a skip link38grep -rl skip-link
Build steps between edit and deploy0Commit to master; GitHub Pages serves the files as written

Decisions

Four choices, and what each one cost

No build step, and therefore no build to break

There is no bundler, no transpiler, no CI pipeline. A file I edit is the file the browser receives. The benefit is that this site cannot rot: no dependency will publish a breaking major, and nothing in it needs a lockfile to reproduce in three years.

What it costs: no tree-shaking, no automatic minification, and cache-busting done by hand — you can see the manual query strings, styles.css?v=p77, in the source of every page. On a site of this size that is the right trade. On a product it would not be, and I would not defend it there.

Fonts are served from this origin, not a third party

Nine woff2 files, latin subsets only, preloaded for the two faces that gate the first paint. No preconnect, no Google Fonts, no request leaving this domain for type. font-display:swap paints text immediately in the fallback and re-renders when the face arrives.

<link rel="preload" as="font" type="font/woff2" crossorigin
      href="/assets/fonts-web/source-sans-3-normal-400.woff2">
<link rel="preload" as="font" type="font/woff2" crossorigin
      href="/assets/fonts-web/source-serif-4-normal-300_600.woff2">
<link rel="stylesheet" href="/fonts.css">

One type scale, written down, enforced by refusal

Every size on the classic site comes from twelve custom properties — --fs-eyebrow through --fs-display-xl — declared once in styles.css and documented in a 174-line spec in the repository. The rule in that file is one sentence: if a value isn't in this file, it doesn't ship. Roughly twenty earlier one-off sizes are listed explicitly as retired, so a stray font-size: 17px reads as a bug rather than a preference.

Writing this page is what caught the drift described at the bottom of it.

The documents are the same HTML, printed

The portfolio PDF and the résumé are not designed twice. They are HTML pages with an @page rule and millimetre units, printed to PDF by Chrome — which is why a wording change reaches the PDF in one step. The ATS-readable .docx is generated by a 107-line Node script using the docx library: the only npm dependency anywhere near this project, and it is offline tooling that never ships to a browser.

One honest exception

The book view does use React

"Zero dependencies" is true of the classic site you are reading and false of the book edition, which is a React application — around 120 KB of my own code plus React and React DOM, self-hosted from the repository rather than a CDN, so the page still has no third-party runtime.

Two views of one portfolio, built two ways, was a deliberate exercise: the classic site proves I can work without a framework, and the book proves I can work with one. It also means I have opinions about both that came from shipping, not reading.

What's missing

The strongest objection, kept in

“A hand-built static site doesn’t prove you can ship a modern stack.” The book edition of this same site runs React — the stack shows up where the job needs it and stays out where it doesn’t. That allocation, not the absence of a framework, is the decision on display here.

The gaps, before you find them

A teardown that only lists strengths is a brochure. These are real, and they are the next things I would fix.

  • There were no automated tests at all until this week. The repository contained zero test files and an empty CI directory; verification was a manual checklist — every page at two widths, no overflow, console clean. The 42 assertions behind loop.js are the first tests in this codebase, which is a genuine gap being closed, not a strength.
  • No continuous integration. Nothing ran those 42 tests on push. A GitHub Action now runs node lab/loop.test.js on every push and pull request touching lab/ — this repository's first CI job.
  • Cache-busting is manual. Version query strings are bumped by hand, which means they can be forgotten — and have been.
  • Two third-party domains gated the first paint. Fixed 2026-08-14: all nine font files are now served from this origin, Google Fonts is gone from the critical path, and one third-party domain remains (analytics). The type stack changed with it — Source Serif 4 and Source Sans 3, a superfamily, replacing a mix that had monospace carrying 34 labels a page.
  • A token had drifted out of sync with its own spec. The design system documented --fs-display while the stylesheet defined --fs-display-xl — a rename that never reached the document. Found while writing this page, and fixed in the commit that published it.