Design system · v1

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.

Building grows with the site monochrome dashed Astro 5
01

Principles

Five rules. Everything below is a consequence of one of them.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

02

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.

Light
--bg-main · #f2f1ec
--bg-muted · #eae9e1
--text-main · #171717
Dark
--bg-main · #171717
--bg-muted · #242424
--text-main · #f2f1ec
Ink levels
  • --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
03

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 · medium
    I write about staying sharp while AI codes.
  • Section Newsreader · 2xl → 3xl · medium
    What actually moves an audience
  • Label Newsreader · xl → 2xl · italic
    Lab notes
  • Body Inter · base · 1.8 leading
    Short essays, screenshots, occasional honest panic.
  • Small Inter · sm · metadata and captions
    8 min read · updated 14 Jul 2026
  • Eyebrow Inter · 11px · 0.14em tracking
    Reproduce this
04

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.

05

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.

Also redraws on scroll, once per element.
right
down
curve
s-curve
s-loop
elbow
u-turn
branch
merge
zigzag
swoop
nudge

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} />
06

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.

underline
underline-double
circle
bracket-left
check
cross
star
wave
scribble
corner
node
spine

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" />
07

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.

config

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.

terminal
$ npx git-bonsai --repo . --season autumn
  → reading 1,284 commits
  → growing…
  ✓ bonsai.svg written
08

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.

cognition 9 tools 4 lab notes builds

Keys

Dashed keycaps. Same border language as everything else.

Toggle the theme with J , jump to search with / .

09

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.

Live v1.2 Building since Jun 2026 Paused Shipped 24 keepers Archived

Meter

The dash motif doing double duty as data. Few ticks read as a rating, many as a bar.

Retention after 30 days 0.62
Recall with spaced review 0.88
Confidence in the effect 3 / 5
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.

54
Participants
Kosmyna et al., 2025
4 mo
Follow-up
-17%
Recall delta
vs. unassisted cohort

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
10

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
bci-bench v0.3 - decoding accuracy on held-out subjects. Lower parameter counts break ties.
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
Install
npx git-bonsai

Changelog

Dated build log on a dashed spine. Cheap to write, shows movement, indexes well. Every project page ends with one.

  1. v1.0

    Hall of Fame

    Public gallery of bonsai grown from real repositories, with a permalink per tree.
  2. v0.4

    Seasons

    Commit recency drives foliage; a repository that has gone quiet turns autumn.
  3. 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>
11

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.

  1. 01 ongoing

    Read the literature

    Cognitive offloading, spaced repetition, metacognitive laziness. Everything that gets cited later starts as a note here.
  2. 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.
  3. 03 Q4 2026

    Run the experiment

    Small n, pre-registered, published whether or not it works. The failure write-ups are the useful ones.
  4. 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.

12

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.

Widget
#

What survives thirty days

Retention decays exponentially. Spaced review moves the whole curve; offloading the encoding moves it the other way.

2
none 5 reviews
0% 25% 50% 75% 100%
Recall
0 7d 14d 21d 30d
Days since
  • You worked it out
  • You accepted the diff
Own, day 7
30%
Own, day 30
1%
Offloaded, day 7
4%
Offloaded, day 30
0%
Model, not measurement: R = e^(-t/S), with S rising per spaced review. Shapes follow Ebbinghaus and the spacing literature; the offload penalty is a stand-in, not a measured effect size.

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.

0 50 100
Score
0 3 6 9 12
Week
  • 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.

up
down
vertical
horizontal
cross
grid
dots
dense
wide
bold
faint
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.

35
0 100
Mode
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.

Benchmark

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
Four runs per model, median reported. Full protocol and seeds in the repo.
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>
13

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.

Essays

Slow, cited, one every six weeks.

Builds

How it works inside, and why that way.

Lab notes

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.0

A repository's history, drawn as a tree that grew that way.

Problem
Commit history is unreadable
Mechanism
L-system over the commit graph
Install
npx git-bonsai
14

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