The design system for LetsBuild.cloud. Dark only — a terminal doesn't have a
light mode, and neither does this site. Built on the
Rosé Pine palette: deep
violet-black surfaces with six muted pastel inks, in place of v1's saturated
gruvbox primaries. Identity comes from structure: a statusline, an
inverted title row, numbered section rules, a tree TOC. One shared stylesheet
(/theme.css), and one script, on one page — the
arcade.
Principles
- The interface is a terminal application. The chrome is a statusline; the title is a selected row; the index is a dated listing; panels are bordered frames with tab labels. Structure does the theming.
- Dark, always. One mode, tuned properly, instead of two
compromises.
color-scheme: darkkeeps native controls and scrollbars in-theme. - Muted inks on deep ground. Six pastel hues, none saturated, each with a defined job. Body text is never coloured.
- Prose first. Monospace everywhere, but with generous leading, a real measure, and decoration that stays out of the reading column.
- Zero external requests. A script has to earn its place.
System font stacks, CSS-only components, one shared same-origin stylesheet
(
/theme.css). A script never gets behaviour a native element or a CSS pattern can carry; the arcade is the one thing on the site neither of them can do.
Colour
Rosé Pine: three stacked surfaces, three text tones, six hues. Contrast
ratios are measured against --color-bg; hues marked
◆ sit below 4.5:1 and are decorative-only — borders,
fills, and large glyphs, never body-size text.
Surfaces
Text & interactive
| Token | Ink | Contrast | Role |
|---|---|---|---|
--color-text |
#E0DEF4 | 13.4:1 | Body copy, headings |
--color-subtle |
#908CAA | 5.5:1 | Descriptions, labels, quotes, secondary text |
--color-muted ◆ |
#6E6A86 | 3.4:1 | Tree branches, separators, placeholders — never sentences |
--color-accent |
#9CCFD8 | 10.4:1 | Links, focus rings, primary actions (= foam) |
--color-accent-2 |
#C4A7E7 | 8.4:1 | Form accents, h3 sidebar (= iris) |
Scroll the table sideways for the rest of the columns.
Hues
| Token | Ink | Contrast | Assigned to |
|---|---|---|---|
--c-love |
#EB6F92 | 6.1:1 | Danger buttons, warnings, "don't" panels |
--c-gold |
#F6C177 | 10.8:1 | Section numbers, dates, tips, badges, strings |
--c-rose |
#EBBCBA | 10.5:1 | Title-block fill, link hover, emphasis |
--c-pine ◆ |
#31748F | 3.4:1 | Decorative rules and fills only |
--c-foam |
#9CCFD8 | 10.4:1 | Links, primary buttons, notes, functions in code |
--c-iris |
#C4A7E7 | 8.4:1 | h3 sidebar, form controls, keywords in code |
Scroll the table sideways for the rest of the columns.
Usage rules
- Body text is
--color-textor--color-subtle. Nothing else colours a paragraph. - One link colour, sitewide: foam, hovering to rose.
- Each hue keeps one job. If a new element needs colour, it reuses the hue whose job it matches — it does not get a new one.
- Surfaces stack in order: bg → surface → overlay. Never skip a level for contrast tricks.
- Never hard-code a hex in a rule; always go through a token.
Typography
One family — monospace, everywhere. System-installed developer fonts, no downloads, so the site renders in the reader's own terminal font.
--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono",
Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
Scale
Seven steps from a 16px base. Monospace runs wide, so the scale is tight.
Weight, leading, tracking
| Token | Value | Use |
|---|---|---|
--weight-normal | 400 | Body copy |
--weight-medium | 600 | Buttons, active tabs, post titles |
--weight-bold | 700 | Headings, statusline segments, badges |
--leading-normal | 1.7 | Body — generous, because monospace is dense |
--leading-tight | 1.25 | Headings, tooltips, book metadata |
--tracking-tight | -0.01em | The h1 title row |
--tracking-wide | 0.08em | All small-caps labels (h2, h4, tags, table headers) |
Scroll the table sideways for the rest of the columns.
Hierarchy is typographic: the h1 is an inverted block, h2s are
numbered small-caps with a rule filling the line, h3s carry an iris sidebar,
h4s are small-caps labels. Uppercase + --tracking-wide is the
signature for anything label-like.
When a caption wants that look but isn't a section, use .label on
a paragraph instead of reaching for an h4. Heading levels are the document
outline. A repeated card caption padding it out — ten alternating
do and don't in the section below — is noise a screen reader
has to walk past to find the real structure. Levels also have to descend one
at a time: an h2 followed by an h4 leaves a hole in the outline.
Spacing & layout
Spacing scale
Six steps. Pick from the scale; never invent an in-between value.
--space-1/2— inside components: label gaps, chip padding.--space-3/4— between elements: paragraphs, card padding, rows.--space-5/6— between ideas: subsections and sections.
Measure
The reading column is --layout-width: 760px — about 76 monospace
characters. One column and no sidebars, unless a page opts into wide mode
below. Horizontal padding
--layout-pad-x: 1.5rem, vertical --layout-pad-y: 2.5rem.
Wide content (code, tables) scrolls inside its own container; the page never
scrolls sideways.
Wide mode
Not every page is prose. A quick reference gets skimmed: you arrive looking for one thing, find it, and leave. Stacked in a 760px column, fifteen short sections turn into several screens of scroll with half the window unused.
.wide on the <body> moves the page cap to
--layout-width-wide: 1272px, but only once the window is wide
enough for two panes — under that the page stays on the measure, because a
single 1175px pane of prose is worse than the 760px it started from. Nothing
else changes — same
statusline, same title row, same prompt — so a wide page still reads as the
same site. Two rules come with it.
Prose goes back on the measure. A paragraph at 1200px runs
past 120 characters and stops being readable. .measure caps a
single block at --layout-width: the intro, an aside, anything
meant to be read in sentences.
Sections tile. .panes lays the page's own
sections side by side. Each pane is a <section>, and its
h2 already draws the top edge — gold number, small caps, a rule
filling the line. A row of those reads as a tiled terminal, and it takes no
new border, glyph or hue to say so.
The track floor is 36rem, and it sits that high on purpose. Much
under that a pane is too narrow to hold a code block at all, so
auto-fit drops back to one column instead of squeezing. Panes
align to start, so a short section keeps its own height rather
than stretching to match a tall neighbour; the ragged bottom edge above is
what that trade looks like.
Panes read in DOM order, left to right and then down, which is the order the
h2 counter numbers them in. Multi-column
(columns) would flow down one column and back up to the top of
the next — right for an index, wrong for fifteen numbered sections. The
quick reference uses both, and that's the split: columns for its
table of contents, .panes for its body.
Wide mode belongs to static/. Posts render through
template.html, which owns the <body> tag and
is prose by definition. The Rust quick
reference is the live example.
Components
The full set. Anything interactive here is styled native HTML, and every component but one is CSS all the way down — nothing to hydrate. The exception, the arcade, is a game.
Statusline
The site's chrome, top and bottom of every page: solid segments in the vim mould. Foam segment for identity, overlay segments for context, surface fill between. Segments can be links. Every page header carries the same pattern: identity links home, and the current page's filename sits in a surface segment after the fill.
The bar wraps rather than clips. Segments are nowrap, so a bar
with more content than room used to hide its tail behind
overflow: hidden — the page width stayed correct and the last
links were unreachable. Segments now drop to a second row instead.
The nav row
When a bar carries links as well as status, give it a second line. Add
.statusline-stack to the bar and wrap each line in
.statusline-row; the row draws the surface tone itself, so a
stacked bar needs no trailing .fill. Both the header and the
footer are built this way — status above, links below.
The second row is the command line.
.statusline-nav drops the fill tone, the foam block and the
segment chips, leaving a foam : prompt and plain subtle links
that go foam and underline on hover. Two rows of solid segments read as
equals, and the row naming the page should win. It also drops the segment
padding and spaces itself with a gap, so the : lands on the
page's left rail — the same column as the ❯ prompt, the
h1 block and every line of prose.
The real thing is a <nav> with an aria-label:
a page with two of them needs to say which is which, so the header's is
Primary and the footer's is Footer. The specimens below are
plain divs — a landmark that exists to be looked at is one more
thing for a screen reader to walk past.
Segments that don't fit
A link gets its own row before it gets truncated. The footer
colophon used to wear a modifier that truncated it to fit alongside the links:
at 1280px it showed 308 of its 449 pixels. You can't tell where a link goes
when its name is cut off. It now sits on the status row with
the links below it, and .seg-note lets it wrap inside its own box
instead: prose in a bar of nowrap segments, read in full at
320px.
.seg-shrink is what's left for the case that genuinely can't
wrap: a one-token label like a filename. It keeps its natural width until the
row is tight, then yields to an ellipsis, and it's hidden outright below 640px
where a label truncated to a sliver is only noise. Never put it on a link or
anything else focusable.
A truncating segment must carry a title holding the same
text. The ellipsis is a CSS effect, so the string stays whole in the
accessibility tree and a screen reader reads all of it. The person who loses
information is the sighted mouse user, and on a page with no JavaScript
title is the only hover affordance there is.
cursor: help signals it. title never reaches a
keyboard or touch user, though, which is why the rule is to keep the modifier
off anything clickable rather than to paper over one with a tooltip.
Prompt
The interactive signature: foam ❯, subtle text, blinking block
cursor. Used once per page at most — under the title. The blink respects
prefers-reduced-motion.
cat 2026-07-01-static-sites.md
Title row
Page titles render as a selected row — rose fill, ground-coloured text, hugging its content width. Nothing else on the site is inverted at this scale, which is what makes it the title.
Buttons
- Primary — foam fill, one per view. Hover shifts to rose.
- Default — outlined; border and text turn foam on hover.
- Danger — love outline, filling on hover. Destructive only.
- Ghost — bracketed text action, for inline or tertiary use.
Tags, badges & keycaps
Tags are outlined and quiet; badges are filled and loud — use badges sparingly. Keycaps get an overlay fill with a thick bottom edge.
Tags also work as links. Wrap a row of them in .tag-list (the
topic row at the head of a post) or .tag-cloud (the tag index),
both flex-wrap containers. A linked chip outlines in rose on hover, following
the sitewide link rule. .tag-count puts a gold post count inside
the chip — numbers are gold everywhere.
The chips don't have to be links. A row that only names things — the riders
a page is following, the teams it covers — is a <ul>, one
chip per <li>, with a .label above it and
aria-labelledby pointing back at that label. Match the element to
the job: linked chips are navigation and take a <nav>;
unlinked chips are a list and take a list. Ten names inside a bare
<div> reach a screen reader as one unbroken run of text,
because a span is not an object the way a link is.
Watching
.who adds a nationality to a name chip: a country code in a
.nat span, tinted by a .hue-* class on the chip.
It's a flag drawn as three letters, which is what a site with no external
requests and no images in its chrome can afford. The hue lands on the code it
repeats, so a reader who can't tell iris from rose loses nothing — and
because it's text, --c-pine and --color-muted stay
out of this set.
There's no rail on a .who, and that's deliberate.
.fixture already draws a 3px --hue rail meaning
which sport this row belongs to, and these chips sit inside one; the
same mark in the same variable would have meant nationality a few hundred
pixels from where it meant sport. Put the space between the code and the name
in the markup too — the gap is a margin, and a margin isn't in the
accessibility tree.
Watching
A chip whose label already names its country doesn't need a code, and isn't a
.who — it's a plain .tag. Reach for the code when
the name alone doesn't say where someone is from.
Following
Cards
A surface panel with its label breaking the top border — the CSS equivalent
of a ┌─ title ─┐ box. Accent variants recolour the tab and
border together.
Watch mode now live-reloads the browser on save. Goodreads shelf data is cached for ten minutes.
The Mythical Man-Month
Frederick P. Brooks Jr.
Reading shelves
The homepage reading block: one card per Goodreads shelf, each tinted by a
.shelf-gold, .shelf-foam or .shelf-iris
modifier so the three groups read as three groups. The hue means
which group here rather than its usual job — see
hue carriers for the rule that allows it. The card title
always says the same thing, so nothing rides on colour alone.
.book-glyph is a book drawn in CSS: a coloured spine down the
left edge and two rules for the title on the cover, varying by position so a
shelf isn't one book repeated. It replaced a hot-linked Goodreads cover that
ran up to 750 KB for a 26px slot. It's decoration, so it's
aria-hidden — the title and author sit beside it as text.
.reading .card-grid aligns to start so a one-book
shelf doesn't stretch to the height of a three-book one.
Callouts
Blockquote
Programs must be written for people to read, and only incidentally for machines to execute. Abelson & Sussman, SICP
Code
One code component, everywhere. The SSG's own tiny tokenizer
(highlight.go — rust, go, python, shell, yaml, js; anything else
stays plain) renders every block at build time, whether it's fenced in a post
or sourced from a static page. They all come out as the same line-numbered
pre.code rows: muted gutter, overlay row hover, surface fill,
hairline border. An optional title bar names the file and language. Inline
code gets the same treatment at 0.9em. Syntax inks, one job per
hue: keywords iris; functions and types foam; strings, macros and flags
gold; numbers and lifetimes rose; comments subtle italic — never
muted, which fails AA at code size.
1// Render a post through the single template.2func render(post Post) string {3 html := markdown.ToHTML(post.Body, 0)4 return strings.Replace(tmpl, "{{content}}", html, 1)5}
Tables
| Command | Effect |
|---|---|
./ssg | Generate the site into ./build |
./ssg --watch | Serve with live reload |
go test ./... | Run the test suite |
Rows highlight on hover with a surface fill; headers are small-caps labels.
A table too wide for a phone goes in a .scroll-x wrapper, which
scrolls it sideways instead of widening the page. A wrapper that actually
scrolls also needs tabindex="0", role="region" and a
name, unless focusable content spans its scrollable width. No browser makes a
bare overflow box focusable, so without those a keyboard user reaches the
first column and nothing else. Links in a frozen first column don't count — the
season grid's row headers are links and every one of them is already on
screen.
Give it a .scroll-hint too. A scroller can end flush with a
column boundary and read as a finished table: the season grid showed July
to September at 390px, stopped exactly on September's edge, and lost the rest
of the year with nothing on screen to say so.
Two rules come with it. The width where a table stops fitting is a fact
about that table, so theme.css hides the hint and the page
reveals it — work out your table's width and put the one media query beside
it. Work it out rather than measure it where you can: a measured breakpoint
holds only for the font it was measured in, and this stack starts with three
fonts a visitor has to install. The season grid states its own width in
--season-label and --season-month, so its number is
a sum. And word the hint so it's true at every width it shows at: naming the
columns that are off to the right is tempting and wrong, because which ones those
are changes as the viewport does, and without JavaScript the page ends up
telling you to scroll for a month that never left the screen.
Forms
Inputs sit on the surface colour with strong borders; focus swaps the border
to foam and doubles it with a ring. Checks, radios and ranges use
accent-color: var(--c-iris) — native controls, themed without
extra markup.
Breadcrumbs & pagination
Tabs
The active tab joins the page: bordered on three sides, ground-filled, sitting on top of the rail. CSS only — each tab is a link to its page.
Progress
Segmented fills — terminal block characters, drawn with a repeating gradient.
Tooltip
The generator supports live reload in watch mode.
Pure CSS: content lives in data-tip, shown on hover and on
keyboard focus. For glossary-grade asides only — anything important belongs
in the prose.
The bubble wraps at min(32ch, 60vw) and is hidden with
display, not opacity alone. An invisible box still occupies the
scrollable overflow, so a tooltip near the right edge used to widen the whole
page on a phone; transition-behavior: allow-discrete keeps the
fade regardless.
Disclosure
Why so little JavaScript?
Every behaviour on the site — theme, tooltips, disclosure, navigation — has a native or CSS-only equivalent, and uses it. That leaves one script to break and one to audit.
Post index
The homepage lists posts by date: gold ISO dates, foam titles, subtle one-line descriptions, dashed hairlines between entries. No glyph prefixes — the date column and separators carry the structure.
- 2026-07-01Building a static site generator in Go
What it takes to replace a framework with the standard library.
- 2026-06-14Live reload without the toolchain
File watching and server-sent events, standard library only.
- 2026-05-02Terminal aesthetics on the web
Monospace, box glyphs, and knowing when to stop.
Fixture list
The post index's counterpart for events that haven't happened yet — a race calendar, a season, a run of fixtures. A row gives the date, the event, and one subtle line holding the place and why it matters. Dates stay gold here, the same as on the post index.
-
22 Aug – 13 Sep
Vuelta a España
SpainThree weeks and the last Grand Tour of the year.
-
10 Oct
Il Lombardia
ItalyThe race of the falling leaves, and the fifth Monument.
A row can hang a .fixture-who under its note — a
.tag-list of .who chips naming who to watch at that
one. It's a third row of the same grid, so it stays on the note's side of the
date column and folds with it below 640px.
The rail down the left takes --hue. Put a .hue-*
class on the list when every row is the same kind, or on the rows themselves
when one list mixes them — a calendar covering six sports gives each sport its
own rail colour and stays one list. A mixed list has to name the kind in the
row as well; .fixture-what .tag is styled for exactly that chip.
Below 640px the date column folds away and each row stacks; the fixed column
is wider than a phone screen.
Season grid
Where a season's weight sits: a row per series, a column per month, the
number of events in the cell. The numbers are the content: the table reads in
order and adds up out loud. Everything else in the cell is decoration in the
row's hue — a column sized by .bar-1 to .bar-5, one
step of height per event, and a wash behind it at the same weight. Every cell
in a row shares a baseline, so the grid reads as a bar chart rather than as a
set of unrelated marks. You apply the class by hand, so keep it in step with
the printed number. A cell with nothing in it drops the bar and prints a
subtle zero.
The scale is proportional rather than banded, which only holds while the counts stay small — five steps is a tall enough column already. A grid with counts in the twenties needs a different encoding.
The columns are fixed, not sized by their contents: the grid is
--season-label wide plus --season-months at
--season-month each, and comes out that wide in any monospace.
A grid with a different number of months sets --season-months,
as the four-month demo below does. That's what makes the scroll hint's
breakpoint a sum instead of a measurement. Measured, it came out a fraction
of a pixel wider here than on CI, and the hint fired a column early.
The label goes in a .label above the scroller, not in a
<caption>. A caption is a child of the table, so it takes
the table's width and slides off the screen with it: the table is allowed to
scroll, its name isn't. Point the table at the label with
aria-labelledby.
Events, by sport and month
| Sport | Aug | Sep | Oct | Nov |
|---|---|---|---|---|
| Cycling | 3 | 5 | 2 | 0 |
| Formula 1 | 1 | 2 | 3 | 2 |
Scroll the table sideways for the rest of it.
Arcade
A character grid in a pre, with a statusline under it for a HUD.
The home page runs a side-scrolling shooter in one: /game.js
writes glyphs into the grid and wraps each run of them in one of the
.a-* inks below. It is the only component on the site that needs
a script, and the only place a hue does a job that isn't its sitewide one —
so each game job maps onto a sitewide one instead of inventing a colour. Foam
is you, love is what's shooting at you, gold is the shot and the score, iris
the second attacker, rose the explosion, muted the starfield behind all of
it, and subtle the two things the panel ever writes in words: the hint and
the game-over line. Each entity has its own glyph as well, so nothing here
rests on colour alone.
A miniature, held to about a phone's width: eight rows, and cut to
twenty-four columns so it fits this page at any width. The live panel
measures its own width and fills it — about thirty columns on a phone,
nearer ninety on a desktop. The grid
states its height in CSS rather than taking it from its contents.
--arcade-rows on the panel is what the script reads to size the
world, and it is why the panel is its full size before a frame has been
drawn — eleven rows on a desktop, eight below 640px. It measures the width at
run time, in the visitor's own monospace, and fills it. Nothing moves in CSS
here: the animation is which character sits in which cell.
The home page ships a still frame eleven rows deep, drawn by the generator,
and the two buttons at the end of the HUD are in the markup from the start —
a noscript rule in the head takes them away again on a page that
can't run them, which keeps a dead control off that page without the HUD
reflowing on every other one. play hands the visitor the
ship, pause stops the panel moving at all. The visitor asks
for the keyboard, and can take it back — see
motion & accessibility.
Hue carriers
.hue-gold, .hue-foam, .hue-iris,
.hue-rose, .hue-love and .hue-pine set
--hue on a container. They mark a group. They don't give the hue
a second job. Two rules keep that true. The hue may only repeat a label the
component already prints — the shelf title, the sport a row belongs to — so
nothing is legible by colour alone. And it may only reach a border, a rail or
a fill, never text, which is what lets pine into the set at all.
The reading shelves use the same variable.
Footer
The statusline mirrored at the page end, stacked the same way: the colophon on
the status row, the way back on the nav row below it. Every footer links home
and ↑ top. Where the header nav points at the content — posts,
tags, about — the footer nav points at the way out: the feed, this guide, the
top of the page.
Motion & accessibility
- Motion budget: one animation. The prompt cursor blink,
removed under
prefers-reduced-motion. Everything else is a 0.15s colour/border transition; nothing moves position. - The arcade asks first, and stops when told. A panel that
animates by itself is what
prefers-reduced-motionis about, so under it the arcade holds its still frame until the visitor presses play. It also stops while off-screen or in a background tab, and its pause button halts it outright — SC 2.2.2 asks for that of anything that starts on its own and runs past five seconds. - Nothing takes the keyboard uninvited. The arcade reads
the arrow keys only after a click or the play button, so someone scrolling
past it never finds the page stuck. Escape, Tab, the stop button, a click
elsewhere and scrolling the panel out of view all hand them back, and Enter
stays with the focused button rather than being spent on the gun. The grid
churns every frame and says nothing worth reading aloud, so it is
aria-hiddenand the buttons carry the names. - Contrast. Text tones and link colours clear WCAG AA at body size (most clear AAA). Pine and muted are locked to decorative roles — the ◆ flags in the colour tables are the contract.
- Colour is never the only signal. Links underline on hover, focus draws a 2px foam ring, active tabs change weight and border, callouts carry text labels.
- Semantics. One h1, ordered heading levels, native
controls (
button,details,progressroles), realnavlandmarks. Decoration lives in CSS generated content, kept out of the markup. - Keyboard. Everything interactive is a native focusable element; tooltips open on focus, not just hover.
Voice & tone
The theme frames the words; style.md governs them: concise,
approachable, informal but professional. Active voice, no weasel words, no
clichés. Short paragraphs, concrete examples, code where code explains
better than prose. The design should feel like the writing.
Token reference
The complete set — every value the theme uses, grouped by concern. The live
copy is the :root block in /theme.css,
the single stylesheet every page on the site (including this one) links.
Change a token there and it changes everywhere at once.
Colour
| Token | Value | Role |
|---|---|---|
--color-bg | #191724 | Page ground |
--color-surface | #1F1D2E | Cards, code, callouts, row hover |
--color-overlay | #26233A | Statusline, keycaps, tooltips, selection |
--color-border | #403D52 | Hairlines |
--color-border-strong | #524F67 | Emphasised edges, table header rule |
--color-text | #E0DEF4 | Body, headings |
--color-subtle | #908CAA | Secondary text |
--color-muted | #6E6A86 | Decorative text (branches, separators) |
--color-accent | #9CCFD8 | Links, focus, primary actions |
--color-accent-2 | #C4A7E7 | Secondary interactive |
--c-love | #EB6F92 | Danger / warning |
--c-gold | #F6C177 | Numbers, dates, tips, strings |
--c-rose | #EBBCBA | Title fill, hover, emphasis |
--c-pine | #31748F | Decorative fills |
--c-foam | #9CCFD8 | = accent |
--c-iris | #C4A7E7 | = accent-2 |
Scroll the table sideways for the rest of the columns.
Typography
| Token | Value | Role |
|---|---|---|
--font-mono | JetBrains Mono → … → monospace | The only family |
--text-xs | 0.8rem | Labels, statusline, tags, captions |
--text-sm | 0.9rem | Buttons, code, tables, trees |
--text-base | 1rem | Body |
--text-lg | 1.2rem | h2 / h3 |
--text-xl | 1.4rem | Display |
--text-2xl | 1.75rem | Section numbers |
--text-3xl | 2.25rem | h1 title row |
--leading-tight | 1.25 | Headings, tooltips |
--leading-normal | 1.7 | Body |
--tracking-tight | -0.01em | h1 |
--tracking-wide | 0.08em | Small-caps labels |
--weight-normal | 400 | Body |
--weight-medium | 600 | Buttons, active states |
--weight-bold | 700 | Headings, segments, badges |
Scroll the table sideways for the rest of the columns.
Spacing & layout
| Token | Value | Role |
|---|---|---|
--space-1 … --space-6 | 0.25 / 0.5 / 1 / 1.5 / 2.5 / 4 rem | The only permitted gaps |
--layout-width | 760px | Reading measure (~76ch) |
--layout-width-wide | 1272px | Page cap under .wide — two panes |
--layout-pad-x | 1.5rem | Column side padding |
--layout-pad-y | 2.5rem | Page top/bottom padding |
Scroll the table sideways for the rest of the columns.
Borders & motion
| Token | Value | Role |
|---|---|---|
--border-width | 1px | All hairlines |
--bar-width | 3px | Accent sidebars (h3, callouts, do/don't) |
--radius | 0px | Square corners, always |
--transition | 0.15s ease | Colour/border transitions |
Scroll the table sideways for the rest of the columns.
Do & don't
Do
Reach for a token. Every colour, size and gap in a rule is a var().
Don't
Hard-code a hex or a magic number — it silently forks the system.
Do
Build hierarchy from structure: inversion, numbering, rules, sidebars, small caps.
Don't
Prefix headings or list items with ornamental glyphs — that was v1's tell.
Do
Keep each hue on its one job, and reuse it when a new element matches that job.
Don't
Colour prose, add a second link colour, or use pine/muted for body-size text.
Do
Use native elements and CSS-only patterns for every behaviour they can carry.
Don't
Reach for a script where an element would do, or add webfonts, CDN assets, or any request that isn't the page.
Do
Let wide content scroll inside its own container.
Don't
Run prose past the measure, or let the page scroll sideways.
Do
Reach for .wide when a page is scanned rather than read, and tile its sections with .panes.
Don't
Widen a page of paragraphs, or leave prose at full width inside a wide one.