carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1---
2name: CONTRA
3description: A host-blind password manager dressed as a station cloakroom's left-luggage ticket.
4colors:
5 stock: "light-dark(#c8dacf, #131a16)"
6 stock-tint: "light-dark(#b3c9bb, #1d2621)"
7 stock-manila: "light-dark(#ddc78d, #3a3018)"
8 ink: "light-dark(#121512, #e6efe8)"
9 ink-strong: "light-dark(#000000, #ffffff)"
10 ink-soft: "light-dark(#bcd0c3, #232e28)"
11 on-ink: "light-dark(#eef3ee, #101512)"
12 ink-2: "light-dark(#b3301a, #ff7a5c)"
13 text-muted: "light-dark(#3a4741, #a9b8ae)"
14 text-faint: "light-dark(#434f49, #93a399)"
15 line: "light-dark(#a3b8ab, #2c3830)"
16 line-strong: "light-dark(#56655c, #7d8d83)"
17 tone-negative-fg: "light-dark(#8b2310, #f0a293)"
18 tone-negative-bg: "light-dark(#e3c2b6, #2a1b18)"
19 tone-positive-fg: "light-dark(#1d5134, #9fc9ac)"
20 tone-positive-bg: "light-dark(#b6d2be, #16241b)"
21 tone-warning-fg: "light-dark(#57430f, #d6c48a)"
22 tone-warning-bg: "light-dark(#d5cc9c, #24211a)"
23 tone-neutral-fg: "light-dark(#33403a, #c4d2c9)"
24 tone-neutral-bg: "light-dark(#b9cec2, #232e28)"
25typography:
26 display:
27 fontFamily: "League Gothic, Oswald, Archivo Narrow, system-ui, sans-serif"
28 fontSize: "clamp(2.4rem, 6vw, 3.6rem)"
29 fontWeight: 400
30 lineHeight: 1
31 letterSpacing: "0.01em"
32 headline:
33 fontFamily: "League Gothic, Oswald, Archivo Narrow, system-ui, sans-serif"
34 fontSize: "1.9rem"
35 fontWeight: 400
36 lineHeight: 1
37 letterSpacing: "0.04em"
38 title:
39 fontFamily: "League Gothic, Oswald, Archivo Narrow, system-ui, sans-serif"
40 fontSize: "1.35rem"
41 fontWeight: 400
42 lineHeight: 1
43 letterSpacing: "0.04em"
44 numbering:
45 fontFamily: "League Gothic, Oswald, Archivo Narrow, system-ui, sans-serif"
46 fontSize: "clamp(3rem, 12vw, 5rem)"
47 fontWeight: 400
48 lineHeight: 1
49 letterSpacing: "0.14em"
50 fontFeature: "tabular-nums"
51 body:
52 fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
53 fontSize: "0.875rem"
54 fontWeight: 400
55 lineHeight: 1.5
56 letterSpacing: "normal"
57 label:
58 fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
59 fontSize: "0.71875rem"
60 fontWeight: 600
61 lineHeight: 1.2
62 letterSpacing: "0.12em"
63 serial:
64 fontFamily: "ui-monospace, SF Mono, Cascadia Mono, Menlo, Consolas, monospace"
65 fontSize: "0.95em"
66 fontWeight: 400
67 lineHeight: 1.5
68 letterSpacing: "0.06em"
69 fontFeature: "tabular-nums"
70rounded:
71 cut: "0"
72 stamp: "999px"
73spacing:
74 sp-1: "0.25rem"
75 sp-2: "0.5rem"
76 sp-3: "0.75rem"
77 sp-4: "1rem"
78 sp-5: "1.5rem"
79 sp-6: "2.5rem"
80components:
81 button-primary:
82 backgroundColor: "{colors.ink}"
83 textColor: "{colors.on-ink}"
84 typography: "{typography.label}"
85 rounded: "{rounded.cut}"
86 padding: "0.5rem 1.5rem"
87 button-primary-hover:
88 backgroundColor: "{colors.ink-strong}"
89 textColor: "{colors.on-ink}"
90 button-primary-disabled:
91 backgroundColor: "{colors.stock-tint}"
92 textColor: "{colors.text-faint}"
93 button-ruled:
94 backgroundColor: "transparent"
95 textColor: "{colors.ink}"
96 typography: "{typography.label}"
97 rounded: "{rounded.cut}"
98 padding: "0.5rem 1.5rem"
99 button-ruled-hover:
100 backgroundColor: "{colors.ink-soft}"
101 textColor: "{colors.ink}"
102 button-marginal:
103 backgroundColor: "transparent"
104 textColor: "{colors.ink}"
105 rounded: "{rounded.cut}"
106 padding: "0.15rem 0.5rem"
107 field-label:
108 textColor: "{colors.text-muted}"
109 typography: "{typography.label}"
110 field-input:
111 backgroundColor: "transparent"
112 textColor: "{colors.ink}"
113 typography: "{typography.body}"
114 rounded: "{rounded.cut}"
115 padding: "0 0 0.1rem"
116 width: "100%"
117 notice-stamp:
118 backgroundColor: "{colors.tone-negative-bg}"
119 textColor: "{colors.ink}"
120 rounded: "{rounded.cut}"
121 padding: "{spacing.sp-4}"
122 width: "68ch"
123 counterfoil:
124 backgroundColor: "{colors.stock-tint}"
125 textColor: "{colors.ink}"
126 rounded: "{rounded.cut}"
127 padding: "{spacing.sp-4}"
128 vault-shared:
129 backgroundColor: "{colors.stock-manila}"
130 textColor: "{colors.ink}"
131 rounded: "{rounded.cut}"
132 padding: "{spacing.sp-4}"
133 numbering-code:
134 backgroundColor: "transparent"
135 textColor: "{colors.ink-2}"
136 typography: "{typography.numbering}"
137 step-overprint:
138 backgroundColor: "transparent"
139 textColor: "{colors.tone-negative-fg}"
140 typography: "{typography.label}"
141 status-pill:
142 backgroundColor: "{colors.tone-neutral-bg}"
143 textColor: "{colors.tone-neutral-fg}"
144 typography: "{typography.label}"
145 rounded: "{rounded.stamp}"
146 padding: "0.1rem 0.6rem"
147 nav-link:
148 backgroundColor: "transparent"
149 textColor: "{colors.ink}"
150 typography: "{typography.label}"
151 padding: "0.5rem 0"
152 footer-overprint:
153 backgroundColor: "transparent"
154 textColor: "{colors.text-faint}"
155 typography: "{typography.label}"
156 padding: "0.75rem 0"
157---
158
159πŸ€– Machine-authored record of CONTRA's visual system, written from the shipped
160stylesheets and templates rather than from intention. Where the direction
161contract in `layout.html` and the built code disagree, the code is recorded and
162the divergence is noted. This file is the visual system only; the cryptographic
163protocol lives in `PROTOCOL.md`, which carried the name `DESIGN.md` until
1642026-09-04 and has nothing to do with anything below.
165
166# Design System: CONTRA
167
168## Overview
169
170**Creative North Star: "Left Luggage"**
171
172A station cloakroom holds your bag, never learns what is in it, and will not
173release it without the stub. That is the product's argument and it is also the
174material: coloured ticket stock, black ink, one vermilion spent only on the
175numbering machine, ruled fields with small letterspaced caps printed above the
176rule, and one perforation where the ticket tears. The world was not chosen to
177look like a ticket; six of the product's mechanics β€” the counterfoil, the
178never-repeating epoch, the coloured stock that codes a category, the expiring
179claim check, the perforation between client and server β€” already *are* ticket
180mechanics, so the visual system describes the protocol rather than costuming it.
181
182The register is institutional, plain, and unapologetic about consequences,
183which is the same voice `PRODUCT.md` records for the product. Ticket printing
184as a station cloakroom would do it: every mark is doing a job. Ticket printing
185as a fairground would do it β€” grain, distress, torn-paper shadows, rotated
186stamps, fake staples, perforated GIFs used as ornament β€” is refused in
187`contra.css` by name, because the moment the material gets texture it turns
188twee and the institution stops being credible. The category default is refused
189equally and explicitly: no deep navy, no rounded cards, no lock mark, no
190green-for-safe.
191
192Density is that of a printed form rather than a dashboard: one sheet of stock
193edge to edge, no cards, ruled rows for line items, more space above a heading
194than below it. Light is the primary scheme, and that is a decision about a
195scene β€” a person at a desk in daylight with a printer running, holding two
196sheets they were told can never be reissued. Dark is the same counter at night
197and is a complete scheme, not an inversion; every token is declared once as
198`light-dark(<light>, <dark>)` under a single `:root`, and
199`:root[data-theme=light|dark]` setting `color-scheme` is the whole toggle.
200
201**Key Characteristics:**
202- Eau-de-nil ticket stock as the ground, emphatically not cream and never white.
203- The accent is the ink: links and primary buttons are black, not coloured.
204- One second colour (vermilion), spent only on counting and on what cannot be undone.
205- Square-cut throughout (`0` radius); the status pill is the one round thing, and it is a rubber stamp.
206- Flat by conviction: ink on paper has no depth.
207- Five named devices β€” the field box, the numbering, the tear-line, the stamp, the stock code β€” and every screen is built from them.
208
209## Colors
210
211One stock, one ink, one second colour, plus a manila category code; ticket
212printing has no third colour and neither does this.
213
214### Primary
215- **Cloakroom Ink** (`{colors.ink}`): the accent *is* the ink. Body text, headings, link colour, the primary button's fill, the 3px rule under the page title, and the top rule that opens a list. A link is black text that underlines (1px thickness, 0.18em offset, thickening to 2px on hover), never a coloured word. **Ink Strong** (`{colors.ink-strong}`) is the hover state of anything filled with ink; **On Ink** (`{colors.on-ink}`) is the label printed on it; **Ink Soft** (`{colors.ink-soft}`) is the wash a ruled button takes on hover.
216
217### Secondary
218- **Numbering Vermilion** (`{colors.ink-2}`, `--contra-ink-2`): the numbering machine's ink, and the only colour on the page. It prints the handoff claim code, the device ladder's rung counters, and the caret in every text field. Its hue also carries the negative tone, so the second colour lands exactly where the product's consequences are: something being counted, or something that cannot be undone.
219
220### Tertiary
221- **Manila Stock** (`{colors.stock-manila}`, `--contra-stock-2`): the category code, coding exactly one thing β€” a vault that belongs to somebody else and you are on the list. `vault.js` sets `data-shared` on the open vault's root, so the whole sheet changes stock the way a cloakroom prints luggage tickets on different stock from coat tickets. It is also the `::selection` background. Never a badge, never decoration.
222
223### Neutral
224- **Eau-de-Nil Stock** (`{colors.stock}`): the page. `--rst-bg` and `--rst-surface` are the same value on purpose β€” the page is one sheet, and a card would be a second sheet on a counter that only ever holds one.
225- **Printed Tint Block** (`{colors.stock-tint}`): the darker band a ticket uses to group a field. Carries the counterfoil and disabled controls. A flatter surface, never a raised one.
226- **Muted / Faint Text** (`{colors.text-muted}`, `{colors.text-faint}`): field labels, secondary list text and status lines take muted; the tear-line caption, placeholders and the footer overprint take faint.
227- **Hairline** (`{colors.line}`) and **Rule** (`{colors.line-strong}`): the hairline divides rows and closes the footer; the rule is the control boundary β€” field underlines, the perforation's holes, the serial's underline.
228- **Status tones** (`{colors.tone-neutral-fg}` … `{colors.tone-negative-bg}`): the rubber stamps, held at low chroma so the page still reads as stock and ink at a glance. Negative is the exception, because that is the second colour doing its other job.
229
230### Named Rules
231
232**The One Stock, One Ink, One Second Colour Rule.** A screen has a ground, a
233black ink, and at most one vermilion mark. If a new element seems to need a
234fourth colour, it does not need a colour; it needs a rule, a case change, or a
235position.
236
237**The Colour Means A Category Rule.** Manila codes a vault owned by somebody
238else, and nothing else, ever. A colour that does not carry a category does not
239get used.
240
241**The Large-Only Rule.** `--contra-ink-2` measures 4.29:1 on the stock in
242light. It is a **large-text colour only**: 24px and up, or 18.66px bold. Small
243red text is `--rst-tone-negative-fg`, which clears 4.5:1 on its own tint. Never
244reach for the numbering ink at body size β€” darkening it until it passes at 14px
245turns a vermilion into a brick and costs the theme its second colour.
246
247### Accessibility floors
248
249WCAG 2.2 AA is binding (`PRODUCT.md`). `theme.css`'s header carries the full
250measured table for both schemes, computed from the exact hex values; if a token
251changes, its rows are recomputed there. Normal text needs 4.5:1; large text and
252UI component boundaries need 3:1. The tightest text pair in the build is
253`--rst-text-faint` on the tint block at **4.89:1** in light against a 4.5:1
254floor. Text on stock measures 12.60:1 light / 15.06:1 dark; a button label on
255the ink 16.37:1; the control border `--rst-line-strong` 4.22:1 light / 5.06:1
256dark against a 3:1 floor; the numbering ink 4.29:1 light / 6.90:1 dark against
257its large-text 3:1 floor, and 3.76:1 on manila.
258
259- `--rst-line` is a hairline **divider**, not a control border: 1.4.11's 3:1 does not apply to it. Control borders use `--rst-line-strong`.
260- `--rst-header-rule` is decorative β€” the ticket's rule under its title β€” and carries no contrast floor. It resolves to the ink, which makes it the heaviest mark on the sheet.
261- The shadow tokens and the overlay paint translucent layers over unknown grounds, so no ratio applies to them.
262
263## Typography
264
265**Display Font:** League Gothic (self-hosted WOFF2, ~10KB, OFL 1.1, `font-display: swap`), falling back to Oswald, Archivo Narrow, then the UI stack.
266**Body / Label Font:** the machine's own UI stack (`system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial`).
267**Serial Font:** `ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas`.
268
269**Character:** an Alternate Gothic revival β€” the condensed face poster and
270ticket printing has been set in for a century β€” over a workhorse UI sans. The
271display face does the ticket's shouting; the sans does the ticket's work. The
272webfont is same-origin because this app fetches nothing from anywhere, and a
273CDN font would be one more party the browser trusts on a page holding vault
274keys.
275
276### Hierarchy
277- **Display / h1** (League Gothic 400, `clamp(2.4rem, 6vw, 3.6rem)`, line-height 1, 0.01em): the page title, in **sentence case**, over a 3px ink rule with `0.5rem` of clearance and `1.5rem` beneath. One per screen.
278- **Headline / h2** (League Gothic 400, 1.9rem, line-height 1, 0.04em, uppercase): section labels. `2.5rem` above, `0.75rem` below.
279- **Title / h3** (League Gothic 400, 1.35rem, line-height 1, 0.04em, uppercase): sub-sections and the device ladder's rung headings.
280- **Numbering** (League Gothic 400, `clamp(3rem, 12vw, 5rem)`, tabular, 0.14em, in `{colors.ink-2}`): the handoff claim code, printed at the size a cloakroom prints a stub number, because a person reads it off one screen and types it into another. Rung counters are the same voice at 2.6rem.
281- **Brand overprint** (League Gothic, 1.5rem, 0.14em, uppercase): the issuing office's name in the shell bar.
282- **Body** (UI stack 400, 0.875rem/14px, line-height 1.5): prose is capped at **68ch**; the column itself is sized for forms and lists.
283- **Label** (UI stack 600, 0.71875rem/11.5px, 0.12em, uppercase): field names, disclosure summaries, the tear-line caption, the footer overprint. Buttons are the same voice at 0.78125rem/700/0.14em; nav links at 0.78125rem/600/0.08em; the status line at 0.78125rem/0.06em.
284- **Serial** (mono, 0.95em, tabular figures, 0.06em, underlined with `{colors.line-strong}`, `word-break: break-all`): opaque ids, build stamps, handoff codes β€” values a person transcribes by hand. Monospace here is measurement, not costume, and a serial sits on a ruled line rather than in a grey box.
285
286### Named Rules
287
288**The Caps Are For Labels Rule.** Capitals name things; they do not speak
289sentences. `h2` and `h3` are section labels and are uppercased; `h1` keeps
290sentence case and takes its character from the face and the rule under it,
291because this app's title is sometimes a whole sentence, and a whole sentence in
292condensed capitals is a poster nobody reads and a screen reader may spell out.
293
294**The Overprint Prints What It Is Rule.** Uppercasing stops at a serial. A
295build hash the reader will paste and compare renders in its own case
296(`[data-overprint] code`), because a page that exists to report a value must
297not restyle the value into a lie.
298
299**The Display Face Stays Big Rule.** League Gothic is a poster face. At 11.5px
300with 0.12em tracking it goes spindly and its condensed forms stop being
301legible, so none of the caps furniture β€” labels, buttons, nav, status, the
302overprint β€” uses it. That furniture is set in the UI stack and takes its ticket
303character from case, tracking, weight and the rule it sits on. **Recorded cost,
304accepted in the build:** the small caps render in a different typeface per
305operating system while the display line does not.
306
307## Layout
308
309One centred column, one sheet of stock. The framework's `tokens.css` declares
310the column at `64rem`; **CONTRA narrows it to `60rem`** in `contra.css` and
311gives it `min-block-size: calc(100vh - 8rem)` so the sheet reaches the fold on
312a short page. Column padding is `1.5rem 1rem 2.5rem`. The shell footer matches
313the column at `60rem`.
314
315The sheet has **no border of its own**. It had cut edges for one round and they
316were wrong: the console shell already rules the side of the navigation rail,
317and the page's own border landed about 55px inboard of it, showing two
318competing sheet edges with a dead strip between them. The rail's rule is the
319structural one; the sheet is defined by its ground and its rules, which is how
320a ticket is actually printed.
321
322Spacing is the framework's six-step rhythm β€” `0.25 / 0.5 / 0.75 / 1 / 1.5 /
3232.5rem` β€” and the build uses all six. Vertical rhythm is asymmetric on purpose:
324more space above a heading than below it. Form rows carry no top margin and
325`1rem` beneath.
326
327The shell is a top bar plus a side rail. Below 800px both fold behind **one**
328`<details>` disclosure with two reveals β€” the bar's tail via the
329adjacent-sibling combinator, the rail from the shell root via `:has()` β€”
330written as hide-when-closed, so an engine without `:has()` degrades to
331everything visible rather than to nothing. The summary control is drawn at
332390px.
333
334Reading measures: prose `68ch`, notices `68ch`, checkbox and radio sentence
335labels `68ch`, text inputs `32rem`.
336
337The `@media print` block strips the bar, rail, footer and skip link and removes
338the column cap, so a printed screen comes out black on white with no chrome.
339
340**The One Sheet Rule.** There are no cards. A card is a second sheet of paper
341on a counter that only ever holds one. Grouping is done with a rule, a tint
342block, or a change of stock.
343
344## Elevation & Depth
345
346Flat by conviction. Ink on paper has no depth and nothing in this app floats.
347The shadow tokens exist only because the framework's stylesheet paints with
348`var(--rst-shadow-*)` unconditionally and a theme cannot opt out of a property
349it must declare, so they are set to the least a theme can declare and still be
350declaring something: a single hairline layer, `0 1px 0`. You get an edge, not a
351lift. Depth, where it is needed at all, is carried by tonal layering β€” the tint
352block behind a counterfoil, manila behind a shared vault β€” and by rules.
353
354### Shadow Vocabulary
355- **Pop edge** (`box-shadow: 0 1px 0 light-dark(rgba(18,21,18,0.22), rgba(0,0,0,0.55))`): the framework's popovers and menus.
356- **Knob edge** (`box-shadow: 0 1px 0 light-dark(rgba(18,21,18,0.30), rgba(0,0,0,0.5))`): switch and slider knobs.
357- **Lift edge** (`box-shadow: 0 1px 0 light-dark(rgba(18,21,18,0.12), rgba(0,0,0,0.35))`): the framework's lightest lift.
358- **Overlay** (`light-dark(rgba(18,21,18,0.55), rgba(0,0,0,0.68))`): the only real darkening in the app, because a modal genuinely is a second sheet laid over the first.
359
360**The No Lift Rule.** Never add a shadow to make something look raised. If an
361element must separate from the sheet, give it a rule, a tint, or a perforation.
362The three shadow tokens are inherited obligations held at their minimum, not an
363elevation ramp to climb.
364
365## Shapes
366
367Cut, not moulded. `--rst-radius` and `--rst-radius-sm` are `0`, and every
368control CONTRA draws is a square rectangle: buttons, inputs, the notice stamp,
369the counterfoil, the manila block. The pill radius (`999px`) survives only
370because `tokens.css` paints status pills and row actions with it; in this world
371a pill is a rubber stamp, and a stamp is the one round thing on a ticket.
372
373The recurring geometry is **the rule**: 3px in the ink under the page title,
3742px under every field, 1px in the ink over a list, 1px hairlines between rows
375and above the footer, 3px in the negative foreground across the top of a stamp.
376Weight encodes rank β€” title, control, boundary, divider β€” and the build never
377substitutes a box for a rule.
378
379The one non-rectilinear mark in the app is the perforation, and it is drawn as
380real holes rather than as a dashed border.
381
382## Components
383
384### Buttons
385- **Shape:** square-cut (`0` radius), 2px border in the ink.
386- **Primary:** a solid rectangle of ink (`{colors.ink}` fill, `{colors.on-ink}` label), 0.78125rem/700 uppercase at 0.14em tracking, padded `0.5rem 1.5rem`. What a ticket office rubber-stamps.
387- **Hover:** fill and border shift to `{colors.ink-strong}`. No transition, no lift, no transform.
388- **Focus:** `outline: 2px solid {colors.ink}` at `3px` offset. The same ring everywhere in the app.
389- **Ruled (secondary):** the same rectangle ruled rather than filled β€” transparent fill, ink label and border; hover washes to `{colors.ink-soft}`. Used for `type="button"`, cancel, edit, delete.
390- **Marginal:** row-level edit and delete drop to a 1px border and `0.15rem 0.5rem` of padding. They are marginal notes on a line item, not stamps.
391- **Disabled:** tint-block fill, faint text, `{colors.line-strong}` border, `cursor: not-allowed`.
392
393### Inputs / Fields β€” Device 1, THE FIELD BOX
394- **Style:** no box. A small letterspaced cap label sits directly on a 2px `{colors.line-strong}` rule, with the value on the rule. Transparent background, no border but the underline, `line-height: 1.2` and `padding: 0 0 0.1rem` so the value sits close under its label β€” the label names the rule beneath it rather than heading an empty line. Width 100%, capped at `32rem`.
395- **Focus:** the underline turns to the numbering vermilion and the 2px ink outline appears at 3px offset. The caret is vermilion in every field, always.
396- **Placeholder:** `{colors.text-faint}`.
397- **Checkbox / radio:** opt out of the caps entirely β€” a checkbox label is a sentence, so it takes body size, normal tracking, sentence case, and a `68ch` measure. `accent-color` is the vermilion; the control is `1.1rem` square.
398- **Textarea:** `6rem` minimum block size, vertical resize only.
399
400### Lists
401Ruled rows, never cards. A 1px ink rule opens the list, 1px hairlines separate
402rows, rows are baseline-aligned and wrap from a `12rem` flex basis with
403`0.75rem` of vertical padding. Secondary text in the row is muted.
404
405### Navigation
406Rail links are uppercase 0.78125rem/600 at 0.08em, undecorated, with `0.5rem`
407of vertical padding, underlining on hover. The shell's disclosure summaries
408take the label voice (0.71875rem/700/0.12em uppercase, square) so a control
409never arrives in the framework's title case.
410
411### Status
412The live region reads as a printed endorsement rather than a toast: muted,
4130.78125rem, 0.06em, uppercase, with `1.4em` of reserved height so a message
414appearing does not shift the sheet.
415
416### Signature components
417
418**Device 2 β€” THE NUMBERING.** Anything counted prints tabular in the second
419colour. Three live instances and no others: `[data-code]`, the handoff claim
420code, in the display face at `clamp(3rem, 12vw, 5rem)`; `[data-rungs]`
421counters, the device ladder, at 2.6rem in the display face, absolutely
422positioned in a 3.5rem gutter beside each rung; and `[rst-steps]`, the signup
423step overprint, which is *small* and therefore prints in
424`{colors.tone-negative-fg}`, never in the numbering ink. Section numbers are
425usually decoration; these carry meaning β€” the rungs are ordered by what works
426with the least, and the last one always works.
427
428**Device 3 β€” THE TEAR-LINE.** A real perforation: `radial-gradient` holes,
4291.7px solid to 1.8px transparent, repeated at a fixed 12px pitch
430(`--contra-perf`, declared once so every tear-line in the app is the same
431machine), 9px tall, flanking a centred uppercase caption. **Exactly one per
432screen, and only on a genuine client/server boundary** β€” sign-in, where it runs
433through the middle of the form because that is where the boundary genuinely is,
434and the vault. Above it what the server holds; below it what only this browser
435knows. Never decoration, never twice.
436
437**Device 4 β€” THE STAMP.** `[data-notice]` and `[data-kind="warning"]`: a block
438on the negative tint with a **3px rule across the top** in the negative
439foreground, `1rem` padding, a `68ch` measure, and strong text in the negative
440foreground. This is how a ticket prints a condition of carriage. Never a
441coloured left border.
442
443**Device 5 β€” THE STOCK CODE.** `[data-vault][data-shared]` prints the entire
444open vault on manila with `1rem` of padding, set from `vault.js` when the vault
445is not owned. The whole sheet changes stock; there is no "shared" badge.
446
447**The counterfoil.** `[data-counterfoil]` is the tint block above the tear-line
448carrying the server's half of the ticket β€” opaque ids, counts, times. It is the
449only place in the app that changes colour without changing meaning, and the
450meaning it carries is "this side is theirs".
451
452**The overprint.** `[data-overprint]` ends the sheet: small, faint,
453letterspaced caps under a hairline, aligned to the start, carrying the build
454serial. A sheet with nothing at the foot is a sheet that just stops.
455
456### Motion
457
458**The One Authored Moment Rule.** The world's native motion is the tear, so
459there is exactly one animation in the app: the vault opening. The sealed half
460below the line is revealed once on unlock β€” `clip-path: inset(0 0 100% 0)` to
461`inset(0 0 0 0)` with opacity, `620ms`, `cubic-bezier(0.16, 1, 0.3, 1)`, `both`
462β€” easing the way paper separates, fast at the start and settling at the end.
463Nothing else animates; there is not one hover transition in `contra.css`. The
464keyframes' end state is the visible one, so an engine that ignores the
465animation shows the content immediately, and `prefers-reduced-motion: reduce`
466removes it outright.
467
468### Browser surfaces
469
470The parts nobody drew still carry the design: selection is manila under ink,
471scrollbars are thin in `{colors.line-strong}` on the tint block, and the caret
472is the numbering vermilion. On a page whose whole argument is deliberateness, a
473default that belongs to no design system shows.
474
475## Do's and Don'ts
476
477### Do:
478- **Do** print the page on one sheet: `{colors.stock}` edge to edge, the column at `60rem`, no border, no card.
479- **Do** build every input as the field box β€” cap label, 2px rule, value sitting on the rule.
480- **Do** keep the second colour large: 24px up, or 18.66px bold. Small red is `{colors.tone-negative-fg}`.
481- **Do** draw the tear-line only where a real client/server boundary exists, once per screen, at the shared 12px pitch.
482- **Do** group with a rule, a tint block, or a change of stock.
483- **Do** set headings in the display face and keep `h1` in sentence case over its 3px ink rule.
484- **Do** recompute the contrast rows in `theme.css`'s header whenever a colour token changes; the table is the record and 4.5:1 / 3:1 are binding.
485- **Do** let `code` print its own case and its own tabular figures β€” a serial is transcribed, not styled.
486- **Do** declare any new token once as `light-dark(<light>, <dark>)` under the single `:root`, so both schemes stay complete and the toggle keeps working.
487
488### Don't:
489- **Don't** edit `internal/contra/static/tokens.css`. It is vendored from the rastrillo framework and pinned byte-identical by a test; it contributes the spacing steps, the type scale, the framework component rules and the `64rem` column default. CONTRA's overrides go in `theme.css` (colour, type, shape) and `contra.css` (grammar and components), which loads last and wins.
490- **Don't** add a card, a rounded corner, or a shadow that reads as a lift. Radius is `0` except the status pill, which is a rubber stamp.
491- **Don't** add texture: no distress, no paper grain, no torn-paper shadows, no rotated stamps, no fake staples, no perforated GIFs as decoration. That is a fairground; this is a station cloakroom.
492- **Don't** reach for the category default β€” deep navy, rounded cards, a lock mark, green-for-safe.
493- **Don't** colour a link or a primary button. The accent is the ink.
494- **Don't** spend manila on anything but a vault owned by somebody else, or vermilion on anything but counting and the irreversible.
495- **Don't** set a sentence in condensed capitals, and don't set caps furniture in League Gothic β€” it is a poster face and goes spindly at label size.
496- **Don't** add hover transitions or a second animation. One authored moment; everything else is instant.
497- **Don't** put a notice behind a coloured left border; it is a top-ruled stamped block.
498
499## Known gaps in the shipped build
500
501Recorded so the next pass inherits the truth rather than rediscovering it.
502These are defects or accepted costs the build carries; none of them is a rule
503for a new surface to imitate.
504
505- **The caps furniture is in the system UI stack, not the display face.** The reasoning is written above the `label` rule in `contra.css` and is a legibility argument, not an omission. The accepted cost: the small caps render in a different typeface per operating system while the display line does not.
506- **The Emergency Kit PDFs** (`internal/contra/static/crypto/kit.js`, `pdf.js`) still use PDF base-14 fonts and have not been brought into this world. It is the product's only printed artefact and the largest outstanding item. Do not read its current typography as a system decision.
507- **`index.html`'s prose** carries two stale sentences awaiting a copy review. Copy, not visual system; out of scope for this file.
508