# 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:

```css
[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:

  ```css
  ::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.

```html
<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

```css
#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.

```html
<div class="wordmark">D<span class="tittle">&#305;</span>spatch</div>
```

```css
.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.02em` holds 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.

```css
* { 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

```css
.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:pointer` and the hover wash are **earned by a click that does
  something** — that is what `.live` claims. 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).

```css
/* 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:

1. **Meta-row** — 10px mono uppercase, letterspaced, `--sub`: product/section
   name on the left; date · operator · theme toggle on the right.
2. **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 (`&nbsp;·&nbsp;`) 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

```css
.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

```css
.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:

```css
.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

```css
.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; }
```

```html
<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:

```css
.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

```css
.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:

```css
@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:

```css
@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.

```css
@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.

```css
.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):

```js
/* 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.

```css
.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%); }
```

```js
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.

```css
@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); }
```

```js
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:

```html
<!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">&#305;</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:

```css
@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.

```css
.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.

```css
@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.

```css
.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:

```css
@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:

```css
.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; }
```

```html
<div class="price">$18.00<span class="per">per set</span></div>
```

- **Quantity is a mono count flanked by typed − / + glyphs** (`&minus;` and
  `+`, the glyph rule of §5) — quiet `--sub` buttons turning ink on hover,
  with the §10.5 hit-area rule applied:

```css
.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:

```css
.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?”*
