Skip to content
Colophon

How this thing is made.

In the tradition of putting the production notes on the last page.

Type

Everything you read is set in Space Grotesk: headlines and body are the same family, just different weights, which keeps the page honest. Anything that behaves like an instrument, such as labels, dates, numbers, or buttons, is JetBrains Mono, uppercase and letterspaced so it reads as data, not prose. Both are served from this site, and the mono has a fallback of the same width, so nothing moves when it arrives.

Backdrop

Behind the page is a dark sheet with three large colour fields, lime, blue and a little white, drifting very slowly over a fine grain. The grain is there so a soft gradient never shows steps on a big screen. The fields are plain gradients that are drawn once and then only moved, so the drift costs almost nothing. They rise a little as you scroll, and they hold still if your system asks for less motion. Dark is the default. There is a paper light sheet if you’d rather, and the toggle in the nav remembers your choice. The lime is for emphasis, never decoration, and on paper it is mixed down so small text stays readable.

Glass

The panels are glass in the plain CSS sense, built from the same few layers: a translucent fill, a one pixel rim that is brighter at the top left, a light edge inside, a soft shadow, and a highlight that follows your pointer. Every value is a custom property, so the paper sheet swaps numbers, not rules. Real blur is only on the surfaces that float over sharp content: the nav, the menu on a phone, the palette and the contents list on a case study. Cards, tiles, chips and fields are translucent without blur, because they sit on the soft backdrop and blur would only cost frames. If your browser has no backdrop blur, or you have asked for reduced transparency, those surfaces turn solid. In print everything is a white box with a thin border.

Motion

Nothing hides the first paint: every headline is real text in the first HTML, and the page is complete before any script runs. Reveals on scroll are CSS scroll timelines, with a small observer only where the browser has none. Only transform, opacity, clip-path, filter and colour ever animate, so layout never moves. Moving between pages uses the browser’s native View Transitions API: the nav, the progress line and the backdrop hold still, and a project card opens into its case study. The tilt, the pull on buttons and the highlight share one small animation loop that wakes when something moves and goes back to sleep when it settles, so an idle page does no work. Scrolling runs through Lenis. With reduced motion set, the drift, tilt, cursor and transitions are all off.

Numbers

I hold the site to numbers and check them on a production build before anything ships. The pages are server-rendered and cached, so there is nothing to wait for.

LIGHTHOUSE, DESKTOP
100 on all four
LIGHTHOUSE, MOBILE
98 performance
LONG TASKS AFTER LOAD
0
LAYOUT SHIFT (CLS)
0

Engine

Next.js rendered on the server, PostgreSQL through Drizzle behind a small admin panel I built, so edits are a form, not a deploy, hosted on Render behind Cloudflare. Uploads go to Supabase storage. Stats come from the GitHub, LeetCode, and Codeforces APIs with a last-good-snapshot fallback: a flaky upstream can never blank a page. The resume page prints properly, too; try it.

  • Next.js 15 · App Router, server components
  • React 19
  • Tailwind CSS v4 · plus hand-written CSS for the glass
  • PostgreSQL · Drizzle ORM
  • Supabase · uploads
  • Lenis · smooth scrolling
  • View Transitions API · page transitions
  • Render · Cloudflare
  • GitHub, LeetCode, Codeforces APIs · Live Record