sergio esteban torres

Notes · July 2026

Building this site

You probably arrived here from the word “Hand-built” in the footer. This is what that word cost.

The constraint

This site had one design constraint that outranked every other: it had to be its own proof. A portfolio that claims performance and craft while shipping a megabyte of framework runtime is arguing against itself. So the rule: no framework, no build step, nothing between the repository and what your browser receives. What is committed is what ships; the deploy pipeline is git push.

Anyone can reach for the tooling. Here, 8 KB of HTML, 12 KB of CSS and 3 KB of JavaScript are the whole job. The repository has no package.json, no lockfile, no config directory. There is nothing to install, and therefore nothing to rot.

Fonts: subset, self-host, metric-match

The heaviest thing on this page is type, and that was a decision. Webfonts are also the classic source of layout shift: the fallback paints first, the webfont lands, everything moves.

The choice here has three parts. First, four static woff2 files (Archivo 400 and 500, Archivo Expanded 700 for the display caps, IBM Plex Mono 400) subset to the Latin range the site actually uses: 42,732 bytes for all four, with font-synthesis: none so no browser fakes a weight I didn’t ship. Second, font-display: swap, because text you can’t read while a font loads is a worse trade than a visible swap. Third — the part that makes swap free — each family has a metric-matched local fallback, Arial and Courier New overridden to lay text out on the webfont’s metrics before it arrives:

@font-face {
  font-family: "Archivo-fb";
  font-weight: 400 500;
  src: local("Arial");
  size-adjust: 98.62%;
  ascent-override: 89.03%;
  descent-override: 21.29%;
  line-gap-override: 0%;
}

The percentages are derived from the two fonts’ metric tables. The result: the swap happens, and nothing moves. Cumulative Layout Shift measures 0 in the recorded Lighthouse runs, on both form factors.

Themes: 130 bytes before first paint

Dark mode is a checkbox; dark mode without a flash of the wrong theme is a discipline. Any theme applied by JavaScript after first paint flashes, and any toggle that lives only in JavaScript breaks without it.

The answer is 130 bytes, inline in the <head>, ahead of the stylesheet:

<script>(()=>{try{const t=localStorage.getItem("theme");
if(t==="dark"||t==="light")document.documentElement.dataset.theme=t}
catch(e){}})()</script>

The stylesheet then carries two routes to the same tokens: a prefers-color-scheme media query for people who never touch the toggle (which is also why the site themes correctly with JavaScript disabled) and a data-theme override for people who do. Yes, that means the dark token block appears twice in the CSS. light-dark() would collapse the duplication, but it fails unrecoverably on pre-2024 browsers, and a locked-down corporate laptop is exactly where this page cannot afford to break. The duplication was the cheaper bill. The toggle button ships hidden and is revealed by the same script that makes it work; the theme-color meta is read back from getComputedStyle rather than maintained as a second hardcoded palette.

The colophon that measures itself

Performance claims on portfolio sites are usually adjectives. The footer of this site states numbers instead — Lighthouse 100 · 100 · 100 · 100 on mobile, ≈49 KB compressed — and then goes one step further: it measures the visit you are currently making. A short block of JavaScript sums compressed payload bytes (encodedBodySize, for entries that actually crossed the network) and prints the request count alongside; a buffered PerformanceObserver re-runs it when a straggler (the favicon) lands after load. Headers are deliberately excluded: they ride HTTP/2 header compression and vary with the CDN’s mood, which makes them a poor unit for a promise. On a return visit it will happily report 0 KB (your cache).

The git history keeps that line honest twice over. The first version printed DOM-ready time; the review pass replaced it, on the grounds that a duration measured on your device mostly describes your device, while bytes are the same on every phone. The commit reads “visit line reports requests + wire bytes instead of device-dependent timing.” And when a later edit pushed the script past the footer’s “under 1 KB” claim, it was the claim that changed, not the code that got golfed to defend it. The commit message states the policy outright: the number stays honest.

One stylesheet, six layers

All styling is one file, and single stylesheets die a known death: specificity creep, then !important, then archaeology. The defense here is native cascade layers. The file opens by declaring its constitution — @layer reset, base, layout, components, motion, print; — and later layers win by design rather than by selector arms race. The only !important in 593 lines appears twice in the reduced-motion kill-switch, where it is the point: prefers-reduced-motion: reduce flattens every animation and transition unconditionally. Motion is double-gated. Entrance animation exists only inside a no-preference query, and the reduce query zeroes everything anyway. Print is not an afterthought but the sixth layer: the display type drops to 2.5rem, sections unstack, rows refuse to break across pages. The whole file is 593 lines and 3,023 bytes gzipped.

What was rejected

The no-list is most of the story.

  • A framework and a bundler. Not purism. Arithmetic. The page is a header, four sections and a footer; the runtime would outweigh the content several times over. No commit in this repository has ever contained a package.json.
  • Motion beyond two keyframes. The stylesheet’s entire animation inventory is rise and fade, on load, once. Nothing scroll-triggered, nothing cursor-following, no particles.
  • A hover effect on the experience rows. It shipped, then died in review with the note “inert row hover removed”: rows that highlight invite clicks they cannot honor.
  • Invented dates. The experience log rendered start-years only, until real end dates were confirmed; the ranges appear in the commit titled “real end dates”, and not before. On this site an unknown stays visibly unknown.

How it was built

Every commit in this repository carries the trailer Co-Authored-By: Claude Fable 5. The site was built AI-assisted, in Claude Code. The git log was never going to hide that, so neither will I. The discipline is in the workflow around it: plan first, human veto on the plan, then verification of everything that ships. The log’s own language is verification: “AA-verified palette,” “Verified at 360/768/1280/1920,” “measured colophon,” “verified no overflow.” The numbers in the footer come from Lighthouse CLI runs, not estimates, and a fresh-context review pass (briefed to be hostile) produced the sixth commit. It is the same bar the main page’s Capabilities section promises for production work.

The numbers

Measured July 2026, Lighthouse 12, uncached first visit of the main page:

Compressed payload50,435 B (≈49 KB), webfonts included
Requests, first visit8
Lighthouse, mobile100 · 100 · 100 · 100
Lighthouse, desktop100 · 100 · 100 · 100
JavaScript1,274 B gzipped
Fonts4 files · 42,732 B
Stylesheet3,023 B gzipped

This page is held to the same bar: the footer below is measuring your visit right now.