Anatomy of a page
A dissection of the page you’re reading, top to bottom: the night above it, the grid under it, the type, the code, the maths, the figures, and every small thing a post here can do.
Every site has a page like this somewhere. The one where the author tries every button at once to check that nothing explodes. Mine used to be a list: here is a table, here is a quote, here is a callout. It worked, but reading it felt like flipping through a furniture catalogue.
So this time we’re doing it properly. We’re going to take this page apart,
layer by layer, top to bottom, and look at how each piece is made while
it’s sitting right in front of you. Every feature on the site shows up
somewhere below, doing its actual job. If something looks broken, that’s
a bug, and you should tell me
Here’s the whole thing, pulled apart:
Four layers. A grid that decides where everything may go. Figures and code that sit on it. Type that does most of the actual work. And on top, a small night sky that has no reason to exist except that I like it.
Let’s start with the useless part.
The night on top
Posts keep their tops clear for reading, but the home page and every section page open on a small night sky. Nobody drew it. Your browser generates it on the spot from a single number, the seed, picked at random every time you arrive, so reload and you get a different one. Each page has its own kind of night: the writing pages get a forest and a cabin with the lamp on, projects get a city still being built, the about page gets the house I grew up in, the setup page gets a laptop out on a hill, and the home page gets a city.
Here is a writing-page night, painted on the CPU instead of the GPU, at its real resolution and blown up six times so you can count the pixels:

Look closely at the sky. It isn’t a smooth gradient. It can’t be: the
whole night uses exactly twenty-four colours, and a gradient needs
hundreds. What you’re seeing instead is ordered dithering. Each pixel
gets nudged up or down by a fixed pattern, a Bayer matrix, before it
snaps to the nearest colour. Squint and the pattern blends back into a
gradient.
Play with it. Switch the pattern off and watch the smooth light break into bands, then turn the colours down to two and see how much the pattern can still fake:
This figure is interactive.
A few other things are hiding up there. Warm colours (Saturn’s cream, the
amber of a lit window) are only allowed where something warm was actually
drawn, so the sky never dithers into orange speckle. A shooting star comes
by every ten seconds or so, a satellite every minute or two, and over the
writing pages’ forest, fireflies
All of it stops when you scroll it out of view or switch tabs, and none of it moves at all if your system asks for reduced motion.
How a post becomes a page
I write every post as one Markdown file. By the time it reaches you, it has been through a small factory:
- Parse. Sätteri, a Markdown parser written in Rust, turns the file into a tree called mdast: one node for every heading, paragraph, list and code block.
- Shape it as Markdown. Plugins rewrite that tree. This is where
:::notebecomes a callout, where::demobecomes a figure waiting for its script, and where the maths gets typeset into MathML. - Shape it as HTML. The tree becomes hast, an HTML tree, and a second round of plugins runs. Lone images become numbered figures, footnotes move into the margin, and every code block goes through Expressive Code.
- Write it out. The result is plain HTML, built once on my machine. Your browser gets no Markdown and no parser.
That last point matters more than it sounds. An article page here ships under fifty kilobytes of JavaScript before you touch an interactive figure. Most of that is the meadow in the footer, which only loads when you scroll near it. The rest is small: the contents rail, the service worker that keeps posts readable offline, and the loader that fetches each figure’s script on demand.
The grid underneath
Every block on this page sits on one CSS grid with five columns. The middle one is the measure, where text lives. The two on either side of it let things go wider when they need the room. The outer two run to the edges of the screen.
The measure is 38rem, which in this type comes out at roughly
seventy characters a line. That number isn’t arbitrary. Typographers have
been arguing about line length for a few hundred years, and the argument
mostly settled somewhere around 45 to 75 characters.
Try it yourself:
This figure is interactive.
Go narrow and your eyes start hopping. Go wide and you’ll catch yourself reading the same line twice, because the jump back to the left edge is long enough to land on the wrong line. If you’d rather have it wider or narrower anyway, the Aa button at the bottom left lets you change the line length, along with text size, spacing and typeface. Your choice sticks for every post.
Type
Body text is set in Figtree. Code, numbers and labels are in
JetBrains Mono. Mixing two typefaces usually has one ugly side effect:
their lowercase letters come out different heights, so inline code
bulges out of the sentence around it. The fix is one CSS property.
font-size-adjust: ex-height 0.5 tells the browser to scale every font
until its x-height is exactly half its size. Now Vec<u8> and
O(n log n) sit inside a sentence like they belong there. Code can even be
highlighted inline, like items.reduce((a, b) => a + b, 0), using the
same theme as the big code blocks.
Sizes come from a modular scale: every step is the one below it times 1.2. The base itself is fluid. It grows from 15px on a phone to 16px on a desktop, and every step grows with it, so headings keep their proportions at any width.
This figure is interactive.
The rest of the inline vocabulary is small on purpose. Emphasis is
italic, strong is heavier, a mistake is struck through,
a highlighted phrase gets a soft wash of green, keys look like
Ctrl + K, and links stay quiet until you need
them. Quotes curl themselves: I typed “straight
quotes” and it’s typed them back as curly ones. Look at the left edge of
this paragraph too. Where the browser supports it, punctuation that
starts or ends a line hangs into the margin, so the text edge looks
straight to the eye instead of to the ruler.
A third-level heading
Headings get lighter and tighter as they go down, so the outline of a long post stays readable at a glance. Hover over one and a link appears so you can point someone at that exact section.
And a fourth
Rarely needed. It’s here so I know it works.
You should notice the words, never the page.
Quotes and lists
A quotation hangs its opening mark in the margin, so its first word lines up with the text around it:
Premature optimisation is the root of all evil, but premature abstraction is the root of all confusion.
— a note I left myself after a long refactor
Unordered lists use a quiet dash, and nest:
- The page cache absorbs writes.
- The journal orders metadata.
- Ordered mode writes data before metadata.
- Writeback mode doesn’t, and is faster.
- The disk cache lies, sometimes.
Ordered lists keep their numbers in the mono face:
- Measure.
- Change one thing.
- Measure again.
Checklists finally have boxes that match the rest of the site:
- A night above every page
- Figures that load only when you get near them
- Code in tabs
- A post that doesn’t need fixing after I publish it
And when a list is really a walkthrough, :::steps turns it into numbered
steps down a rail, like the build steps a few sections up.
Code
Code blocks sit in a recessed well and widen past the text column when they need the room. They can carry a title, highlight lines, and show what was added and what was removed:
const targets = links.map((a) => document.getElementById(a.hash.slice(1)))
function active(line: number) { let current = -1 for (const [i, target] of targets.entries()) { if ((target?.getBoundingClientRect().top ?? Infinity) < line) current = i else break } return current + 0 return current}That function is real, by the way. It’s how the contents rail on the left knows which section you’re in.
Shell sessions get a terminal frame:
bun run check && bun run build# 23 page(s) built in 9.21sLong files can fold their boring parts away, and show line numbers when you need to talk about line 12:
5 collapsed lines
use core::iter::Iterator;use x86_64::structures::paging::{ FrameAllocator, PhysFrame, Size4KiB,};use bootloader_api::info::{MemoryRegionKind, MemoryRegions};
pub struct BumpFrames<I: Iterator<Item = PhysFrame>> { frames: I,}
unsafe impl<I: Iterator<Item = PhysFrame>> FrameAllocator<Size4KiB> for BumpFrames<I> { fn allocate_frame(&mut self) -> Option<PhysFrame> { self.frames.next() }}Code in tabs
The newest trick. When the same thing can be written several ways, put
the versions in one window with a tab each. Tab sets that share a sync
name switch together, so pick your package manager here:
bun add satterinpm install satteripnpm add satteri…and this one follows along:
bun run buildnpm run buildpnpm buildYour choice is remembered, so the next post with install steps opens on the same tab. Tabs work with the keyboard too: the arrow keys move between them. Here’s the Bayer matrix from the night sky, three ways:
const bayer = (n: number): number[][] => n === 1 ? [[0]] : Array.from({ length: n }, (_, y) => Array.from({ length: n }, (_, x) => { const half = bayer(n / 2)[y % (n / 2)][x % (n / 2)] return 4 * half + [[0, 2], [3, 1]][(2 * y) / n | 0][(2 * x) / n | 0] }), )fn bayer(n: usize) -> Vec<Vec<u32>> { if n == 1 { return vec![vec![0]]; } let half = bayer(n / 2); let corner = [[0, 2], [3, 1]]; (0..n) .map(|y| { (0..n) .map(|x| 4 * half[y % (n / 2)][x % (n / 2)] + corner[2 * y / n][2 * x / n]) .collect() }) .collect()}def bayer(n): if n == 1: return [[0]] half = bayer(n // 2) corner = [[0, 2], [3, 1]] return [ [4 * half[y % (n // 2)][x % (n // 2)] + corner[2 * y // n][2 * x // n] for x in range(n)] for y in range(n) ]Maths
Inline maths like sits on the baseline with the text. It’s typeset into MathML when the site is built, so your browser draws it natively, with no maths library to download. Display maths gets its own line and is allowed to go wide:
Aligned derivations keep their equals signs in a column:
And the dithering from earlier, written down properly: a pixel with brightness , at position , in an Bayer matrix , gets painted with palette entry
where is the number of colours. That’s the whole algorithm.
Tables
A small table stays as narrow as its content:
| Structure | Query | Update |
|---|---|---|
| Prefix sums | ||
| Fenwick tree | ||
| Segment tree |
Numbers align on the right, so the digits line up:
| Level | Latency | Size | Bandwidth |
|---|---|---|---|
| L1 | 1 ns | 64 KiB | 2 TB/s |
| L2 | 4 ns | 1 MiB | 1 TB/s |
| L3 | 12 ns | 32 MiB | 500 GB/s |
| DRAM | 80 ns | 64 GiB | 60 GB/s |
| NVMe | 20 µs | 2 TiB | 7 GB/s |
A wide table scrolls sideways instead of squashing itself:
| Model | Params | Layers | Heads | d_model | Context | Tokens | FLOPs | Year |
|---|---|---|---|---|---|---|---|---|
| tiny | 6.8M | 6 | 8 | 256 | 1024 | 0.6B | 2.4e16 | 2024 |
| small | 124M | 12 | 12 | 768 | 2048 | 10B | 7.4e18 | 2025 |
| medium | 350M | 24 | 16 | 1024 | 4096 | 26B | 5.5e19 | 2025 |
| large | 1.3B | 24 | 32 | 2048 | 8192 | 100B | 7.8e20 | 2026 |
Figures
Images are measured when the site is built and placed by their shape. You already saw a panorama run edge to edge. A landscape image takes the wide band:

A portrait image stays at text width, so it never towers over the page:

Pictures that belong together can sit side by side with :::grid. Each
one keeps its own number and caption, and on a phone they stack:

There are two ways to put an SVG on the page, and the difference matters.
Written as an image, like the memory bars above, it’s a picture: its
colours are baked in, and it looks the same in both themes. Written with
::art, the drawing is pasted into the page itself. That means it can use
the site’s own colours. Flip the theme toggle (bottom left) and watch
every drawing in this post repaint itself. It also means it can move:
data-build="1","2"and so on on a group makes the drawing assemble in that order as it scrolls in. That’s how the exploded page at the top puts itself together.data-motiongives an element a small loop.floatis Saturn bobbing,twinkleis the stars,pulseis the lit windows, andflowcarries the dots through the pipeline.
All of it is just transform and opacity, so it’s cheap, and all of it
switches off if you’ve asked your system for less motion.
Then there are figures you can poke. ::demo places one of my own custom
elements in a figure, and its script only downloads when you scroll near
it. Every interactive figure in this post loaded that way. If you never
reach the bottom of a post, you never pay for its toys.
Asides
Not everything deserves the reader’s full attention. Callouts come in five kinds:
Note
Notes add context the reader might want, but doesn’t need.
Tip (Measure first)
Profile before you optimise. The slow part is rarely where you think it is.
Warning
Benchmarks on a laptop running on battery are really benchmarks of its power saving.
Caution
fsync errors are not retryable on Linux. Treat them as data loss.
Important (Read this)
Every number in the tables above is illustrative.
Any of them can start closed, for things only some people want:
Note (How the sidenotes work)
Footnotes in the Markdown become margin notes on a wide screen and
tap-to-open notes on a phone. No JavaScript is involved: the number is a
<label> for a hidden checkbox, and CSS shows the note when it’s checked.
A plain collapsible digression
Sometimes a detail is worth keeping, but not worth everyone’s time. This one says that the page you’re reading was generated from a single Markdown file, plus five drawings and three small scripts.
Then there are the notes that hang in the margin. Numbered ones come from
ordinary footnotes:margin[…], for an aside that doesn’t need a number.
Around the edges
A page is more than its article. Here’s everything around this one:
- The rail. On a wide screen, the contents sit on the left, and the current section lights up as you read (that’s the scrollspy from earlier). On a narrower one it steps out of the way entirely.
- Reading options. The Aa button sets text size, line length, spacing and typeface, for posts only.
- The theme. The half-moon button switches between light and dark. Everything on this page is drawn in colour tokens, so all of it follows, except the night, which stays a night.
- Share. At the end of every post, a share link. On a phone it opens your phone’s own share sheet.
- Series. A post can come in parts: a folder with one file per part, read as one long page with the parts numbered. There isn’t one yet, but the shelf is built.
- Moving between pages. A post’s title slides from the list into place, and the night keeps its sky from page to page, using the browser’s own View Transitions.
- Offline. The site installs as an app, and any post you’ve opened stays readable without a connection.
- The feed. RSS, for people who read like it’s 2005, which is the correct way to read.
The writing kit
That’s the tour. Here’s the cheat sheet I keep for myself, in tabs, for next time:

::art[A caption]{src="./assets/drawing.svg" alt="What it shows" span="wide"}
::demo[A caption]{is="element-name" span="wide"}
:::grid{cols=3} and , each in its own paragraph::::::note[Optional title]{closed}Callouts: note, tip, warning, caution, important.:::
A line with a numbered note.[^1] And :margin[an unnumbered one].
[^1]: Becomes a margin note on wide screens.:::steps1. **First.** Steps down a rail.2. **Second.** Any ordered list will do.:::
:::pull[— whoever said it]One line, set large.::::::tabs{sync="pm"}```sh title="bun"bun add thing```
```sh title="npm"npm install thing```:::
Inline and highlighted: `const x = 1{:ts}`And here’s what’s left on my list:
- Rebuild this page so it shows everything
- Find a reason to use a fourth-level heading
- Stop adding features and write more posts
That last box might stay empty for a while.