Ink, paper, and a dashed line .
Everything this site draws comes from one palette and one motif. This page is the source of truth for both: the tokens, the pencil pack, and each element built on top of them, with the markup that produced it.
Principles
Five rules. Everything below is a consequence of one of them.
- 01
One palette, two directions
Ink on paper, or paper on ink. There is no accent colour and there will not be one - every distinction has to be carried by weight, size, spacing or the dash. That constraint is why an artefact from this site is recognisable at thumbnail size.
- 02
The dash is the brand
Borders, rules, underlines, arrows, meters and bars are all the same interrupted line at different scales. A solid stroke means something is finished or filled; a dashed stroke means it is a container, a path, or a work in progress.
- 03
Drawn, not rendered
Curves have uneven control points, strokes carry a displacement filter, and lines arrive dash by dash rather than fading in. The page should look like someone worked it out on paper, because that is what the writing claims to be.
- 04
Serif thinks, sans reports
Newsreader carries voice: titles, pull quotes, section labels, buttons. Inter carries data: metadata, tables, tags, status, code context. If you cannot decide, the text is probably doing both jobs and should be split.
- 05
Artefacts over posts
Leaderboards, meters, tracks and changelogs exist because they earn repeat visits and survive the week a post does not. Every element here is built to be screenshotted on its own and still make sense.
Palette
Two surface tokens, two ink tokens, and five opacities derived from them. Dark mode swaps ink and paper - nothing else changes, which is why no component ever branches on theme.
- --ink-100 body text, drawn strokes, filled ticks
- --ink-70 eyebrows, secondary copy, metadata
- --ink-45 dashed frames, rules, arrow strokes
- --ink-25 dot leaders, table rules, empty checkboxes
- --ink-12 unfilled meter and bar ticks
Type
Newsreader for anything with a voice, Inter for anything with a number in it. Both variable, both self-hosted, both preloaded.
- Display Newsreader · 3xl → 5xl · mediumI write about staying sharp while AI codes.
- Section Newsreader · 2xl → 3xl · mediumWhat actually moves an audience
- Label Newsreader · xl → 2xl · italicLab notes
- Body Inter · base · 1.8 leadingShort essays, screenshots, occasional honest panic.
- Small Inter · sm · metadata and captions8 min read · updated 14 Jul 2026
- Eyebrow Inter · 11px · 0.14em trackingReproduce this
The dash
Native dashed borders give you no control over dash length, so every framed surface here is painted with repeating gradients and every stroke is an SVG with an explicit dasharray. Five presets, in CSS pixels, shared by CSS and SVG alike.
- hair 2px on / 3px off - inline rules under dense metadata
- fine 4px on / 3px off - small glyphs, checkmarks, nav arrows
- default 7px on / 5px off - frames, rules, every connector arrow
- coarse 12px on / 7px off - display marks, circled annotations
- solid - on / - off - filled state only: arrow tips, ticks
Scale independence
The same glyph at 16, 48 and 120 pixels. Stroke weight and dash rhythm are identical because every stroke carries vector-effect: non-scaling-stroke, which reads them as CSS pixels instead of viewBox units. Drop that and the small one dissolves into specks while the large one goes slab-thick.
Texture off
The default. Hand-drawn quality comes from the authored control points, not a filter.
Texture on
feTurbulence displacement. Its scale is in viewBox units, so it only behaves on large glyphs - on a 40-unit mark it eats 4% of the shape and the stroke reads as crooked. Opt in, never default.
Arrows
Twelve shapes, one engine. Each is a dashed stroke revealed by a solid stroke sweeping the same path, so the dashes appear one at a time instead of sliding into place. Tips fade in at 72% of the draw.
Props
variant · stroke · dash · duration · delay · flip · flipY · rotate · headStyle · texture · animate · stretch · label
Markup
<Arrow variant="s-loop" class="w-64" />
<Arrow variant="right" flip dash="fine" stroke={1.5} />
<Arrow variant="branch" duration={1200} delay={200} /> Marks
The strokes you would add to a printed page. Same drawing engine as the arrows, no tip. Two underlines ship: the single pass is the default, and the double is a second lap for something you came back to - at body size it turns the first stroke into noise, which is why it is not the default.
Usage
Marks carry meaning, not decoration. A tick means shipped; a cross means cut; a scribble means deprecated but kept for the record.
- Hall of Fame shipped
- Real-time collaborative editing - cut, out of scope
- Needs a second look before v1.1
Markup
<Mark variant="underline" class="w-40" />
<Mark variant="check" class="h-4 w-4" dash="solid" /> {* under 24px: solid *}
<Mark variant="bracket-left" class="h-40" dash="coarse" /> Surfaces
Rules, frames, callouts, code. Anything that encloses content is dashed; anything solid is filled data.
Rules
dashed · hair · coarse · double · wave · ornament. The wave and the ornament draw in; the rest are CSS gradients.
Frame
Dashed on all four sides, with an optional label that punches through the top edge.
A frame says "this is a thing you can lift out of the page". Use it for anything a reader might screenshot.
Markup
<Frame label="config" class="px-5 py-5">…</Frame>
<Frame variant="ticks" class="p-6">…</Frame> Frame - ticks
Corner ticks imply the box without drawing it. Default for cards, so a grid of them does not read as a cage.
Lighter than a full frame. Everything on the workbench homepage uses this.
Callouts
note · aside · repro · source · warn. Each opens with its own mark.
Markup
<Callout variant="repro" title="Reproduce this">
Repo, notebook and the exact config are in <a href="…">git-bonsai</a>.
</Callout> Code frame
Filename tab on a dashed border. The three dots are a nod at a window chrome the site otherwise refuses to draw.
$ npx git-bonsai --repo . --season autumn
→ reading 1,284 commits
→ growing…
✓ bonsai.svg written Inline
What happens inside a paragraph: annotations, margin notes, tags, keys.
Annotate
Draws a stretched mark over a run of text. Underline for emphasis, circle for the one claim of the paragraph, scribble for something being retracted.
The problem is not output , it is what you retain afterwards. The old advice - just read more carefully - does not survive contact with an agent that writes faster than you read.
Markup
The problem is not <Annotate>output</Annotate>, it is
<Annotate mark="circle">what you retain</Annotate> afterwards. Side note
Replaces footnotes. Floats into the right margin above 1300px, folds inline below it. Numbering is a CSS counter, so inserting one mid-draft renumbers the rest for free.
Kosmyna measured EEG differences MIT Media Lab, 2025. n = 54, four months, three cohorts. that persisted after the tool was taken away, which is the part everyone skips when they quote the study The press coverage almost universally reported the during-task result and stopped there. .
Markup
Kosmyna measured EEG differences<SideNote>
MIT Media Lab, 2025. n = 54, four months.
</SideNote> that persisted after the tool was removed. Tags
Five to seven in the whole vocabulary. Past that they stop working as navigation.
Keys
Dashed keycaps. Same border language as everything else.
Toggle the theme with ⌘ J , jump to search with / .
Signals
Status, progress, magnitude. Solid ticks are filled data; empty ticks are the ink-12 ghost behind them.
Status
live · building · paused · shipped · archived. Only 'building' animates - everything else is at rest, which is what makes the moving one mean something.
Meter
The dash motif doing double duty as data. Few ticks read as a rating, many as a bar.
Markup
<Meter value={0.62} label="Retention" readout="0.62" />
<Meter value={3} max={5} ticks={5} label="Confidence" readout="3 / 5" /> Stats
One number worth stopping for. Serif, ticked corners, never more than three in a row.
Checklist
Roadmaps and replication steps. Done items get a drawn tick, not a filled box.
- Ship the dashed pack
- Move footnotes to margin notes above 1300px
- Track navigation on series posts
- Auto-generated OG images at build time satori
Data
Leaderboards, spec lists, changelogs. These are the pages people come back to; a post is read once.
Leaderboard
Rows sort themselves by score. Tick bars make the ranking legible before you read a number, and `self` marks your own entry.
| # | Entry | Score | |
|---|---|---|---|
| 1 | EEGNet-lite | 2.1M params | 0.81 |
| 2 | Ours - ours | 0.4M params | 0.74 |
| 3 | ShallowConvNet | 1.4M params | 0.71 |
| 4 | Riemannian + LR | no training | 0.68 |
Markup
<Leaderboard
caption="bci-bench v0.3 - decoding accuracy, held-out subjects."
rows={[
{ name: 'EEGNet-lite', score: 0.81, note: '2.1M params' },
{ name: 'Ours', score: 0.74, note: '0.4M params', self: true }
]}
/> Spec list
Dot leaders. The spine of a project page: problem, mechanism, demo, install, changelog - each answered in one line.
- Problem
- Commit history is unreadable
- Mechanism
- L-system over the commit graph
- Demo
- Hall of Fame
- Install
- npx git-bonsai
- Source
- github.com/egorthinks
Changelog
Dated build log on a dashed spine. Cheap to write, shows movement, indexes well. Every project page ends with one.
- v1.0
Hall of Fame
Public gallery of bonsai grown from real repositories, with a permalink per tree. - v0.4
Seasons
Commit recency drives foliage; a repository that has gone quiet turns autumn. - v0.1
First tree
One branch per contributor, one leaf per commit. Everything since has been pruning.
Markup
<Changelog title="Build log">
<ChangelogEntry date="2026-07-14" tag="v1.0" title="Hall of Fame">
Public gallery of bonsai grown from real repos.
</ChangelogEntry>
</Changelog> The track
The one place the big connectors earn their size. Stages alternate sides, the loop gives the reader a beat between them, and each connector draws itself as it comes into view.
- 01 ongoing
Read the literature
Cognitive offloading, spaced repetition, metacognitive laziness. Everything that gets cited later starts as a note here. - 02 Q3 2026
Build the bench
A benchmark is the only honest way to argue about a claim. If a result cannot be scored, it does not go on the track. - 03 Q4 2026
Run the experiment
Small n, pre-registered, published whether or not it works. The failure write-ups are the useful ones. - 04 continuous
Publish the numbers
Leaderboard first, essay second. The table is what people come back to.
Markup
Connector shape is per-step, so the rhythm of the track is authored, not automatic. The last step drops its connector.
Markup
<Track title="Research track">
<TrackStep n="01" title="Read the literature" connector="s-loop">…</TrackStep>
<TrackStep n="02" title="Build the bench" connector="s-curve">…</TrackStep>
<TrackStep n="03" title="Publish the numbers" />
</Track> Track navigation
Goes at the foot of every post in a series. Turns scattered posts into something readers finish.
Artifacts
Things that travel without the post around them: widgets, plots, generated images. One of these is worth more than five good essays, so they get their own chrome, their own controls and their own permalink.
Live artifact
Chrome, controls, plot and readouts together. Server-rendered complete, then enhanced - without JS it is still a correct figure at the default settings.
What survives thirty days
Retention decays exponentially. Spaced review moves the whole curve; offloading the encoding moves it the other way.
- You worked it out
- You accepted the diff
- Own, day 7
- 30%
- Own, day 30
- 1%
- Offloaded, day 7
- 4%
- Offloaded, day 30
- 0%
Plot
The drawing area is a normalised 1000x1000 box that CSS stretches to the container, so the curve is responsive; strokes hold their weight through vector-effect and the tick labels are HTML, so they stay readable on a phone instead of shrinking to 6px.
- Measured
- Projected
- Baseline
Markup
<Plot
height={220}
x={{ min: 0, max: 12, ticks: [0, 6, 12], label: 'Week' }}
y={{ min: 0, max: 100, ticks: [0, 50, 100], label: 'Score' }}
series={[
{ name: 'Measured', points: [[0, 12], [12, 71]], style: 'solid', fill: 'hatch-up' },
{ name: 'Projected', points: [[0, 12], [12, 86]], style: 'dashed' },
{ name: 'Baseline', points: [[0, 40], [12, 40]], style: 'hair', muted: true }
]}
/> Hatching
The only fill this palette has. Direction separates categories, spacing and weight separate magnitudes - two series in one figure should never share an angle. These are CSS gradients, so they hold their angle when the plot stretches; an SVG pattern inside the stretched box would shear to 30 degrees at one window width and 50 at another.
Markup
{* anywhere, not just plots *}
<div class="hatch hatch-down hatch-dense" />
<div class="hatch hatch-cross hatch-bold" />
{* as a plot fill *}
series={[{ name: 'Measured', points, style: 'solid', fill: 'hatch-up' }]} Controls
Native inputs underneath, so they work with the keyboard and without JS. The slider track is solid up to the value and dashed past it - value versus the space it could occupy.
Markup
<Slider name="weight" label="Weight" min={0} max={100} value={35} ends={['0', '100']} />
<Segmented name="mode" label="Mode" value="smoothed" options={[
{ value: 'raw', label: 'Raw' },
{ value: 'smoothed', label: 'Smoothed' }
]} />
<Toggle name="projection" label="Show the projection" checked /> Artifact chrome
Wraps anything embeddable. The permalink and the source line are not decoration: an artifact is meant to be screenshotted alone, and it has to survive being separated from the paragraph that introduced it.
bci-bench v0.3
Decoding accuracy on held-out subjects.
| # | Entry | Score | |
|---|---|---|---|
| 1 | EEGNet-lite | 2.1M params | 0.81 |
| 2 | Ours - ours | 0.4M params | 0.74 |
| 3 | Riemannian + LR | no training | 0.68 |
Markup
<Artifact
id="bci-bench"
kind="Benchmark"
title="bci-bench v0.3"
lede="Decoding accuracy on held-out subjects."
source="Four runs per model, median reported."
resettable
>
<div slot="controls">…</div>
<Leaderboard rows={rows} />
</Artifact> Page patterns
How the elements compose. Two layouts carry most of the site.
Workbench - the homepage
Not a chronological feed. One line of thesis, three tracks, the current project's status. The feed moves to /archive/, where it belongs.
I write about staying sharp while AI codes.
Slow, cited, one every six weeks.
How it works inside, and why that way.
200-400 words. These keep the cadence.
Project page - the living document
Problem → mechanism → demo → install → changelog. Posts link here, not the other way round: this page accumulates weight over years while a post dies a week after publication.
git-bonsai
Live v1.0A repository's history, drawn as a tree that grew that way.
- Problem
- Commit history is unreadable
- Mechanism
- L-system over the commit graph
- Demo
- Hall of Fame
- Install
- npx git-bonsai
Open
What this system does not cover yet. Listed here so it stays visible rather than becoming folklore.
- OG images generated at build time from the same tokens satori - needs a dependency
- MDX islands: a spaced-repetition widget, an AST-diff viewer one good widget beats five essays
- /archive/ as the chronological feed, homepage as workbench
- /projects/<slug>/ content collection with a changelog schema
- Series metadata in frontmatter so TrackNav derives itself