carlos / movement Public

A type ramp that exists

The design detector flagged nine literal font sizes off the documented
ramp, and it was right about something worse than drift: there was no ramp.
Twenty-one font-size declarations across twelve unrelated values — 1.0625,
1.15, 1.1875, 1.35, 1.5, 1.6, 1.75, 1.9, 2, 2.75, 3, 3.25rem — each picked
for the component in front of me. DESIGN.md described a system the CSS had
never had, because it was written from a build that only looked systematic.

Nine steps now, declared once in theme.css: four display, five text.
Nothing in the app sets a literal size. Two em multipliers stay relative,
because they size against their own context rather than against the ramp,
and they are named as the only exceptions.

Deleting the mobile tab-word override fell out of it: display-2 clamps down
to 1.5rem on its own, which is all the override was doing.

Detector clean, screenshots unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell 7e12ad01942bf819a1ec6b579f1566b32a516735 parent 4262594
4 files changed, +67 −35
  • .impeccable/hook.cache.json +1 −1
  • DESIGN.md +32 −15
  • internal/movement/static/bill.css +18 −19
  • internal/movement/static/theme.css +16 −0
diff --git a/.impeccable/hook.cache.json b/.impeccable/hook.cache.json
index b3df96e..9dfc53f 100644
--- a/.impeccable/hook.cache.json
+++ b/.impeccable/hook.cache.json
@@ -1 +1 @@
-{"version":1,"sessions":{"91e646a9-0b65-4ca7-83c7-59b544490463":{"updatedAt":1788370973442,"files":{"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/static/theme.css":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/static/bill.css":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/layout.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/index.html":{"editCount":4,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/stance.html":{"editCount":2,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/sent.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/confirm.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/signin.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/admin.html":{"editCount":1,"findings":[],"cleanAcked":true}}}}}
\ No newline at end of file
+{"version":1,"sessions":{"91e646a9-0b65-4ca7-83c7-59b544490463":{"updatedAt":1788371701138,"files":{"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/static/theme.css":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/static/bill.css":{"editCount":1,"findings":["design-system-font-size:163:1.25rem","design-system-font-size:224:1.6rem","design-system-font-size:283:1.75rem","design-system-font-size:450:3rem","design-system-font-size:486:1.5rem","design-system-font-size:522:2rem","design-system-font-size:530:0.9375rem","design-system-font-size:547:0.9375rem","design-system-font-size:552:0.75rem"],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/layout.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/index.html":{"editCount":4,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/stance.html":{"editCount":2,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/sent.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/confirm.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/signin.html":{"editCount":1,"findings":[],"cleanAcked":true},"/home/paulca/amadan.net/carlos/movement/.claude/worktrees/design/internal/movement/templates/admin.html":{"editCount":1,"findings":[],"cleanAcked":true}}}}}
\ No newline at end of file
diff --git a/DESIGN.md b/DESIGN.md
index 686582e..f3e198f 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -207,21 +207,31 @@ unfussy text face that never raises its voice. The display face is only ever
used at weight 800, uppercase, with negative tracking and sub-1 line height, so
it sets as a solid block of ink rather than a line of letters.
-### Hierarchy
-- **Shout** (800, `clamp(4rem, 16vw, 10.5rem)`, 0.84, `-0.02em`, caps): the
- first-viewport headline only. Capped at `15ch` with balanced wrapping so it
- always breaks into a block.
-- **Headline** (800, `clamp(1.9rem, 5vw, 3.25rem)`, 0.92, caps): receipt marks,
- the ledger title, the tab word, the torn stub's word.
-- **Title** (800, `clamp(1.35rem, 3vw, 1.9rem)`, 1, caps): the slip's heading
- above a form.
-- **Body** (400, `clamp(1rem, 1.15vw, 1.125rem)`, 1.55): the bill's two-column
- argument.
-- **Reply** (400, `clamp(1.0625rem, 1.3vw, 1.1875rem)`, 1.62, max `68ch`): the
- stance sheet's answer; its first paragraph steps up to `1.15em` as a lede.
-- **Label** (700, `0.8125rem`, `0.06em`): field labels, tab addresses, status
- lines and table heads (`0.75rem`/`0.12em` in the ledger head), uppercase where
- it is a status or a column head.
+### The ramp
+
+Nine steps, declared once in `theme.css`. Four display and five text. Nothing
+in the app sets a literal font size — a bill has a handful of sorts in the
+case, not a continuum.
+
+| Token | Value | Used by |
+|---|---|---|
+| `--t-shout` | `clamp(4rem, 16vw, 10.5rem)` | the first-viewport headline, and nothing else |
+| `--t-display-1` | `clamp(2rem, 5vw, 3.25rem)` | ledger title, receipt mark |
+| `--t-display-2` | `clamp(1.5rem, 3.2vw, 2.75rem)` | tab word, torn stub's word |
+| `--t-display-3` | `clamp(1.35rem, 3vw, 1.9rem)` | the slip's heading above a form |
+| `--t-lede` | `clamp(1.0625rem, 1.3vw, 1.1875rem)` | stance reply, receipt body, tear instruction |
+| `--t-body` | `clamp(1rem, 1.15vw, 1.125rem)` | the bill's two-column argument |
+| `--t-base` | `0.9375rem` | ledger table |
+| `--t-sm` | `0.875rem` | imprint line, stamp label |
+| `--t-xs` | `0.8125rem` | field labels, tab addresses, status lines |
+| `--t-xxs` | `0.75rem` | ledger column heads |
+
+Display steps carry weight 800, uppercase, negative tracking and sub-1 line
+height (0.84 on the shout, 0.9–1 below it). Text steps run 1.5–1.62.
+
+Two relative multipliers are not steps and are allowed to stay relative: the
+stance reply's first paragraph at `1.15em`, and the ledger's tally figure at
+`1.5em`. Both size against their own context rather than against the ramp.
### Named Rules
**The Self-Hosted Faces Rule.** Both families are served from
@@ -235,6 +245,13 @@ vendoring the woff2.
running text, labels or numbers in prose — except the ledger's tally figures,
where it sets the count as a struck number beside body-face copy.
+**The Nine Sorts Rule.** Every size comes from the ramp above, as a `--t-*`
+token. A literal `font-size` anywhere in the app is a defect, not a local
+decision — the first version of this stylesheet had twenty-one of them across
+twelve unrelated values, and the ramp documented here did not exist until the
+design detector pointed out that the record described a system the code had
+never had. The two `em` multipliers named above are the only exceptions.
+
**The No-Costume-Mono Rule.** A monospace stack is declared but is not used on
any surface. Monospace here would be a costume for "technical" on pages that are
not about machines. Tabular figures in the ledger come from
diff --git a/internal/movement/static/bill.css b/internal/movement/static/bill.css
index 76a4368..179a88c 100644
--- a/internal/movement/static/bill.css
+++ b/internal/movement/static/bill.css
@@ -60,7 +60,7 @@ body {
not about machines. */
.bill__slug {
margin: 0;
- font-size: 0.875rem;
+ font-size: var(--t-sm);
}
/* No underline here: a 3px rule ten pixels above the perforation reads as a
doubled rule, not as a link. */
@@ -74,7 +74,7 @@ body {
───────────────────────────────────────────────────────────────────── */
.bill {
- --shout: clamp(4rem, 16vw, 10.5rem);
+ --shout: var(--t-shout);
}
.bill__shout {
@@ -120,7 +120,7 @@ body {
column-gap: clamp(1.5rem, 4vw, 3.5rem);
column-rule: 1px solid var(--rst-line);
max-width: 62rem;
- font-size: clamp(1rem, 1.15vw, 1.125rem);
+ font-size: var(--t-body);
line-height: 1.55;
}
.bill__body p {
@@ -160,7 +160,7 @@ body {
}
.tear__instruction {
font-weight: 700;
- font-size: clamp(1rem, 1.4vw, 1.25rem);
+ font-size: var(--t-lede);
margin: 0;
}
@@ -210,7 +210,7 @@ body {
.tab__addr {
margin-top: 0.15rem;
- font-size: 0.8125rem;
+ font-size: var(--t-xs);
font-weight: 700;
color: var(--bill-blue);
overflow-wrap: anywhere;
@@ -221,7 +221,7 @@ body {
font-family: var(--bill-display);
font-weight: 800;
text-transform: uppercase;
- font-size: clamp(1.6rem, 3.2vw, 2.75rem);
+ font-size: var(--t-display-2);
line-height: 0.92;
min-height: 1.84em;
letter-spacing: -0.01em;
@@ -280,13 +280,13 @@ body {
font-family: var(--bill-display);
font-weight: 800;
text-transform: uppercase;
- font-size: clamp(1.75rem, 5vw, 2.75rem);
+ font-size: var(--t-display-2);
line-height: 0.9;
margin: 0;
}
.sheet__reply {
- font-size: clamp(1.0625rem, 1.3vw, 1.1875rem);
+ font-size: var(--t-lede);
line-height: 1.62;
max-width: 68ch;
}
@@ -309,7 +309,7 @@ body {
font-family: var(--bill-display);
font-weight: 800;
text-transform: uppercase;
- font-size: clamp(1.35rem, 3vw, 1.9rem);
+ font-size: var(--t-display-3);
line-height: 1;
margin: 0 0 1.25rem;
}
@@ -320,7 +320,7 @@ body {
}
.field > span {
display: block;
- font-size: 0.8125rem;
+ font-size: var(--t-xs);
font-weight: 700;
letter-spacing: 0.06em;
margin-bottom: 0.35rem;
@@ -356,7 +356,7 @@ body {
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
- font-size: 0.875rem;
+ font-size: var(--t-sm);
color: var(--bill-stock);
background: var(--bill-blue);
border: 0;
@@ -392,7 +392,7 @@ body {
.slip__status {
margin: 0.85rem 0 0;
- font-size: 0.8125rem;
+ font-size: var(--t-xs);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
@@ -447,7 +447,7 @@ body {
font-family: var(--bill-display);
font-weight: 800;
text-transform: uppercase;
- font-size: clamp(1.9rem, 5vw, 3rem);
+ font-size: var(--t-display-1);
line-height: 0.92;
color: var(--bill-blue);
margin: 0 0 0.85rem;
@@ -455,7 +455,7 @@ body {
}
.receipt p {
margin: 0;
- font-size: 1.0625rem;
+ font-size: var(--t-lede);
line-height: 1.6;
}
@@ -483,7 +483,6 @@ body {
border-bottom: 0;
}
.tab__word {
- font-size: 1.5rem;
min-height: 0;
}
.tab:hover,
@@ -519,7 +518,7 @@ body {
font-family: var(--bill-display);
font-weight: 800;
text-transform: uppercase;
- font-size: clamp(2rem, 5vw, 3.25rem);
+ font-size: var(--t-display-1);
line-height: 0.9;
margin: 0 0 1.25rem;
color: var(--bill-blue);
@@ -527,7 +526,7 @@ body {
.ledger__tally,
.ledger__split {
margin: 0 0 0.65rem;
- font-size: 0.9375rem;
+ font-size: var(--t-base);
}
.ledger__tally b,
.ledger__split b {
@@ -544,12 +543,12 @@ body {
width: 100%;
border-collapse: collapse;
margin-top: 1.5rem;
- font-size: 0.9375rem;
+ font-size: var(--t-base);
font-variant-numeric: tabular-nums;
}
.ledger__table th {
text-align: left;
- font-size: 0.75rem;
+ font-size: var(--t-xxs);
letter-spacing: 0.12em;
text-transform: uppercase;
border-bottom: 2px solid var(--bill-ink);
diff --git a/internal/movement/static/theme.css b/internal/movement/static/theme.css
index 4b8d8d1..a17b690 100644
--- a/internal/movement/static/theme.css
+++ b/internal/movement/static/theme.css
@@ -96,6 +96,22 @@
--rst-tone-neutral-bg: rgba(23, 20, 15, 0.08);
--rst-tone-neutral-fg: #4a4437;
+
+ /* ── The type ramp ──────────────────────────────────────────────────
+ Four display steps and five text steps. Nothing sets a literal size:
+ a bill has a handful of sorts in the case, not a continuum. */
+ --t-shout: clamp(4rem, 16vw, 10.5rem);
+ --t-display-1: clamp(2rem, 5vw, 3.25rem);
+ --t-display-2: clamp(1.5rem, 3.2vw, 2.75rem);
+ --t-display-3: clamp(1.35rem, 3vw, 1.9rem);
+
+ --t-lede: clamp(1.0625rem, 1.3vw, 1.1875rem);
+ --t-body: clamp(1rem, 1.15vw, 1.125rem);
+ --t-base: 0.9375rem;
+ --t-sm: 0.875rem;
+ --t-xs: 0.8125rem;
+ --t-xxs: 0.75rem;
+
--rst-font: "Public Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
--bill-display: "Big Shoulders", "Haettenschweiler", "Arial Narrow", sans-serif;
--bill-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Menlo, monospace;