theme v2 — night terminal
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. Zero JavaScript, one shared stylesheet
(/theme.css).
color-scheme: dark keeps native controls and
scrollbars in-theme./theme.css).
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.
| 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) |
| 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 |
--color-text or --color-subtle. Nothing else colours a paragraph.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;
Seven steps from a 16px base. Monospace runs wide, so the scale is tight.
| 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) |
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.
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.
The reading column is --layout-width: 760px — about 76 monospace
characters. One column, no sidebars, no grid. 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.
The full set, all CSS-only. Anything interactive here is styled native HTML — no scripts to load, nothing to hydrate.
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.
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.
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.
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
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.
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.
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.
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. This is the one place a hue
means which group rather than its usual job. 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.
Programs must be written for people to read, and only incidentally for machines to execute. Abelson & Sussman, SICP
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}
| 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.
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.
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.
Segmented fills — terminal block characters, drawn with a repeating gradient.
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.
Every behaviour on the site — theme, tooltips, disclosure, navigation — has a native or CSS-only equivalent. Shipping zero scripts means nothing to break and nothing to audit.
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.
What it takes to replace a framework with the standard library.
File watching and server-sent events, standard library only.
Monospace, box glyphs, and knowing when to stop.
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.
prefers-reduced-motion. Everything else is a
0.15s colour/border transition; nothing moves position.button, details, progress
roles), real nav landmarks. Decoration lives in CSS generated
content, kept out of the markup.
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.
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.
| 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 |
| 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 |
| 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-pad-x | 1.5rem | Column side padding |
--layout-pad-y | 2.5rem | Page top/bottom padding |
| 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 |
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.
Don't
Add JavaScript, webfonts, CDN assets, or any request that isn't the page.
Do
Let wide content scroll inside its own container.
Don't
Break the 760px measure or let the page scroll sideways.