carlos / movement Public

DESIGN.md, written from what shipped rather than what was intended

Six named rules, each derived from the built world: the overprint (green is
a product of cyan multiplied into stock, never a paint), the 1-2px
misregistration, two stocks and no white, the halftone as the only surface
material, one shadow in the whole system, and the tabs being equal and
carrying their own address.

The documenter kept two things out on purpose. The remaining placeholder
string is a build defect blocking the production boot check, not a
typographic rule, so it is recorded nowhere in the system. And the
framework's own tokens.css radii — 9px and 14px on menus, modals and
switches — are left out, because they belong to the pinned vendored file
and contradict this app's zero-radius doctrine. That contradiction is
latent rather than visible: nothing in the app renders those components
yet, and the first one that does will look wrong.

This closes the FINISH line the direction contract opened: review, verdict,
DESIGN.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell 4262594dd510a24bca253df35fae352ce66351c6 parent 6386215
2 files changed, +620 −0
  • .impeccable/design.json +218 −0
  • DESIGN.md +402 −0
diff --git a/.impeccable/design.json b/.impeccable/design.json
new file mode 100644
index 0000000..7d9c9b6
--- /dev/null
+++ b/.impeccable/design.json
@@ -0,0 +1,218 @@
+{
+ "schemaVersion": 2,
+ "generatedAt": "2026-09-02T00:00:00Z",
+ "title": "Design System: Movement",
+ "extensions": {
+ "colorMeta": {
+ "stock": {
+ "role": "neutral",
+ "displayName": "Acid Stock",
+ "canonical": "#f0e34a",
+ "tonalRamp": ["#2b2707", "#4a4110", "#6b5f14", "#8d7d1c", "#b09c24", "#d0ba33", "#f0e34a", "#faf3ae"]
+ },
+ "bone": {
+ "role": "neutral",
+ "displayName": "Bone",
+ "canonical": "#f7f4e6",
+ "tonalRamp": ["#2a281f", "#454236", "#615d4e", "#7f7a68", "#9e9884", "#c0baa4", "#dfd9c5", "#f7f4e6"]
+ },
+ "ink": {
+ "role": "neutral",
+ "displayName": "Press Ink",
+ "canonical": "#17140f",
+ "tonalRamp": ["#17140f", "#2e291f", "#4a4437", "#6a6455", "#8b8574", "#aea896", "#d2ccba", "#f0ece0"]
+ },
+ "blue": {
+ "role": "primary",
+ "displayName": "Ultramarine Ink",
+ "canonical": "#1b2a8f",
+ "tonalRamp": ["#0a0f36", "#101857", "#121d68", "#1b2a8f", "#2b3db5", "#5566d0", "#8f9ce5", "#ccd2f5"]
+ },
+ "blue-strong": {
+ "role": "primary",
+ "displayName": "Ultramarine Deepened",
+ "canonical": "#121d68",
+ "tonalRamp": ["#080c2c", "#0c1244", "#121d68", "#1b2a8f", "#2b3db5", "#5566d0", "#8f9ce5", "#ccd2f5"]
+ },
+ "cyan": {
+ "role": "primary",
+ "displayName": "Press Cyan (second ink, multiply only)",
+ "canonical": "#14a0c8",
+ "tonalRamp": ["#052a35", "#08414f", "#0a5a6f", "#0f7b96", "#14a0c8", "#3fbcdd", "#7fd4ea", "#c2ecf7"]
+ },
+ "overprint": {
+ "role": "secondary",
+ "displayName": "Overprint Green (cyan multiplied into yellow stock)",
+ "canonical": "#2f6b46",
+ "tonalRamp": ["#0d2016", "#153322", "#1f5334", "#2f6b46", "#43895c", "#6dab82", "#a1cbaf", "#d5e8dc"]
+ },
+ "dim-on-stock": {
+ "role": "neutral",
+ "displayName": "Thinned Ink on Stock",
+ "canonical": "#6b5f14",
+ "tonalRamp": ["#231f06", "#3b340a", "#524810", "#6b5f14", "#8d7d1c", "#b09c24", "#d0ba33", "#f0e34a"]
+ },
+ "dim-on-bone": {
+ "role": "neutral",
+ "displayName": "Thinned Ink on Bone",
+ "canonical": "#6a6455",
+ "tonalRamp": ["#211f1a", "#37342c", "#4f4b3f", "#6a6455", "#8b8574", "#aea896", "#d2ccba", "#f0ece0"]
+ },
+ "negative-fg": {
+ "role": "tertiary",
+ "displayName": "Refusal Red",
+ "canonical": "#8e2417",
+ "tonalRamp": ["#2c0b07", "#4a120c", "#6c1b11", "#8e2417", "#b23a2a", "#cf6d5e", "#e6a79c", "#f6dcd7"]
+ }
+ },
+ "typographyMeta": {
+ "shout": { "displayName": "Shout", "purpose": "The first-viewport headline only. Uppercase, capped at 15ch, balanced wrap, crossed by the cyan overprint band." },
+ "headline": { "displayName": "Headline", "purpose": "Receipt marks, ledger title, tab words, the torn stub's word." },
+ "title": { "displayName": "Title", "purpose": "The slip heading above a form." },
+ "body": { "displayName": "Body", "purpose": "The bill's two-column argument." },
+ "reply": { "displayName": "Reply", "purpose": "The stance sheet's answer, max 68ch; first paragraph steps up to 1.15em." },
+ "label": { "displayName": "Label", "purpose": "Field labels, tab addresses, status lines, table column heads." },
+ "stamp": { "displayName": "Stamp", "purpose": "Button lettering only: uppercase, 0.14em tracking." }
+ },
+ "shadows": [
+ { "name": "tab-lift", "value": "0 10px 30px rgba(23, 20, 15, 0.32)", "purpose": "The only shadow in the system: a tear-off tab lifting off the wall on hover/focus. Offset and blur, because that is what a real shadow has. Suppressed below 46rem and under prefers-reduced-motion." }
+ ],
+ "motion": [
+ { "name": "tab-lift-ease", "value": "260ms cubic-bezier(0.16, 1, 0.3, 1)", "purpose": "Transform and shadow on the tear-off tab. The only expressive motion in the build." },
+ { "name": "stamp-ease", "value": "160ms ease-out", "purpose": "Background deepening on the stamp button's hover." }
+ ],
+ "breakpoints": [
+ { "name": "stack", "value": "46rem", "purpose": "Below: the argument drops to one column and the tear strip stacks with horizontal dashed dividers; tab lift is replaced by a bone tint." },
+ { "name": "split", "value": "60rem", "purpose": "Below: the stance sheet's reply/slip grid stacks to one column." },
+ { "name": "full-bleed", "value": "46rem wide and 40rem tall", "purpose": "At or above both: the bill fills 100svh so the tear strip lands near the fold." }
+ ],
+ "textures": [
+ { "name": "halftone-stock", "value": "inline SVG dot screen, 6px tile, r=0.65, ink at 13% opacity", "purpose": "Paper grain on the acid stock. Authored dot screen, never a raster texture." },
+ { "name": "halftone-bone", "value": "inline SVG dot screen, 6px tile, r=0.6, ink at 7% opacity", "purpose": "Paper grain on the bone reading surface." }
+ ]
+ },
+ "components": [
+ {
+ "name": "Stamp (primary button)",
+ "kind": "button",
+ "refersTo": "stamp",
+ "description": "The primary action on every form surface: a blue block with the second ink struck across its face.",
+ "html": "<button class=\"ds-stamp\">Sign the list</button>",
+ "css": ".ds-stamp { position: relative; overflow: hidden; font-family: \"Public Sans\", ui-sans-serif, system-ui, sans-serif; font-weight: 700; font-size: 0.875rem; letter-spacing: 0.14em; text-transform: uppercase; color: #f0e34a; background: #1b2a8f; border: 0; border-radius: 0; padding: 0.85rem 1.75rem; cursor: pointer; transition: background 160ms ease-out; } .ds-stamp::after { content: \"\"; position: absolute; left: 1.15rem; right: 1.15rem; top: 56%; height: 7px; background: #14a0c8; mix-blend-mode: multiply; transform: translateY(1px); pointer-events: none; } .ds-stamp:hover:not(:disabled) { background: #121d68; } .ds-stamp:focus-visible { outline: 3px solid #1b2a8f; outline-offset: 2px; }"
+ },
+ {
+ "name": "Stamp, disabled",
+ "kind": "button",
+ "refersTo": "stamp-disabled",
+ "description": "An unstruck stamp: the ground drops out to a dashed outline and the cyan strike is removed. Used until the proof-of-work module enables the form.",
+ "html": "<button class=\"ds-stamp-disabled\" disabled>Sign the list</button>",
+ "css": ".ds-stamp-disabled { font-family: \"Public Sans\", ui-sans-serif, system-ui, sans-serif; font-weight: 700; font-size: 0.875rem; letter-spacing: 0.14em; text-transform: uppercase; background: transparent; color: #6a6455; border: 2px dashed rgba(23, 20, 15, 0.55); border-radius: 0; padding: 0.85rem 1.75rem; cursor: not-allowed; }"
+ },
+ {
+ "name": "Tear-off Tab",
+ "kind": "custom",
+ "refersTo": "tab",
+ "description": "The signature component and the primary action of the first surface: equal tabs hanging off a perforation, each carrying its own address. Never numbered.",
+ "html": "<div class=\"ds-tear\"><div class=\"ds-tear-perf\"></div><div class=\"ds-tear-tabs\"><a class=\"ds-tab\" href=\"#\"><span class=\"ds-tab-word\">Sceptical</span><span class=\"ds-tab-addr\">movement.bdj.oncarlos.com/sceptical</span></a><a class=\"ds-tab\" href=\"#\"><span class=\"ds-tab-word\">Curious</span><span class=\"ds-tab-addr\">movement.bdj.oncarlos.com/curious</span></a><a class=\"ds-tab\" href=\"#\"><span class=\"ds-tab-word\">All in</span><span class=\"ds-tab-addr\">movement.bdj.oncarlos.com/all-in</span></a></div></div>",
+ "css": ".ds-tear-perf { height: 2px; background-image: repeating-linear-gradient(to right, #17140f 0 7px, transparent 7px 14px); } .ds-tear-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; border-bottom: 2px solid #17140f; } .ds-tab { display: flex; flex-direction: column; gap: 0.5rem; min-height: 6.5rem; padding: 0.9rem 1.1rem 1rem; background: #f0e34a; color: #17140f; text-decoration: none; border-right: 2px dashed #17140f; transform-origin: top center; transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 260ms cubic-bezier(0.16, 1, 0.3, 1); } .ds-tab:last-child { border-right: 0; } .ds-tab:hover, .ds-tab:focus-visible { transform: translateY(-10px); box-shadow: 0 10px 30px rgba(23, 20, 15, 0.32); z-index: 1; } .ds-tab-word { font-family: \"Big Shoulders\", Haettenschweiler, \"Arial Narrow\", sans-serif; font-weight: 800; text-transform: uppercase; font-size: 2.75rem; line-height: 0.92; letter-spacing: -0.01em; } .ds-tab-addr { font-size: 0.8125rem; font-weight: 700; color: #1b2a8f; overflow-wrap: anywhere; }"
+ },
+ {
+ "name": "Ruled Field",
+ "kind": "input",
+ "refersTo": "field-input",
+ "description": "A ruled line on paper, not a box on a screen. Label above in tracked bold; 2px ink underline; square focus registration mark.",
+ "html": "<label class=\"ds-field\"><span>Email address</span><input type=\"email\" placeholder=\"you@example.com\"></label>",
+ "css": ".ds-field { display: block; margin-bottom: 1.35rem; font-family: \"Public Sans\", ui-sans-serif, system-ui, sans-serif; } .ds-field > span { display: block; font-size: 0.8125rem; font-weight: 700; letter-spacing: 0.06em; margin-bottom: 0.35rem; color: #17140f; } .ds-field input { width: 100%; box-sizing: border-box; font: inherit; color: #17140f; background: transparent; border: 0; border-bottom: 2px solid #17140f; border-radius: 0; padding: 0.45rem 0; } .ds-field input::placeholder { color: #6a6455; } .ds-field input:focus-visible { outline: 3px solid #1b2a8f; outline-offset: 2px; }"
+ },
+ {
+ "name": "Tinted Textarea",
+ "kind": "input",
+ "refersTo": "field-textarea",
+ "description": "The one field variation: a faint ink tint that shows the field's extent. The tint is not a fill and never becomes a card.",
+ "html": "<label class=\"ds-field-area\"><span>How could you help?</span><textarea rows=\"5\"></textarea></label>",
+ "css": ".ds-field-area { display: block; margin-bottom: 1.35rem; font-family: \"Public Sans\", ui-sans-serif, system-ui, sans-serif; } .ds-field-area > span { display: block; font-size: 0.8125rem; font-weight: 700; letter-spacing: 0.06em; margin-bottom: 0.35rem; color: #17140f; } .ds-field-area textarea { width: 100%; box-sizing: border-box; font: inherit; line-height: 1.5; color: #17140f; background: rgba(23, 20, 15, 0.05); border: 0; border-bottom: 2px solid #17140f; border-radius: 0; padding: 0.55rem 0.7rem; min-height: 7.5rem; resize: vertical; } .ds-field-area textarea:focus-visible { outline: 3px solid #1b2a8f; outline-offset: 2px; }"
+ },
+ {
+ "name": "Shout with Overprint Band",
+ "kind": "custom",
+ "refersTo": "shout",
+ "description": "The first-viewport headline in condensed caps, crossed by the second ink multiplied into the stock and offset 2px off true.",
+ "html": "<div class=\"ds-stock\"><h1 class=\"ds-shout\">Software is being handed to machines. Decide where you stand.</h1></div>",
+ "css": ".ds-stock { background-color: #f0e34a; background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Ccircle cx='1' cy='1' r='0.65' fill='%2317140f' fill-opacity='0.13'/%3E%3Ccircle cx='4' cy='4' r='0.65' fill='%2317140f' fill-opacity='0.13'/%3E%3C/svg%3E\"); padding: 2rem; overflow: hidden; } .ds-shout { position: relative; font-family: \"Big Shoulders\", Haettenschweiler, \"Arial Narrow\", sans-serif; font-weight: 800; text-transform: uppercase; color: #1b2a8f; font-size: clamp(2.5rem, 9vw, 6rem); line-height: 0.84; letter-spacing: -0.02em; margin: 0; max-width: 15ch; text-wrap: balance; } .ds-shout::after { content: \"\"; position: absolute; left: -100vw; right: -100vw; top: 46%; height: 0.26em; background: #14a0c8; mix-blend-mode: multiply; transform: translateY(2px); z-index: 1; pointer-events: none; }"
+ },
+ {
+ "name": "Torn Stub",
+ "kind": "card",
+ "refersTo": "tab",
+ "description": "The tab you tore off, carried onto the stance sheet. Its lower edge is ragged because that is where the perforation gave. The word is the heading; nothing labels it.",
+ "html": "<div class=\"ds-torn\"><p class=\"ds-torn-word\">Sceptical</p></div>",
+ "css": ".ds-torn { background: #f0e34a; width: fit-content; max-width: 100%; padding: 0.9rem 1.4rem 1.5rem; clip-path: polygon(0 0, 100% 0, 100% calc(100% - 9px), 96% 100%, 92% calc(100% - 7px), 88% 100%, 84% calc(100% - 8px), 80% 100%, 76% calc(100% - 6px), 72% 100%, 68% calc(100% - 9px), 64% 100%, 60% calc(100% - 7px), 56% 100%, 52% calc(100% - 8px), 48% 100%, 44% calc(100% - 6px), 40% 100%, 36% calc(100% - 9px), 32% 100%, 28% calc(100% - 7px), 24% 100%, 20% calc(100% - 8px), 16% 100%, 12% calc(100% - 6px), 8% 100%, 4% calc(100% - 9px), 0 100%); } .ds-torn-word { font-family: \"Big Shoulders\", Haettenschweiler, \"Arial Narrow\", sans-serif; font-weight: 800; text-transform: uppercase; font-size: 2.75rem; line-height: 0.9; color: #17140f; margin: 0; }"
+ },
+ {
+ "name": "Receipt",
+ "kind": "card",
+ "refersTo": "notice-negative",
+ "description": "The terminal surface for every send, confirm, dead link and sign-in: a bone block between two ink rules with a cyan mark struck across the top rule.",
+ "html": "<div class=\"ds-receipt\"><p class=\"ds-receipt-mark\">Check your inbox</p><p>We sent one message to the address you gave. Open it and confirm.</p></div>",
+ "css": ".ds-receipt { position: relative; max-width: 46rem; margin: 0 auto; padding: 3rem 2.5rem; border-top: 2px solid #17140f; border-bottom: 2px solid #17140f; background: #f7f4e6; font-family: \"Public Sans\", ui-sans-serif, system-ui, sans-serif; color: #17140f; } .ds-receipt::before { content: \"\"; position: absolute; top: -3px; left: 2.5rem; width: 9rem; height: 8px; background: #14a0c8; mix-blend-mode: multiply; } .ds-receipt-mark { font-family: \"Big Shoulders\", Haettenschweiler, \"Arial Narrow\", sans-serif; font-weight: 800; text-transform: uppercase; font-size: 3rem; line-height: 0.92; color: #1b2a8f; margin: 0 0 0.85rem; } .ds-receipt p { margin: 0; font-size: 1.0625rem; line-height: 1.6; }"
+ },
+ {
+ "name": "Ledger Table",
+ "kind": "custom",
+ "refersTo": "body",
+ "description": "The admin tally in the same hand: 2px ink head rule, 1px row rules, tracked uppercase column heads, tabular numerals.",
+ "html": "<table class=\"ds-ledger\"><thead><tr><th>Email</th><th>Stance</th><th>When</th></tr></thead><tbody><tr><td>ada@example.com</td><td>Sceptical</td><td>2026-09-01</td></tr><tr><td>grace@example.com</td><td>Curious</td><td>2026-09-02</td></tr></tbody></table>",
+ "css": ".ds-ledger { width: 100%; border-collapse: collapse; font-family: \"Public Sans\", ui-sans-serif, system-ui, sans-serif; font-size: 0.9375rem; font-variant-numeric: tabular-nums; color: #17140f; } .ds-ledger th { text-align: left; font-size: 0.75rem; letter-spacing: 0.12em; text-transform: uppercase; border-bottom: 2px solid #17140f; padding: 0 0.75rem 0.5rem 0; } .ds-ledger td { border-bottom: 1px solid rgba(23, 20, 15, 0.22); padding: 0.7rem 0.75rem 0.7rem 0; vertical-align: top; }"
+ },
+ {
+ "name": "Refusal Notice",
+ "kind": "custom",
+ "refersTo": "notice-negative",
+ "description": "Form error. Ruled along its top edge, never a left-border callout and never a rounded alert.",
+ "html": "<p class=\"ds-notice\" role=\"alert\">That address could not be accepted.</p>",
+ "css": ".ds-notice { border-left: 0; border-top: 2px solid #8e2417; background: rgba(158, 42, 26, 0.14); color: #8e2417; padding: 0.75rem 0.9rem; margin: 0 0 1.25rem; font-family: \"Public Sans\", ui-sans-serif, system-ui, sans-serif; font-weight: 700; border-radius: 0; }"
+ }
+ ],
+ "narrative": {
+ "northStar": "The Flyposted Bill",
+ "overview": "This is a sheet pasted on a wall, photographed as it is: cheap coloured stock, two inks, a heavy impression, and the halftone dots of a job that was printed rather than rendered. Nothing here is a card floating on a canvas. The stock is the surface — full-bleed acid yellow on the bill itself — and the reading surfaces drop to bone because acid yellow is a shout and a shout cannot also be a paragraph. Depth is ink and perforation. The one exception is a tear-off tab lifting off the wall, which is a thing paper actually does.\n\nThe two inks are the whole colour theory. Ultramarine is the first ink; cyan is the second; the green is not a chosen colour at all but the consequence of cyan multiplied into yellow stock, offset a couple of pixels off true because a two-colour job never registers perfectly. That misregistration is the tell that the thing was printed, and it is load-bearing: remove the offset or swap the blend and the world stops being a press and becomes a gradient.\n\nTypographically it is a condensed display face shouting in caps against a plain grotesque doing the reading. The composition is hard: ruled columns, ruled underlines for fields, square dashes, zero radius anywhere. It refuses the centred single-field waitlist page — the page sorts you before it asks you for anything, and the primary action on the first surface is tearing off a tab, not pressing a button.",
+ "keyCharacteristics": [
+ "Drenched stock, not accented white; two paper colours, never a screen white",
+ "Two inks that build where they cross; green only as a product of overprint",
+ "Zero radius everywhere — a printed sheet has corners",
+ "Perforations, dashed rules and torn edges instead of borders and shadows",
+ "Condensed uppercase display against a quiet body grotesque",
+ "Self-hosted faces only; no third-party font origin exists in this world"
+ ],
+ "rules": [
+ { "name": "The Overprint Rule", "body": "Green is never picked from a swatch. Wherever ink crosses ink, the upper layer is mix-blend-mode: multiply so the inks build and darken. screen is a defect in this world, not an option; it lightens and turns a press into a light-box.", "section": "colors" },
+ { "name": "The Misregistration Rule", "body": "Every overprint band sits 1–2px off true (translateY(2px) on the shout, 1px on the stamp). The offset is the evidence of a two-colour job. It is not a rendering bug and must not be \"corrected\".", "section": "colors" },
+ { "name": "The Two-Stock Rule", "body": "There are exactly two grounds: acid stock for the bill and its tabs, bone for everything meant to be read. No third surface colour, and no white anywhere — white is a screen colour and this is a printed thing.", "section": "colors" },
+ { "name": "The Self-Hosted Faces Rule", "body": "Both families are served from static/fonts/*.woff2 with font-display: swap. This is a constraint, not a preference: the app ships default-src 'self', so a third-party font link is blocked at the browser. Never introduce a font CDN, and never add a face without vendoring the woff2.", "section": "typography" },
+ { "name": "The Display-Is-Caps Rule", "body": "The condensed face appears only uppercase, only at 800, and only for shouts, marks, titles and tab words. It is never used for 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.", "section": "typography" },
+ { "name": "The No-Costume-Mono Rule", "body": "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 font-variant-numeric: tabular-nums, not from a mono face.", "section": "typography" },
+ { "name": "The One Shadow Rule", "body": "A shadow exists in this world only to show paper leaving the wall. Nothing else is elevated: no panels, no menus, no hovering cards. A hard offset shadow with no blur belongs to a different world and is not this one.", "section": "elevation" },
+ { "name": "The Halftone Rule", "body": "Paper texture is the 6px inline-SVG dot screen at low opacity, multiplied into the stock. Never a raster texture, never a bevel, emboss, gradient or inner shadow.", "section": "elevation" },
+ { "name": "The Equal Tabs Rule", "body": "Tabs are never numbered, ranked, ordered by emphasis, or given a \"recommended\" treatment. Three positions numbered 1-2-3 imply a rank; these three are equal by design, and the only differences between them are the word and the address.", "section": "components" },
+ { "name": "The Tab Carries Its Address Rule", "body": "A tear-off tab exists to be the thing you walk away holding, so it prints its own destination. A separate \"take one\" call to action beside it says the same thing twice.", "section": "components" }
+ ],
+ "dos": [
+ "Do build every ink-over-ink crossing with mix-blend-mode: multiply and a 1–2px offset, so the colour is produced by the press rather than picked.",
+ "Do keep radius at 0 on every new surface and control, including restating border-radius: 0 on native controls.",
+ "Do put new reading surfaces on bone and reserve the acid stock for the bill, its tabs, and stubs torn from it.",
+ "Do draw dividers as perforations, dashed 2px rules, or solid 2px ink rules, and fields as ruled underlines.",
+ "Do vendor any new face into static/fonts/ as woff2; the CSP serves default-src 'self' and an external font link is blocked at the browser.",
+ "Do keep tokens.css byte-identical to the framework's copy and make all visual changes in theme.css (palette, faces, shape, browser surfaces) or bill.css (composition). internal/movementtest/vendored_test.go records that divergence deliberately."
+ ],
+ "donts": [
+ "Don't use screen, overlay, lighten, or a flat green fill to make the overprint colour. Cyan on its own is never a surface colour.",
+ "Don't \"fix\" the 2px band offset, the ragged clip-path edge, or the square perforation dashes — each is authored, and each is the evidence that this was printed.",
+ "Don't add a second shadow, an elevated panel, or a card. Elevation exists only as a tab leaving the wall.",
+ "Don't number, rank or otherwise differentiate the three tabs.",
+ "Don't introduce white (#fff) as a surface, or a third stock colour.",
+ "Don't set running text, labels or UI copy in the monospace stack, and don't set the display face in anything but uppercase 800.",
+ "Don't replace the halftone with a raster texture, a gradient, or a bevel.",
+ "Don't centre a single email field as a surface strategy; forms sit beside the argument they belong to, in the sheet's two-up grid."
+ ]
+ }
+}
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..686582e
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,402 @@
+---
+name: Movement
+description: A flyposted bill — acid stock, two inks, and the green that only exists where they overprint.
+colors:
+ stock: "#f0e34a"
+ bone: "#f7f4e6"
+ ink: "#17140f"
+ blue: "#1b2a8f"
+ blue-strong: "#121d68"
+ cyan: "#14a0c8"
+ overprint: "#2f6b46"
+ dim-on-stock: "#6b5f14"
+ dim-on-bone: "#6a6455"
+ faint: "#8b8574"
+ rule: "rgba(23, 20, 15, 0.22)"
+ rule-on-stock: "rgba(23, 20, 15, 0.3)"
+ rule-strong: "rgba(23, 20, 15, 0.55)"
+ negative-fg: "#8e2417"
+ negative-bg: "rgba(158, 42, 26, 0.14)"
+typography:
+ shout:
+ fontFamily: "Big Shoulders, Haettenschweiler, Arial Narrow, sans-serif"
+ fontSize: "clamp(4rem, 16vw, 10.5rem)"
+ fontWeight: 800
+ lineHeight: 0.84
+ letterSpacing: "-0.02em"
+ headline:
+ fontFamily: "Big Shoulders, Haettenschweiler, Arial Narrow, sans-serif"
+ fontSize: "clamp(1.9rem, 5vw, 3.25rem)"
+ fontWeight: 800
+ lineHeight: 0.92
+ letterSpacing: "normal"
+ title:
+ fontFamily: "Big Shoulders, Haettenschweiler, Arial Narrow, sans-serif"
+ fontSize: "clamp(1.35rem, 3vw, 1.9rem)"
+ fontWeight: 800
+ lineHeight: 1
+ letterSpacing: "normal"
+ body:
+ fontFamily: "Public Sans, ui-sans-serif, system-ui, -apple-system, sans-serif"
+ fontSize: "clamp(1rem, 1.15vw, 1.125rem)"
+ fontWeight: 400
+ lineHeight: 1.55
+ letterSpacing: "normal"
+ reply:
+ fontFamily: "Public Sans, ui-sans-serif, system-ui, -apple-system, sans-serif"
+ fontSize: "clamp(1.0625rem, 1.3vw, 1.1875rem)"
+ fontWeight: 400
+ lineHeight: 1.62
+ letterSpacing: "normal"
+ label:
+ fontFamily: "Public Sans, ui-sans-serif, system-ui, -apple-system, sans-serif"
+ fontSize: "0.8125rem"
+ fontWeight: 700
+ lineHeight: 1.3
+ letterSpacing: "0.06em"
+ stamp:
+ fontFamily: "Public Sans, ui-sans-serif, system-ui, -apple-system, sans-serif"
+ fontSize: "0.875rem"
+ fontWeight: 700
+ lineHeight: 1.2
+ letterSpacing: "0.14em"
+rounded:
+ none: "0"
+spacing:
+ hair: "0.35rem"
+ tight: "0.5rem"
+ field: "1.35rem"
+ block: "1.25rem"
+ gutter: "clamp(1.5rem, 4vw, 3.5rem)"
+ band: "clamp(1.75rem, 4vw, 3rem)"
+ page-x: "clamp(1.25rem, 4vw, 3.5rem)"
+ page-y: "clamp(1.25rem, 3vw, 2.5rem)"
+components:
+ stamp:
+ backgroundColor: "{colors.blue}"
+ textColor: "{colors.stock}"
+ typography: "{typography.stamp}"
+ rounded: "{rounded.none}"
+ padding: "0.85rem 1.75rem"
+ stamp-hover:
+ backgroundColor: "{colors.blue-strong}"
+ textColor: "{colors.stock}"
+ stamp-disabled:
+ backgroundColor: "transparent"
+ textColor: "{colors.dim-on-bone}"
+ rounded: "{rounded.none}"
+ tab:
+ backgroundColor: "{colors.stock}"
+ textColor: "{colors.ink}"
+ typography: "{typography.headline}"
+ rounded: "{rounded.none}"
+ padding: "0.9rem 1.1rem 1rem"
+ height: "clamp(5rem, 7vw, 6.5rem)"
+ field-input:
+ backgroundColor: "transparent"
+ textColor: "{colors.ink}"
+ typography: "{typography.body}"
+ rounded: "{rounded.none}"
+ padding: "0.45rem 0"
+ field-textarea:
+ backgroundColor: "rgba(23, 20, 15, 0.05)"
+ textColor: "{colors.ink}"
+ typography: "{typography.body}"
+ rounded: "{rounded.none}"
+ padding: "0.55rem 0.7rem"
+ notice-negative:
+ backgroundColor: "{colors.negative-bg}"
+ textColor: "{colors.negative-fg}"
+ rounded: "{rounded.none}"
+ padding: "0.75rem 0.9rem"
+---
+
+# Design System: Movement
+
+## Overview
+
+**Creative North Star: "The Flyposted Bill"**
+
+This is a sheet pasted on a wall, photographed as it is: cheap coloured stock,
+two inks, a heavy impression, and the halftone dots of a job that was printed
+rather than rendered. Nothing here is a card floating on a canvas. The stock is
+the surface — full-bleed acid yellow on the bill itself — and the reading
+surfaces drop to bone because acid yellow is a shout and a shout cannot also be
+a paragraph. Depth is ink and perforation. The one exception is a tear-off tab
+lifting off the wall, which is a thing paper actually does.
+
+The two inks are the whole colour theory. Ultramarine is the first ink; cyan is
+the second; the green is not a chosen colour at all but the consequence of cyan
+multiplied into yellow stock, offset a couple of pixels off true because a
+two-colour job never registers perfectly. That misregistration is the tell that
+the thing was printed, and it is load-bearing: remove the offset or swap the
+blend and the world stops being a press and becomes a gradient.
+
+Typographically it is a condensed display face shouting in caps against a plain
+grotesque doing the reading. The composition is hard: ruled columns, ruled
+underlines for fields, square dashes, zero radius anywhere. It refuses the
+centred single-field waitlist page — the page sorts you before it asks you for
+anything, and the primary action on the first surface is tearing off a tab, not
+pressing a button.
+
+**Key Characteristics:**
+- Drenched stock, not accented white; two paper colours, never a screen white
+- Two inks that build where they cross; green only as a product of overprint
+- Zero radius everywhere — a printed sheet has corners
+- Perforations, dashed rules and torn edges instead of borders and shadows
+- Condensed uppercase display against a quiet body grotesque
+- Self-hosted faces only; no third-party font origin exists in this world
+
+## Colors
+
+Two paper stocks and two inks, plus the third colour the inks make between them.
+
+### Primary
+- **Ultramarine Ink** (`{colors.blue}`): the first ink. Every headline that
+ carries a claim, every outbound link's 3px underline, the stamp button's
+ ground, the caret, the selection ground, and the focus outline. It is the
+ voice of the press.
+- **Press Cyan** (`{colors.cyan}`): the second ink, and never used as a flat
+ colour on its own. It exists only as an overprint band multiplied into
+ whatever is underneath: the stripe across the shout, the strike across the
+ stamp's face, the mark struck across a receipt's top rule.
+
+### Secondary
+- **Overprint Green** (`{colors.overprint}`): what cyan makes of yellow stock.
+ Recorded as a token because status tones are tinted from it, but it is a
+ *result*, not a paint. Where it appears on a surface it is produced by
+ multiply, not painted on.
+
+### Neutral
+- **Acid Stock** (`{colors.stock}`): the bill's paper. Full-bleed on the first
+ surface, and the ground of every tear-off tab and every torn stub carried
+ onto a later page. Also the text colour on ultramarine.
+- **Bone** (`{colors.bone}`): the reading paper. Every stance sheet, receipt,
+ sign-in and ledger sits on it. Warm, never white.
+- **Press Ink** (`{colors.ink}`): body text, perforations, table head rules,
+ field underlines.
+- **Thinned Ink on Stock** (`{colors.dim-on-stock}`) and **Thinned Ink on Bone**
+ (`{colors.dim-on-bone}`): secondary text, one per stock. The bill page
+ swaps the muted token to the yellow-safe one on its own surface.
+- **Rules** (`{colors.rule}`, `{colors.rule-on-stock}`, `{colors.rule-strong}`):
+ hairlines and column rules; the stock variant is heavier because yellow
+ swallows a light rule.
+
+### Named Rules
+**The Overprint Rule.** Green is never picked from a swatch. Wherever ink
+crosses ink, the upper layer is `mix-blend-mode: multiply` so the inks *build*
+and darken. `screen` is a defect in this world, not an option; it lightens and
+turns a press into a light-box.
+
+**The Misregistration Rule.** Every overprint band sits 1–2px off true
+(`transform: translateY(2px)` on the shout, `1px` on the stamp). The offset is
+the evidence of a two-colour job. It is not a rendering bug and must not be
+"corrected".
+
+**The Two-Stock Rule.** There are exactly two grounds: acid stock for the bill
+and its tabs, bone for everything meant to be read. No third surface colour, and
+no white anywhere — white is a screen colour and this is a printed thing.
+
+## Typography
+
+**Display Font:** Big Shoulders 800 (with Haettenschweiler, Arial Narrow)
+**Body Font:** Public Sans 400/700 (with ui-sans-serif, system-ui)
+
+**Character:** A tall condensed grotesque shouting in uppercase against a plain,
+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.
+
+### Named Rules
+**The Self-Hosted Faces Rule.** Both families are served from
+`static/fonts/*.woff2` with `font-display: swap`. This is a constraint, not a
+preference: the app ships `default-src 'self'`, so a third-party font link is
+blocked at the browser. Never introduce a font CDN, and never add a face without
+vendoring the woff2.
+
+**The Display-Is-Caps Rule.** The condensed face appears only uppercase, only at
+800, and only for shouts, marks, titles and tab words. It is never used for
+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 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
+`font-variant-numeric: tabular-nums`, not from a mono face.
+
+## Layout
+
+One column of measured width per surface, never a centred narrow strip. The bill
+and the stance sheet share a `78rem` max width with fluid page padding
+(`{spacing.page-y} {spacing.page-x}`); the ledger sits at `66rem`, the receipt at
+`46rem`.
+
+The bill fills the viewport (`min-height: 100svh`) only above `46rem` wide *and*
+`40rem` tall, so the tear strip lands near the fold on real screens without
+stranding it on short ones. Its argument sets in two hard columns divided by a
+1px column rule, collapsing to one column below `46rem`.
+
+The stance sheet is a two-up grid under a single 2px rule: reply
+(`minmax(0, 1.55fr)`) beside slip (`minmax(19rem, 1fr)`), stacking below `60rem`.
+The tear strip is three equal grid columns, stacking to one below `46rem` where
+the tabs' vertical dashed dividers become horizontal ones.
+
+Rhythm is fluid rather than stepped: `clamp()` bands do the vertical spacing
+(`{spacing.band}` between major blocks, `{spacing.gutter}` for gutters), with
+fixed small steps only inside components (`{spacing.field}` between fields,
+`{spacing.hair}` label-to-input).
+
+## Elevation & Depth
+
+The system is flat. Depth is ink density, rules, perforation and torn edges — not
+layering. Both stocks carry an authored inline-SVG halftone: a two-dot screen at
+a 6px pitch, at 13% opacity on the acid stock and 7% on bone. It is a dot screen,
+not a texture image, and it is what makes the stock read as paper.
+
+There is exactly one shadow, and it belongs to a tab lifting off the wall on
+hover or focus. It carries an offset *and* a blur because that is what a real
+shadow has.
+
+### Shadow Vocabulary
+- **Tab lift** (`box-shadow: 0 10px 30px rgba(23, 20, 15, 0.28→0.32)`, paired
+ with `translateY(-10px)` over `260ms cubic-bezier(0.16, 1, 0.3, 1)`): the only
+ shadow any app surface may use. Suppressed below `46rem` (where the tab tints
+ to bone instead) and under `prefers-reduced-motion: reduce`.
+
+### Named Rules
+**The One Shadow Rule.** A shadow exists in this world only to show paper
+leaving the wall. Nothing else is elevated: no panels, no menus, no hovering
+cards. A hard offset shadow with no blur belongs to a different world and is not
+this one.
+
+**The Halftone Rule.** Paper texture is the 6px inline-SVG dot screen at low
+opacity, multiplied into the stock. Never a raster texture, never a bevel,
+emboss, gradient or inner shadow.
+
+## Shapes
+
+Radius is zero, everywhere, on purpose: `--rst-radius`, `--rst-radius-sm` and
+`--rst-radius-pill` are all `0`, and every app-authored control restates
+`border-radius: 0`. A printed sheet has corners.
+
+Form language is drawn with rules, not boxes:
+- **Perforation:** a 2px `repeating-linear-gradient` at 7px on / 7px off, so the
+ dashes stay square. Not a dashed border.
+- **Dashed dividers:** 2px dashed ink between tear-off tabs.
+- **Torn edge:** a 28-point `clip-path` polygon giving the carried-over stub a
+ ragged lower edge, because that is where the perforation gave.
+- **Rules:** 2px solid ink for structural divisions (strip foot, sheet head,
+ receipt top and bottom, table head); 1px translucent ink for column rules and
+ table rows.
+- **Fields:** ruled underlines, never outlined boxes.
+
+## Components
+
+### Stamp (the primary button)
+A blue block with the second ink struck across its face.
+- **Shape:** square (`0`)
+- **Primary:** ultramarine ground, acid-stock lettering, uppercase 700 at
+ `0.875rem` with `0.14em` tracking, padding `0.85rem 1.75rem`; a 7px cyan bar
+ multiplied across at 56% height and offset 1px.
+- **Hover:** ground deepens to `{colors.blue-strong}` over `160ms ease-out`.
+- **Disabled:** ground drops out entirely to a 2px dashed ink outline with
+ thinned lettering, and the cyan strike is removed — an unstruck stamp.
+
+### Tear-off Tab (signature component)
+The primary action of the first surface: three equal tabs hanging off a
+perforation, each printed on the stock and carrying its own address.
+- **Shape:** square, no border except the 2px dashed divider between tabs and a
+ 2px solid rule closing the strip's foot.
+- **Content:** the position word in display caps, and beneath it the URL that tab
+ leads to in `0.8125rem` blue label type. Nothing else.
+- **State:** lifts `-10px` with the single system shadow on hover and
+ focus-visible; on narrow screens it tints to bone instead of lifting.
+
+**The Equal Tabs Rule.** Tabs are never numbered, ranked, ordered by emphasis, or
+given a "recommended" treatment. Three positions numbered 1-2-3 imply a rank;
+these three are equal by design, and the only differences between them are the
+word and the address.
+
+**The Tab Carries Its Address Rule.** A tear-off tab exists to be the thing you
+walk away holding, so it prints its own destination. A separate "take one" call
+to action beside it says the same thing twice.
+
+### Fields
+Ruled lines on paper, one grammar for all of them.
+- **Style:** transparent ground, no border except a 2px solid ink underline,
+ padding `0.45rem 0`, radius `0`. Label above in 700 `0.8125rem` with `0.06em`
+ tracking.
+- **Textarea:** the one variation — a faint `rgba(23,20,15,0.05)` ink tint and
+ boxed padding, present to show the field's extent, not as a fill. It keeps the
+ same square corners and vertical-only resize.
+- **Focus:** the global registration mark — a 3px ultramarine outline offset 2px,
+ square.
+- **Error:** a notice ruled 2px along its *top* edge in the negative tone over a
+ tinted ground; never a left-border callout, never a rounded alert.
+
+### Receipt
+The terminal surface for every send, confirm, dead link and sign-in: a bone block
+between two 2px ink rules, with a cyan mark struck across the top rule and the
+outcome set as a display-caps mark in ultramarine.
+
+### Ledger (admin)
+The same hand, operating rather than persuading: display-caps title in
+ultramarine, tallies where the figure is set in the display face at `1.5em`
+beside body-face copy, and a `border-collapse` table with a 2px ink head rule,
+1px row rules, uppercase `0.75rem`/`0.12em` column heads and tabular numerals.
+
+### Browser surfaces
+Not decoration — the cheapest place to stay consistent. Selection is the inks
+swapped (ultramarine ground, stock lettering), caret is ultramarine, the
+scrollbar is thin ink on transparent, and focus is a square 3px ultramarine
+registration mark.
+
+## Do's and Don'ts
+
+### Do:
+- **Do** build every ink-over-ink crossing with `mix-blend-mode: multiply` and a
+ 1–2px offset, so the colour is produced by the press rather than picked.
+- **Do** keep radius at `0` on every new surface and control, including
+ restating `border-radius: 0` on native controls.
+- **Do** put new reading surfaces on bone and reserve the acid stock for the
+ bill, its tabs, and stubs torn from it.
+- **Do** draw dividers as perforations, dashed 2px rules, or solid 2px ink rules,
+ and fields as ruled underlines.
+- **Do** vendor any new face into `static/fonts/` as woff2; the CSP serves
+ `default-src 'self'` and an external font link is blocked at the browser.
+- **Do** keep `tokens.css` byte-identical to the framework's copy and make all
+ visual changes in `theme.css` (palette, faces, shape, browser surfaces) or
+ `bill.css` (composition). `internal/movementtest/vendored_test.go` records that
+ divergence deliberately.
+
+### Don't:
+- **Don't** use `screen`, `overlay`, `lighten`, or a flat green fill to make the
+ overprint colour. Cyan on its own is never a surface colour.
+- **Don't** "fix" the 2px band offset, the ragged `clip-path` edge, or the square
+ perforation dashes — each is authored, and each is the evidence that this was
+ printed.
+- **Don't** add a second shadow, an elevated panel, or a card. Elevation exists
+ only as a tab leaving the wall.
+- **Don't** number, rank or otherwise differentiate the three tabs.
+- **Don't** introduce white (`#fff`) as a surface, or a third stock colour.
+- **Don't** set running text, labels or UI copy in the monospace stack, and don't
+ set the display face in anything but uppercase 800.
+- **Don't** replace the halftone with a raster texture, a gradient, or a bevel.
+- **Don't** centre a single email field as a surface strategy; forms sit beside
+ the argument they belong to, in the sheet's two-up grid.