The Dispatch Design Bible
A complete, self-contained specification of the Dispatch design system — a press-room interface language. This document is written to be handed to a builder (human or Claude Code) with no other context: everything needed to build new screens, components, or whole applications in this system is codified here. Copy the tokens and recipes verbatim; internalize the principles for everything they don't cover.
0. Identity
Dispatch's interface is a small-town newspaper press room. The metaphor is not decoration — it is the information architecture, the typography, the motion design, and the emotional model:
- The consequential act (publishing, sending, committing) is running a press: mechanical, slightly dangerous, irreversible once the cylinders turn. The UI carries that weight — calm, physical, matter-of-fact.
- The aesthetic is print itself, not screens imitating print: paper and ink tones, hairline rules, one accent the color of vermillion press ink, and a total absence of glossy SaaS furniture.
- The interface is quiet so the work can be loud. Stillness, warm paper, typographic hierarchy — then one blinking vermillion dot means the machine is running.
The one test. Before adding any element, ask: would it make sense taped to the wall of a 1960s print shop? An ink-coverage progress track passes. A toast with a green checkmark circle does not. A mechanical counter passes. A skeleton shimmer does not.
1. Color
Two themes switched by a data-theme attribute on the root element. Dark
is the default (the shop at night). Use these tokens exactly:
[data-theme="dark"] {
--paper:#171512; /* the page. warm near-black — never pure #000 */
--ink:#ece7dd; /* text & structure. warm off-white — never pure #fff */
--sub:#9b958a; /* secondary text */
--tert:#716b60; /* tertiary: placeholders, faint metadata */
--rule:#2e2b26; /* hairline rules between rows */
--fill:#26231e; /* recessed fills: progress tracks, disabled buttons */
--hovfill:#221f1b; /* row-hover wash */
--inbord:#3a362f; /* input borders */
--insurf:#211e1a; /* input surfaces */
--acc:#d65f35; /* vermillion press ink — THE accent */
--acchov:#e57547; /* accent hover */
}
[data-theme="light"] {
--paper:#faf8f4; --ink:#1c1a17; --sub:#6f6a63; --tert:#a39d93;
--rule:#e3ded6; --fill:#eeeae2; --hovfill:#f4f0e8; --inbord:#d8d2c6;
--insurf:#ffffff; --acc:#c14a24; --acchov:#a83d1c;
}
The material rule
There are exactly two materials: paper (the background family) and ink (the text/structure family). Every gray is warm — it carries a brown/yellow undertone. Introducing a cool gray, a pure black, or a pure white breaks the paper illusion instantly. If you need a new shade, derive it by mixing paper and ink, never by reaching for a neutral hex.
The accent rule
Vermillion is punctuation, never prose:
It marks live state (a blinking "on press" dot, an active tab's underline, an alarming status word), the single consequential action in a button pair, and one signature dot in the wordmark.
It is never a large fill, never a background, never decoration.
If a screen shows vermillion in more than two or three places, remove some.
Light theme darkens it (
#c14a24) to hold contrast on cream paper — the accent adapts, the meaning doesn't.Selection is the reader's ink. Highlighting text pulls a vermillion proof of it:
::selection { background:var(--acc); color:var(--paper); }The reader's own mark is transient and never counts against the accent budget — ink the shop lays down is rationed; ink the reader lays down is theirs.
Status without traffic lights
There is no green, no red, no yellow. Status is expressed with the two materials plus vermillion:
| State kind | Treatment |
|---|---|
| Done / confirmed | ink, prefixed with a typed ✓ |
| Staged / pending (valid, awaiting the act) | plain ink mono, no glyph — the ✓ is earned by the act, not the plan |
| Inactive / draft | --sub or --tert |
| Live / running | --acc, optionally with the blinking dot |
| Alarming / quarantined | --acc, uppercase mono |
| Failure text | plain ink prose that says what happened — the words alarm, not the color |
Success is stated, not colored.
2. Typography
Three families, three jobs. The system's texture comes from never blurring them.
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400..700;1,6..72,400..700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
Self-hosting (e.g. next/font) instead of the CDN link: the Newsreader
subset MUST include latin-ext (or explicitly U+0131) — the dotless ı is
not in the base latin subset, and without it the tittle's base glyph
silently falls back to another face. Keep the optical-sizing axis
(opsz 6..72) in the bundle; a single-instance cut flattens the headline
voice.
| Family | Voice | Used for |
|---|---|---|
| Newsreader (serif, optical sizing) | The newspaper — headlines, pride | Wordmark, page titles, section heads, item titles, empty-state italics, dialog titles |
| IBM Plex Mono | The machinery — anything a machine produced or will consume | Numbers, counts, dates, timestamps, addresses, IDs, statuses, labels, buttons, table headers, input text |
| Helvetica Neue (system fallback: Helvetica, Arial) | The workhorse — plain prose | Body copy, form flow, descriptions |
The sorting rule (memorize this): if a value came from a machine — a count, an address, a timestamp, a status — it is mono. If it's a headline or a name the shop is proud of, it is serif. Everything else is Helvetica. Applying this one rule generates most of the system's look.
Among machine values, rank by role: the value that is the row's subject
(the email in a subscriber ledger) is 13px ink mono; machine values that
are that row's metadata — timestamps, ids riding alongside the
subject — take .meta12.
Scale
#root { font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;
font-size:15px; line-height:1.5; background:var(--paper); color:var(--ink); }
.serif { font-family:'Newsreader',Georgia,serif; }
.mono { font-family:'IBM Plex Mono',monospace; }
h1.page { font-family:'Newsreader',Georgia,serif; font-size:36px; font-weight:700;
letter-spacing:-0.01em; margin:0 0 24px; }
h2.sect { font-family:'Newsreader',Georgia,serif; font-size:24px; font-weight:600; margin:0; }
h3.sub { font-family:'Newsreader',Georgia,serif; font-size:22px; font-weight:600; margin:0 0 4px; }
.kicker { font-family:'IBM Plex Mono',monospace; font-size:11px;
letter-spacing:0.22em; text-transform:uppercase; color:var(--sub); }
.label10 { font-family:'IBM Plex Mono',monospace; font-size:10px;
letter-spacing:0.18em; text-transform:uppercase; color:var(--sub); }
.meta12 { font-family:'IBM Plex Mono',monospace; font-size:12px; color:var(--sub); }
.state { font-family:'IBM Plex Mono',monospace; font-size:11px; font-weight:600;
letter-spacing:0.08em; text-transform:uppercase; }
Rules:
- Big display numbers (KPIs) are serif, 38–44px, weight 600,
letter-spacing:-0.02em— a number the shop is proud of is a headline. - Mono labels are uppercase and letterspaced; the smaller the type, the wider the tracking (10px → 0.18em, 11px → 0.22em on kickers, 0.08em on 11px status words).
- Headlines are tracked slightly tight (−0.01 to −0.02em). Never letter-space the serif beyond that; never uppercase it.
- Weights live in 400–700. Nothing thinner, nothing heavier.
- Links are ink with a 1px underline offset 3px, turning accent on hover.
3. The wordmark and the tittle
The signature: the product wordmark is set in Newsreader 700 with a dotless ı, and the dot (tittle) is drawn as a vermillion disc — the one drop of press ink in the chrome.
<div class="wordmark">D<span class="tittle">ı</span>spatch</div>
.wordmark { color:var(--ink); font-family:'Newsreader',Georgia,serif;
font-size:40px; font-weight:700; letter-spacing:-0.02em; line-height:1; }
/* Geometry pixel-measured against Newsreader 700's real tittle
(canvas-diff of "i" vs "ı"). Requires line-height:1 on the wordmark. */
.tittle { position:relative; }
.tittle::after { content:''; position:absolute; left:0.073em; top:-0.007em;
width:0.175em; height:0.167em; border-radius:9999px; background:var(--acc); }
- The geometry is measured, not eyeballed — it sits exactly where Newsreader's own tittle would. Do not adjust it by feel. (Technique, if you ever re-derive it for another face: render "i" and "ı" at 1000px on two canvases, pixel-diff a restricted y-range above the x-height, convert the bounding box to em.)
- Works at any size because it's all em-relative — the same markup serves a
40px masthead and a 64px login card. Those are the two canonical sizes;
others derive freely from the same markup, changing only
font-size.letter-spacing:-0.02emholds at every size — the geometry and the tracking scale together or the dot drifts. - Any animation of the tittle must be transform-only so the resting geometry stays exact (see §7).
4. Structure & layout
Broadsheet, not cards. Content is one column on the paper, separated by horizontal rules — never by boxes, panels, or shadows.
* { box-sizing:border-box; }
body { margin:0; }
.shell { border-top:6px solid var(--ink); } /* the masthead bar */
.wrap { max-width:1180px; margin:0 auto; padding:20px 40px 0; }
main.wrap { padding:36px 40px 80px; }
The rule hierarchy
Rules (lines) are the entire structural vocabulary. Three weights, used strictly:
| Weight | Color | Means |
|---|---|---|
| 6px | --ink |
The top of the page (and the top of a dialog card) — the masthead bar. Once per surface. |
| 2px | --ink |
A major section begins: under the masthead title row, on top of a table header, starting a form section, the top of a stat strip. |
| 1px | --rule |
Hairlines between repeating rows and minor separations. |
Corners: 2px radius on inputs and buttons, 0 everywhere else. The only circles in the system are the press dot and the tittle.
Shadows: none — with one deliberate print artifact allowed: a hard offset
"paper stack" shadow (box-shadow:4px 4px 0 var(--fill)) under a document
preview, and a dim backdrop behind dialogs. Never a blur.
Tables are ledgers
.thead { display:grid; gap:16px; padding:0 0 10px; border-bottom:2px solid var(--ink);
font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:0.16em;
text-transform:uppercase; color:var(--sub); }
.row { display:grid; gap:16px; align-items:baseline;
padding:14px 0; border-bottom:1px solid var(--rule); }
.row.live { cursor:pointer; }
.row.live:hover { background:var(--hovfill); }
.right { text-align:right; }
- CSS-grid rows with hairline separators and generous vertical padding (13–16px). Explicit column templates per table.
- Numbers and statuses right-aligned; numbers always mono.
- Hover is a faint wash — never a lift, border glow, or shadow.
cursor:pointerand the hover wash are earned by a click that does something — that is what.liveclaims. A purely informational ledger is plain.row— stillness is the default; affordance is a claim the row must honor.- Follow tables with a mono footnote if truncated:
312 matching · first 100 shown.
Stat strips, not stat cards
KPIs share one strip: 2px ink rule on top, hairline below, thin vertical rules between cells (no boxes around them).
/* supply the template per strip: grid-template-columns: repeat(N, 1fr) for N cells */
.stats { display:grid; gap:0; border-bottom:1px solid var(--rule); }
.stats.top2 { border-top:2px solid var(--ink); }
.stat-cell { padding:24px 24px 24px 0; border-right:1px solid var(--rule); margin-right:24px; }
.stat-cell.last { border-right:none; margin-right:0; }
.stat-cell .num { font-family:'Newsreader',Georgia,serif; font-size:44px; font-weight:600;
letter-spacing:-0.02em; line-height:1.1; margin-top:6px; }
.stat-cell .sub { font-size:12px; color:var(--sub); margin-top:2px; }
Cell anatomy: mono label on top, big serif number, small sub-line below.
The masthead
A real newspaper masthead, twice ruled:
- Meta-row — 10px mono uppercase, letterspaced,
--sub: product/section name on the left; date · operator · theme toggle on the right. - Title row — the wordmark and the nav sharing a baseline
(
align-items:flex-end), closed by a 2px ink rule.
The meta-row's · separators are set with non-breaking spaces on both
sides ( · ) so a narrow viewport never orphans the dot from its
neighbors.
Tabs are plain text buttons: --sub, turning ink on hover; the active tab is
ink with a 2px vermillion underline. A standing primary shortcut (e.g.
"+ New Send") sits last in the nav as mono uppercase vermillion text — and
it counts against the vermillion budget (§1). When a live apparatus +
the active-tab underline + the shortcut would exceed the two-or-three
ceiling, the shortcut yields: it drops to ink and takes accent on hover
only.
The theme toggle is copy, not an icon: "Lights on" / "Lights off" — a
text button set in the meta-row's own mono style, joined to its neighbors
with the same · separator, last in the order (date · operator · toggle).
The choice persists across reloads (localStorage); a first visit is always
dark — the shop at night.
5. Components
Buttons — typographic blocks
.btn { border:0; border-radius:2px; cursor:pointer;
font-family:'IBM Plex Mono',monospace; font-weight:600; text-transform:uppercase; }
.btn-ink { background:var(--ink); color:var(--paper); padding:12px 24px;
font-size:12px; letter-spacing:0.14em; }
.btn-ink:hover { background:var(--acc); }
.btn-acc { background:var(--acc); color:var(--paper); padding:12px 22px;
font-size:11px; letter-spacing:0.14em; }
[data-theme="light"] .btn-acc { color:#faf8f4; }
.btn-acc:hover { background:var(--acchov); }
.btn-acc:disabled { background:var(--fill); color:var(--tert); cursor:default; }
.btn-outline { background:none; border:1px solid var(--ink); color:var(--ink);
padding:10px 18px; }
.btn-outline:hover { background:var(--ink); color:var(--paper); }
.linklike { background:none; border:0; padding:0; cursor:pointer;
font-family:'IBM Plex Mono',monospace; font-size:12px; letter-spacing:0.08em;
text-transform:uppercase; color:var(--sub); text-decoration:underline; }
.linklike:hover { color:var(--acc); }
Three voices, one job each:
.btn-ink— the default commitment (an ink block)..btn-acc— reserved for the consequential action in a decision pair. At most one per view..btn-outline— the secondary of a pair; fills to ink on hover..linklike— quiet actions that don't deserve a block.
Disabled = recessed (--fill + --tert). Never opacity.
Inputs
.in { background:var(--insurf); border:1px solid var(--inbord); color:var(--ink);
border-radius:2px; padding:9px 12px; font-size:13px;
font-family:'IBM Plex Mono',monospace; outline:none; }
.in:focus { border-color:var(--ink); }
input::placeholder, textarea::placeholder { color:var(--tert); }
Focus = the border sharpens to ink. No glow, no ring, no accent border.
For document-like forms (composing content, not filtering), use the underline variant — writing on a form:
.in-underline { background:transparent; border:0; border-bottom:1px solid var(--ink);
color:var(--ink); padding:8px 2px; outline:none; border-radius:0; }
.in-underline:focus { border-bottom:2px solid var(--acc); padding-bottom:7px; }
(The padding compensation keeps the layout from shifting when the rule thickens.)
Selects — no native chrome
.sel { position:relative; display:inline-flex; }
.sel select.in { appearance:none; -webkit-appearance:none; padding-right:32px; cursor:pointer; }
.sel::after { content:'▾'; position:absolute; right:12px; top:50%;
transform:translateY(-50%); pointer-events:none; color:var(--sub);
font-size:12px; font-family:'IBM Plex Mono',monospace; }
<span class="sel"><select class="in">…</select></span>
This is an instance of the glyph rule: there are no icons in this
system — no icon fonts, no SVG icons in chrome. Anything icon-like is a
typed character: ● (live dot), ✓ (done), ▾ (dropdown), + (new),
· (separator), — (empty value). If a typed glyph can't express it, use a
word.
Dialogs
Replace every native alert/confirm/prompt. A dialog is a small piece of
the same paper:
.dlg-backdrop { position:fixed; inset:0; background:rgba(12,10,8,0.62);
display:grid; place-items:center; z-index:300; }
.dlg-backdrop[hidden] { display:none; } /* display:grid silently defeats the
hidden attribute without this — a live trap; do not omit it */
.dlg-card { background:var(--paper); color:var(--ink); border-top:6px solid var(--ink);
width:460px; max-width:calc(100vw - 48px); padding:24px 28px 22px; }
.dlg-kicker { font-family:'IBM Plex Mono',monospace; font-size:10px;
letter-spacing:0.2em; text-transform:uppercase; color:var(--sub); }
.dlg-title { font-family:'Newsreader',Georgia,serif; font-size:24px; font-weight:600; }
.dlg-body { font-size:14px; line-height:1.55; color:var(--sub); }
.dlg-actions { display:flex; gap:12px; justify-content:flex-end; margin-top:20px; }
Anatomy: 6px ink bar on top (it's a page), mono kicker with the product name, serif title, prose body, actions right-aligned. Machine values quoted in the body get their own mono lines between hairlines.
Confirm pairs read as decisions with weight, phrased in the domain's
voice — the cancel names the safe act, the confirm names the consequence
(canonically: "Hold the press" / "Run the press"). The consequential side is
.btn-acc; the safe side .btn-outline. Wire Escape, backdrop click, and
Enter — Enter fires the consequential side, Escape the safe side. On open,
focus lands on the dialog card itself (tabindex="-1"), never on a button —
focusing a button would silently invert the default. A button the operator
has tabbed to wins Enter only; Escape takes the safe side regardless of
focus — the way out never depends on where the hand is resting. Mount
dialogs outside any re-rendered root so a UI refresh can't destroy them.
Progress — the ink-coverage track
.press-track { height:5px; background:var(--fill); position:relative; }
.press-fill { height:100%; background:var(--acc); transition:width 1s linear; }
A recessed groove filling with vermillion ink. Pair with mono captions below
(left: what's done; right: what remains). When the run completes, the whole
apparatus — dot, track, captions — leaves with the state cut, replaced by the
stated result (a ✓ line, the counts updated). A finished track never
persists as decoration; a full groove of vermillion blows the accent budget
for zero information. The live indicator is a blinking dot:
@keyframes pressblink { 0%,100% { opacity:1; } 50% { opacity:0.25; } }
.press-dot { width:8px; height:8px; border-radius:9999px; background:var(--acc);
animation:pressblink 1.6s infinite; display:inline-block; }
Empty states
One italic serif line in --sub, phrased as an invitation in the shop's
voice — never an apology, never a graphic:
Nothing on the press. Open a job ticket to start one.
Login
Logging in is clocking in: a centered card on otherwise empty paper — kicker, oversized wordmark (with tittle), an italic serif motto, one input and a "Clock In" button. The 6px ink bar still tops the page.
6. Voice
The metaphor lives in the copy as much as the CSS. Principles:
- Name features as press-room artifacts, not software abstractions. A campaign editor is a Job Ticket; history is The Ledger; the audience is Circulation; a test is a proof you pull. When building a new feature, find its press-room name first — the name will tell you how the screen should behave.
- Errors are jams. State plainly what happened and what to do next ("Something jammed" + a human sentence). Never surface raw exception text; convert machine errors to prose before they reach a dialog.
- Numbers are counted copies. This is a shop that counts what it prints — display counts honestly, mono, with their units ("6,079 copies", "2,222 to go").
- Sentence case prose; uppercase lives only in mono labels. No exclamation marks in system copy. No corporate filler.
7. Motion
Philosophy: the press physically working — nothing else moves.
Every animation must be tied to a real, named state change. Decorative
motion is forbidden. The stillness of everything else is what makes the
moving parts feel mechanical. Screens don't transition — they cut, like
turning a page. Hover states snap. There are no loading spinners (loading is
a mono one-liner: Loading…).
The bar for adding motion: name the state change it expresses; confirm the moment isn't already covered; confirm it wouldn't fire so often it becomes wallpaper. Hover, navigation, and loading never qualify.
All motion respects reduced-motion preferences:
@media (prefers-reduced-motion: reduce) {
.tittle::after { animation:none !important; }
.odo-strip { transition:none !important; }
/* …one line per animation… */
}
The four canonical animations:
7.1 The tittle stamp — arrival
Once per page load, the wordmark's vermillion dot drops in and stamps, with a tiny squash on landing. Transform-only, so the measured resting geometry (§3) is untouched.
@keyframes tittle-stamp {
0% { transform:translateY(-0.22em); opacity:0; }
55% { transform:translateY(0); opacity:1; }
70% { transform:translateY(0.01em) scale(1.25,0.72); }
100% { transform:translateY(0) scale(1); }
}
body:not(.stamped) .tittle::after { transform-origin:50% 100%;
animation:tittle-stamp 0.5s cubic-bezier(0.3,0.7,0.3,1) 0.15s both; }
If the app re-renders its DOM, guard against replay: after the first render,
set document.body.classList.add('stamped') (delayed ~800ms so the animation
finishes).
7.2 The impression counter — work in progress
A count that increases while the machine runs rolls like the mechanical counter bolted to a real press. Digits only roll forward — 9 rolls up through 0 to reach 6, never backwards.
.odo { display:inline-flex; line-height:1; }
.odo-d { display:inline-block; height:1em; overflow:hidden; }
.odo-strip { display:block; transition:transform 0.65s cubic-bezier(0.25,0.6,0.25,1); }
.odo-strip span { display:block; height:1em; line-height:1; }
Reference implementation (framework-free; works with destructive re-renders):
/* Each render bakes the strips at the last-shown value; rollOdometers()
(called after render) rolls them to the live one — so the data poll is
what physically moves the digits. Strips hold 0-9 twice so a digit can
wrap forward through zero. */
const odoShown = {};
function odoCells(str) {
const rows = Array.from({ length: 20 }, (_, k) => `<span>${k % 10}</span>`).join('');
return [...str].map(ch => ch === ','
? '<span>,</span>'
: `<span class="odo-d"><span class="odo-strip" style="transform:translateY(-${ch}em);">${rows}</span></span>`
).join('');
}
function odoHtml(id, value) {
const target = Number(value || 0).toLocaleString('en-US');
let shown = odoShown[id];
if (!shown || shown.length !== target.length) shown = odoShown[id] = target;
return `<span class="odo" data-odo="${id}" data-odo-target="${target}">${odoCells(shown)}</span>`;
}
function rollOdometers() {
document.querySelectorAll('.odo').forEach(el => {
const id = el.getAttribute('data-odo'), target = el.getAttribute('data-odo-target');
const shown = odoShown[id];
if (!shown || shown === target) return;
if (shown.length !== target.length) { odoShown[id] = target; el.innerHTML = odoCells(target); return; }
const strips = el.querySelectorAll('.odo-strip');
odoShown[id] = target;
requestAnimationFrame(() => requestAnimationFrame(() => {
let si = 0;
for (let i = 0; i < shown.length; i++) {
if (shown[i] === ',') continue;
const from = Number(shown[i]), to = Number(target[i]);
const off = from + ((to - from + 10) % 10);
if (strips[si]) strips[si].style.transform = `translateY(-${off}em)`;
si++;
}
}));
});
}
Display recipe: 26px mono 500 count, a 12px --sub unit line beside it
("of 6,079 copies"), the ink-coverage track beneath.
Captions and counts state the ledger truth immediately; the digits roll to meet them. A moment of disagreement while the strips turn is correct — the mechanical counter catches up to the count, never the reverse.
The per-digit forward wrap is for increases. A semantically lower total never rolls — it cuts (a page turn) straight to the corrected number: bake the strips at the new value and skip the transition. The press counts up; a correction is a reprint, not an un-print.
And only mono counts roll at all — a serif KPI is pride, not machinery; it never rolls, it cuts to its new value (§10).
7.3 The ink stroke — the irreversible click
Reserved for the one irreversible action in the application. On confirm, a near-black bar with a vermillion trailing edge sweeps once across the viewport; the state flips behind the bar, mid-sweep. Never reuse it for anything less.
.ink-stroke { position:fixed; inset:0; z-index:400; pointer-events:none;
background:#12100e; border-right:3px solid var(--acc); transform:translateX(-102%); }
.ink-stroke.go { transition:transform 0.95s cubic-bezier(0.6,0,0.3,1);
transform:translateX(102%); }
function inkStroke(midway) {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) { midway(); return; }
const bar = document.createElement('div');
bar.className = 'ink-stroke';
document.body.appendChild(bar);
requestAnimationFrame(() => requestAnimationFrame(() => bar.classList.add('go')));
setTimeout(midway, 430); // state flip, hidden behind the bar
setTimeout(() => bar.remove(), 1100);
}
7.4 The fresh sheet — new output
When a live document preview re-renders, the sheet settles in as if delivered off the folder. Deliberately gentle (it can fire on every typing pause): 8px rise, 300ms, slight fade.
@keyframes proof-settle { from { transform:translateY(8px); opacity:0.65; } }
.proof-frame-wrap.fresh { animation:proof-settle 0.3s cubic-bezier(0.2,0.8,0.2,1); }
wrap.classList.remove('fresh'); void wrap.offsetWidth; wrap.classList.add('fresh');
Pair with an input debounce (~400ms) and skip the repaint entirely when the rendered output is unchanged — motion that fires on every keystroke is wallpaper.
Document previews
Preview documents send-exact — render the very HTML the system will
output, in a sandboxed iframe, scaled to fit (e.g. 604px → 380px via
transform:scale() inside an overflow:hidden frame with the paper-stack
shadow). Never preview a simplified mock; trust in the proof is the product.
The proof sheet is content, not chrome. Its interior keeps its own print-light paper and ink literals in BOTH themes — paper is paper; the shop lights change, the proof does not. The warm-materials rule (§1) governs chrome only; do not re-token the document inside the frame to match the dark shop.
The bleeding edge — the press, run magnificently (LAW)
A real print shop rationed its motion because the machinery was bought with money: one counter on the wall, one guillotine, proofs pulled when the ink budget allowed. That was a limitation, not a law. This language is the press brought to the bleeding edge — and at the bleeding edge the press does NOT gain a fifth animation. It performs its four to the hilt:
- The counter rolls at ledger scale. Any mono count that increased
may roll — and the roll is proudest when it wraps: a tens digit riding
9 through 0 to 1 is the machine seen working. Wraps are derived
(
(to − from + 10) % 10), never staged. - The ink stroke is wealth spent once. The grandest gesture in the room buys the most irreversible moment in the room — and nothing else, ever. Spending it twice is how it becomes wallpaper.
- Every proof settles. New output always arrives off the folder — 8px, 300ms, gentle on purpose, timed to the delivery (never to the keystroke, never to the machinery warming up).
- The tittle stamps on arrival — once, transform-only, on the measured §3 geometry.
What stays forbidden breaks the fiction, never merely the tech: nothing floats, nothing spins, hover never moves, screens still cut like turning a page. A press with an unlimited budget buys better paper and truer ink — it does not buy a carousel.
The second test, asked of every screen after the front-page test: is this the best this shop has ever printed? If yes requires motion that isn't one of the four, the answer is no.
8. Do / Don't
Do
- Structure with the rule hierarchy (6px / 2px ink, 1px hairline); align to baselines.
- Mono for machine data, serif for headlines, Helvetica for prose.
- One vermillion accent as punctuation; warm grays only.
- Name features in press vocabulary; state errors plainly with a next step.
- Tie every animation to a real, named state change; respect
prefers-reduced-motion. - Build both themes from day one; test with the lights on and off.
Don't
- No icons (typed glyphs only), pills, badges, toasts, cards, floating panels, gradients, blur shadows, or border-radius above 2px.
- No cool grays, no
#000/#fff, no green/red/yellow status colors. - No vermillion fills or decoration.
- No animation on hover, navigation, or loading; no spinners, no skeletons.
- No letter-spaced or uppercase serif; no weights outside 400–700.
- Never eyeball the tittle geometry; animate it transform-only.
- No opacity for disabled states (use recessed fill + tertiary text).
- On the web (§10): nothing sticks — no sticky masthead, no floating cart, no persistent bars; the vermillion budget counts per document, not per viewport; a ledger line never wraps to two lines (columns yield instead); smooth scrolling never — jumps cut.
9. Starter skeleton
A minimal page in the system — the fastest way to check a new build is speaking the language:
<!doctype html>
<html data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400..700;1,6..72,400..700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* §1 tokens, §2 scale, §4 layout, §5 components go here */
</style>
</head>
<body>
<div class="shell">
<header class="wrap">
<div class="meta-row"><div>Your Shop · Section</div><div>Sat · Jul 11, 2026 · Operator</div></div>
<div class="title-row">
<div class="wordmark">D<span class="tittle">ı</span>spatch</div>
<nav class="tabs">
<button class="tab active">Today</button>
<button class="tab">The Ledger</button>
</nav>
</div>
</header>
<main class="wrap">
<h1 class="page">Today</h1>
<!-- stat strip, ledger rows, press strip… -->
</main>
</div>
</body>
</html>
Sanity checklist for the first screenshot: warm paper (not gray), 6px bar at the very top, the tittle sitting exactly on the ı, mono uppercase labels, serif headline, hairline rows, at most a couple of vermillion marks — and nothing moving unless the machine is running.
10. The shop window — Dispatch on the web
A print shop has a shop window and a mail-order catalog; selling off the front counter is as old as the trade. And the broadsheet was born a scrolling medium — a column of type you move down through, ruled and measured, is what this system has been all along. Dispatch on the web is not an adaptation; it is the language more at home than any desktop metaphor could be. Everything in §§0–9 stands unchanged here; this chapter adds only what a scrolling, narrow, touched page asks of it.
The one test gains a companion: would it make sense in the shop's window, or printed in its mail-order catalog? A specimen sheet of editions passes. A price in mono passes. A floating cart button following the reader down the page does not.
10.1 The narrow measure
The broadsheet narrows to a single column gracefully; only the margins and the largest type give ground:
@media (max-width:640px) {
.wrap { padding:20px 24px 0; }
main.wrap { padding:28px 24px 64px; }
}
@media (max-width:420px) {
.wrap { padding:20px 16px 0; }
main.wrap { padding:24px 16px 56px; }
h1.page { font-size:30px; }
.wordmark { font-size:34px; }
}
The wordmark's canonical sizes become a trio: 64px login, 40px
masthead, 34px narrow masthead (under 420px). The geometry is
em-relative and letter-spacing:-0.02em holds at every one (§3) — nothing
else about the markup changes.
10.2 Ledgers under compression
A ledger line never wraps to two lines — fewer honest columns beat a mangled row. Columns yield in declared order, and a column whose value derives from surviving columns yields first: a line's EACH goes before its AMOUNT, because the reader can divide. The minimum width a column may be given is its own header word; below that, it yields entirely.
.slipgrid { grid-template-columns:minmax(0,1fr) 90px 120px 100px 64px; }
@media (max-width:640px) {
/* EACH derives from AMOUNT ÷ QTY — it yields first */
.slipgrid { grid-template-columns:minmax(0,1fr) 68px 72px 46px; gap:8px; }
.col-each { display:none; }
}
(Mark the yielding column's header and cells with one class, col-each
style, so the whole column leaves together.)
10.3 Stat strips stacked
Below the breakpoint the strip turns on its side: the thin vertical rules
between cells rotate into 1px hairline row-rules. .top2 still tops the
strip; cell anatomy (mono label, serif number, sub-line) is unchanged.
@media (max-width:640px) {
.stats { grid-template-columns:1fr; }
.stat-cell { border-right:none; margin-right:0;
border-bottom:1px solid var(--rule); padding:18px 0; }
.stat-cell.last { border-bottom:none; }
}
10.4 The masthead wraps
When the title row runs out of measure, the nav wraps to its own line below the wordmark, left-aligned; the 2px ink rule still closes the whole masthead beneath both. In the meta-row, the right side (date · operator · toggle) wins the space; the left side truncates with an ellipsis.
.title-row { display:flex; justify-content:space-between; align-items:flex-end;
flex-wrap:wrap; gap:12px 32px; padding-bottom:14px; border-bottom:2px solid var(--ink); }
.tabs { display:flex; gap:26px; align-items:flex-end; flex-wrap:wrap; }
.meta-row { display:flex; justify-content:space-between; flex-wrap:wrap; gap:4px 24px; }
.meta-row > :first-child { min-width:0; overflow:hidden;
text-overflow:ellipsis; white-space:nowrap; }
.meta-row > :last-child { flex:none; }
10.5 Touch
On hover-less pointers the hover vocabulary translates to :active — the
press is the hover: rows take the wash while pressed; linklike and buttons
take their hover color as their pressed color. And typed-glyph controls
(the − / + of a quantity, linklike actions) keep their visual size but
grow their hit area to ≥40px with padding — the target grows, the ink
does not:
@media (hover:none) {
.row.live:active { background:var(--hovfill); }
.linklike:active { color:var(--acc); }
.btn-ink:active { background:var(--acc); }
.btn-acc:active { background:var(--acchov); }
.btn-outline:active { background:var(--ink); color:var(--paper); }
.qty-btn:active { color:var(--ink); }
.qty-btn, .linklike { padding:13px 12px; margin:-13px -12px; }
}
10.6 Nothing sticks
Print does not follow the reader down the page. No sticky masthead, no floating cart, no persistent bars — period. (Note the deliberate contrast: this language answers the web differently than a desktop-era language would; the broadsheet trusts the reader to scroll back up.)
Tabs on a one-page site may anchor-jump — and the jump cuts, like
turning a page, never smooth-scrolls (el.scrollIntoView(), no
behavior:'smooth', and no scroll-behavior:smooth anywhere in the CSS).
The active tab moves on click, never by scroll-spy — attention is
granted, not tracked.
10.7 The budget reads per document
On a scrolling page the vermillion budget (§1) counts per document, not per viewport — the stricter reading is canonical. A long specimen sheet does not earn a fresh two-or-three marks every screenful; the whole page is one sheet of paper, and it gets one sheet's worth of ink.
10.8 Results stay on the record
A stated result — the ✓ line a completed run leaves behind (§5 Progress)
— persists until the next run begins or the page turns. It is a fact on
the record, not a toast: no timeout, no dismiss button, no fade. The ledger
does not erase itself while you're reading it.
10.9 The commerce vocabulary
The shop sells the way it prints:
- A price is a machine value — mono, right-set, currency symbol included. Per-unit qualifiers ride beside it in the small mono voice:
.price { font-family:'IBM Plex Mono',monospace; font-size:16px;
font-weight:600; color:var(--ink); }
.price .per { font-size:11px; font-weight:400; color:var(--sub); margin-left:6px; }
<div class="price">$18.00<span class="per">per set</span></div>
- Quantity is a mono count flanked by typed − / + glyphs (
−and+, the glyph rule of §5) — quiet--subbuttons turning ink on hover, with the §10.5 hit-area rule applied:
.qty-btn { background:none; border:0; cursor:pointer;
font-family:'IBM Plex Mono',monospace; font-size:14px; color:var(--sub);
padding:0 7px; }
.qty-btn:hover { color:var(--ink); }
- Product imagery is the proof sheet. A product is shown as a small typeset document — set in the system's own faces, never photography-dependent — and, being a document, it keeps print-light paper and ink literals in BOTH themes (§7 Document previews: the shop lights change, the proof does not), under the paper-stack shadow:
.proof { background:#f6f2ea; color:#221f1a; border:1px solid rgba(28,26,23,0.14);
box-shadow:4px 4px 0 var(--fill); font-family:'Newsreader',Georgia,serif; }
- A staged line ("On the slip · 2") is plain ink mono with no glyph — the
✓is earned by the act (§1), and adding to a cart is a plan, not an act.
10.10 Rolling numbers are mono
Only machine-voice (mono) counts may roll (§7.2) — the odometer is machinery, and mono digits are effectively tabular, so the strips align. A serif KPI is pride, not machinery: it never rolls, it cuts to its new value when the fact changes. This resolves §7.2 against the serif stat strip — an impressions total in a stat cell updates by page-turn; the same number in a running press apparatus rolls, because there it is set in mono.
v1.0.1 — 2026-07-11. Eight gaps closed after the blind-build QA gate
(qa/subscriber-import.html): non-interactive ledger affordance, the
staged/pending status row, progress-apparatus removal on completion,
odometer-caption precedence, theme persistence, meta-row toggle recipe,
dialog Enter/Escape semantics, stat-strip column templates.
v1.0.2 — port rulings closed after the starter-codebase build: dialog
focus-on-card and Escape-over-focus, the .row.live affordance modifier,
subject/metadata mono ranking, proof-sheet literals in both themes,
meta-row non-breaking separators and the shortcut's vermillion yield,
cut-don't-roll corrections, canonical wordmark sizes, self-hosted font
subsetting (latin-ext / U+0131, opsz axis).
v1.1.0 — the shop window: web mode authored after the blind storefront gate.
v1.2.0 — 2026-07-18. The bleeding-edge round, under the pipeline's standing doctrine (limitations shed, laws kept): §7 gains THE BLEEDING EDGE — THE PRESS, RUN MAGNIFICENTLY (LAW). The four canonical animations are the whole budget at any scale: counters roll with derived through-zero wraps, the ink stroke is spent exactly once on the one irreversible act, every delivered proof settles off the folder, the tittle stamps once on measured geometry. The forbidden list is fiction-bound: an unlimited press buys truer ink, not a carousel. Second test codified: “is this the best this shop has ever printed?”