phosphor

A design system with one hue. Everything a second colour would normally carry — a link, the current place, progress, weight — is carried by inversion and by how full a run of block characters is.

  • ok hues: 1 (+1 for failure)
  • ok faces: 1 — IBM Plex Mono
  • ok radius: 0, no exceptions
  • ok moving parts: 1 — the cursor

cat one-hue.txt

the constraint

puchkode spent four colours on four jobs: amber for the accent, cyan for links, green and red for state. phosphor spends one. The greys are not grey — they are the accent, starved of chroma, so nothing on the page is off-family.

the consequence

State has to go somewhere. It goes into density: a run of ░▒▓█ that always encodes a number you could otherwise read. Never decoration — if you cannot say what value it shows, it does not ship.

cat tokens/colour.oklch

Every ratio below was measured by converting the OKLCH value to sRGB and computing WCAG contrast, not estimated. worst is the lowest the token reaches on any surface it is painted on; the floor is 4.5:1 for text and 3:1 for a control border.

tokenoklchhexroleon groundworst
ph-00.148 0.008 265#090b0eground — the glass——n/a
ph-10.200 0.006 70#181613surface — code blocks, kbd——n/a
ph-20.250 0.010 70#25211draised — inline code, hovered rows——n/a
ph-30.340 0.014 70#3d3730line — decorative dashes only1.67:11.36:1n/a
ph-40.545 0.028 70#7b6e5fline-strong — frame corners, input underline3.96:13.22:1ok
ph-50.660 0.035 72#a08f7bmuted — dates, metadata, captions6.31:15.13:1ok
ph-60.850 0.032 78#daccb7ink — body text12.45:110.11:1ok
ph-70.950 0.022 85#f5eedeink-bright — headings, strong17.04:113.84:1ok
ph-80.810 0.145 72#fab048amber — accent, prompt, cursor, focus10.68:18.67:1ok
ph-fail0.660 0.170 27#e86156the only off-hue — failure states5.87:14.76:1ok

A link has no colour of its own. It is body ink with a dotted underline, and hover inverts it into a selected terminal cell — amber field, ground text, measuring 10.68:1 either way round. This link demonstrates it.

cat tokens/type.css

One face doing every job, display included. The mono here is the concept — a character grid — not a shorthand for “technical”, which is why puchkode's second display face was retired rather than restyled.

t-display · clamp(2.5rem, 8vw, 6rem) / 0.92 / -0.03em / 700
phosphor
t-h2 · 1.375rem / 2rem / 700 · 22 / 32
## What survives the trip
t-h3 · 1.125rem / 1.5rem / 700 · 18 / 24
### Keeping your classes
t-body · 1rem / 1.5rem / 400 · 16 / 24
It implements the structured clone algorithm, and that algorithm has opinions.
t-code · 0.875rem / 1.375rem / 400 · 14 / 22
const copy = structuredClone(original);
t-small · 0.8125rem / 1.25rem / 400 · 13 / 20
#javascript #browser
t-label · 0.75rem / 1rem / 500 / 0.08em · 12 / 16
NOTE

cat tokens/grid.css

One line is 24px, one column is 1ch. Every space token is a fraction or multiple of the line — if something does not land on it, it is wrong.

space-16px — quarter line — code-head padding
space-212px — half line — gaps inside controls
space-324px — one line — the base unit
space-448px — two lines — before a section
space-596px — four lines — between page regions
density ramps
bar
shade
wave

ls ~/brand

24px
48px
96px

The k is a hole in the path, so whatever is behind the mark shows through it.

ls ~/structure

frame

One sliced SVG: crosses on the corners, dashed runs between them. The corners sit on the edges by construction, so nothing can drift.

frame--strong

The same frame with its dashes stepped up to line-strong. One box per page earns this.

rule

A dash of 0.6ch every 1ch — the character pitch, not an arbitrary dotted border.

ls ~/content

What survives the trip

Anything that crosses a thread boundary as plain data comes through intact, including the cases JSON mangles. Press Ctrl + F and search for => when a clone throws.

Keeping your classes

  • Functions and DOM nodes throw
  • Class instances come back as plain objects
  • Getters are evaluated once and stored as values
money.js
class Money {  static from({ cents, currency }) {    return new Money(cents, currency);  }}// no hue left for syntax — weight and luminance carry itconst price = Money.from(structuredClone(new Money(1999, "EUR"));

Structured clone copies data, not behaviour. If it has methods, it needs a constructor on the other side.

the rule I taped to my monitor
valueJSON round-tripstructuredClonesupport
DatestringDate100%
Map / Set{}Map / Set97%
class instanceplain objectplain object0%
functiondroppedDataCloneError0%

ls ~/actions

Weight is the tag's real share of the archive. A tag cloud usually says this with font size; the grid is fixed here, so density says it instead.

subscribe

ls ~/edge-cases

The states a specimen page usually skips. Everything below is the real component under hostile input, not a mock-up of one.

empty listing

grep: no matches for webassembly —

a quote past the old twenty-line rail

puchkode drew this rail as a literal run of twenty ">" characters in a pseudo-element, which meant a quote longer than twenty lines simply lost its rail partway down with nothing to indicate anything was wrong. This paragraph is deliberately long enough to cross that boundary on a narrow viewport, and the rail beside it is now a masked chevron tiled at the line height, so it continues for as many lines as the quote runs to. The colour still comes from the amber token rather than being baked into the artwork, because the mask carries the shape and the background-color carries the hue. Keep reading and the rail keeps going.

the reason the rail is a mask now
density at the boundaries
0
1
negative
over 1
NaN

ls ~/feedback

  • ok now: writing about AbortSignal
  • waiting reading: the Temporal spec
  • failed build: 1 test failing on main
bar — progress
62%
shade — weight, sub-cell
wave — a series, one datum per cell
posts / month