carlos / contra Public
--- name: CONTRA description: A host-blind password manager dressed as a station cloakroom's left-luggage ticket. colors: stock: "light-dark(#c8dacf, #131a16)" stock-tint: "light-dark(#b3c9bb, #1d2621)" stock-manila: "light-dark(#ddc78d, #3a3018)" ink: "light-dark(#121512, #e6efe8)" ink-strong: "light-dark(#000000, #ffffff)" ink-soft: "light-dark(#bcd0c3, #232e28)" on-ink: "light-dark(#eef3ee, #101512)" ink-2: "light-dark(#b3301a, #ff7a5c)" text-muted: "light-dark(#3a4741, #a9b8ae)" text-faint: "light-dark(#434f49, #93a399)" line: "light-dark(#a3b8ab, #2c3830)" line-strong: "light-dark(#56655c, #7d8d83)" tone-negative-fg: "light-dark(#8b2310, #f0a293)" tone-negative-bg: "light-dark(#e3c2b6, #2a1b18)" tone-positive-fg: "light-dark(#1d5134, #9fc9ac)" tone-positive-bg: "light-dark(#b6d2be, #16241b)" tone-warning-fg: "light-dark(#57430f, #d6c48a)" tone-warning-bg: "light-dark(#d5cc9c, #24211a)" tone-neutral-fg: "light-dark(#33403a, #c4d2c9)" tone-neutral-bg: "light-dark(#b9cec2, #232e28)" typography: display: fontFamily: "League Gothic, Oswald, Archivo Narrow, system-ui, sans-serif" fontSize: "clamp(2.4rem, 6vw, 3.6rem)" fontWeight: 400 lineHeight: 1 letterSpacing: "0.01em" headline: fontFamily: "League Gothic, Oswald, Archivo Narrow, system-ui, sans-serif" fontSize: "1.9rem" fontWeight: 400 lineHeight: 1 letterSpacing: "0.04em" title: fontFamily: "League Gothic, Oswald, Archivo Narrow, system-ui, sans-serif" fontSize: "1.35rem" fontWeight: 400 lineHeight: 1 letterSpacing: "0.04em" numbering: fontFamily: "League Gothic, Oswald, Archivo Narrow, system-ui, sans-serif" fontSize: "clamp(3rem, 12vw, 5rem)" fontWeight: 400 lineHeight: 1 letterSpacing: "0.14em" fontFeature: "tabular-nums" body: fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif" fontSize: "0.875rem" fontWeight: 400 lineHeight: 1.5 letterSpacing: "normal" label: fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif" fontSize: "0.71875rem" fontWeight: 600 lineHeight: 1.2 letterSpacing: "0.12em" serial: fontFamily: "ui-monospace, SF Mono, Cascadia Mono, Menlo, Consolas, monospace" fontSize: "0.95em" fontWeight: 400 lineHeight: 1.5 letterSpacing: "0.06em" fontFeature: "tabular-nums" rounded: cut: "0" stamp: "999px" spacing: sp-1: "0.25rem" sp-2: "0.5rem" sp-3: "0.75rem" sp-4: "1rem" sp-5: "1.5rem" sp-6: "2.5rem" components: button-primary: backgroundColor: "{colors.ink}" textColor: "{colors.on-ink}" typography: "{typography.label}" rounded: "{rounded.cut}" padding: "0.5rem 1.5rem" button-primary-hover: backgroundColor: "{colors.ink-strong}" textColor: "{colors.on-ink}" button-primary-disabled: backgroundColor: "{colors.stock-tint}" textColor: "{colors.text-faint}" button-ruled: backgroundColor: "transparent" textColor: "{colors.ink}" typography: "{typography.label}" rounded: "{rounded.cut}" padding: "0.5rem 1.5rem" button-ruled-hover: backgroundColor: "{colors.ink-soft}" textColor: "{colors.ink}" button-marginal: backgroundColor: "transparent" textColor: "{colors.ink}" rounded: "{rounded.cut}" padding: "0.15rem 0.5rem" field-label: textColor: "{colors.text-muted}" typography: "{typography.label}" field-input: backgroundColor: "transparent" textColor: "{colors.ink}" typography: "{typography.body}" rounded: "{rounded.cut}" padding: "0 0 0.1rem" width: "100%" notice-stamp: backgroundColor: "{colors.tone-negative-bg}" textColor: "{colors.ink}" rounded: "{rounded.cut}" padding: "{spacing.sp-4}" width: "68ch" counterfoil: backgroundColor: "{colors.stock-tint}" textColor: "{colors.ink}" rounded: "{rounded.cut}" padding: "{spacing.sp-4}" vault-shared: backgroundColor: "{colors.stock-manila}" textColor: "{colors.ink}" rounded: "{rounded.cut}" padding: "{spacing.sp-4}" numbering-code: backgroundColor: "transparent" textColor: "{colors.ink-2}" typography: "{typography.numbering}" step-overprint: backgroundColor: "transparent" textColor: "{colors.tone-negative-fg}" typography: "{typography.label}" status-pill: backgroundColor: "{colors.tone-neutral-bg}" textColor: "{colors.tone-neutral-fg}" typography: "{typography.label}" rounded: "{rounded.stamp}" padding: "0.1rem 0.6rem" nav-link: backgroundColor: "transparent" textColor: "{colors.ink}" typography: "{typography.label}" padding: "0.5rem 0" footer-overprint: backgroundColor: "transparent" textColor: "{colors.text-faint}" typography: "{typography.label}" padding: "0.75rem 0" ---
π€ Machine-authored record of CONTRA's visual system, written from the shipped stylesheets and templates rather than from intention. Where the direction contract in layout.html and the built code disagree, the code is recorded and the divergence is noted. This file is the visual system only; the cryptographic protocol lives in PROTOCOL.md, which carried the name DESIGN.md until 2026-09-04 and has nothing to do with anything below.
Design System: CONTRA
Overview
Creative North Star: "Left Luggage"
A station cloakroom holds your bag, never learns what is in it, and will not release it without the stub. That is the product's argument and it is also the material: coloured ticket stock, black ink, one vermilion spent only on the numbering machine, ruled fields with small letterspaced caps printed above the rule, and one perforation where the ticket tears. The world was not chosen to look like a ticket; six of the product's mechanics β the counterfoil, the never-repeating epoch, the coloured stock that codes a category, the expiring claim check, the perforation between client and server β already are ticket mechanics, so the visual system describes the protocol rather than costuming it.
The register is institutional, plain, and unapologetic about consequences, which is the same voice PRODUCT.md records for the product. Ticket printing as a station cloakroom would do it: every mark is doing a job. Ticket printing as a fairground would do it β grain, distress, torn-paper shadows, rotated stamps, fake staples, perforated GIFs used as ornament β is refused in contra.css by name, because the moment the material gets texture it turns twee and the institution stops being credible. The category default is refused equally and explicitly: no deep navy, no rounded cards, no lock mark, no green-for-safe.
Density is that of a printed form rather than a dashboard: one sheet of stock edge to edge, no cards, ruled rows for line items, more space above a heading than below it. Light is the primary scheme, and that is a decision about a scene β a person at a desk in daylight with a printer running, holding two sheets they were told can never be reissued. Dark is the same counter at night and is a complete scheme, not an inversion; every token is declared once as light-dark(<light>, <dark>) under a single :root, and :root[data-theme=light|dark] setting color-scheme is the whole toggle.
Key Characteristics:
- Eau-de-nil ticket stock as the ground, emphatically not cream and never white.
- The accent is the ink: links and primary buttons are black, not coloured.
- One second colour (vermilion), spent only on counting and on what cannot be undone.
- Square-cut throughout (
0radius); the status pill is the one round thing, and it is a rubber stamp. - Flat by conviction: ink on paper has no depth.
- Five named devices β the field box, the numbering, the tear-line, the stamp, the stock code β and every screen is built from them.
Colors
One stock, one ink, one second colour, plus a manila category code; ticket printing has no third colour and neither does this.
Primary
- 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.
Secondary
- 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.
Tertiary
- 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.jssetsdata-sharedon 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::selectionbackground. Never a badge, never decoration.
Neutral
- Eau-de-Nil Stock (
{colors.stock}): the page.--rst-bgand--rst-surfaceare 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. - 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. - 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. - 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. - 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.
Named Rules
The One Stock, One Ink, One Second Colour Rule. A screen has a ground, a black ink, and at most one vermilion mark. If a new element seems to need a fourth colour, it does not need a colour; it needs a rule, a case change, or a position.
The Colour Means A Category Rule. Manila codes a vault owned by somebody else, and nothing else, ever. A colour that does not carry a category does not get used.
The Large-Only Rule. --contra-ink-2 measures 4.29:1 on the stock in light. It is a large-text colour only: 24px and up, or 18.66px bold. Small red text is --rst-tone-negative-fg, which clears 4.5:1 on its own tint. Never reach for the numbering ink at body size β darkening it until it passes at 14px turns a vermilion into a brick and costs the theme its second colour.
Accessibility floors
WCAG 2.2 AA is binding (PRODUCT.md). theme.css's header carries the full measured table for both schemes, computed from the exact hex values; if a token changes, its rows are recomputed there. Normal text needs 4.5:1; large text and UI component boundaries need 3:1. The tightest text pair in the build is --rst-text-faint on the tint block at 4.89:1 in light against a 4.5:1 floor. Text on stock measures 12.60:1 light / 15.06:1 dark; a button label on the ink 16.37:1; the control border --rst-line-strong 4.22:1 light / 5.06:1 dark against a 3:1 floor; the numbering ink 4.29:1 light / 6.90:1 dark against its large-text 3:1 floor, and 3.76:1 on manila.
--rst-lineis a hairline divider, not a control border: 1.4.11's 3:1 does not apply to it. Control borders use--rst-line-strong.--rst-header-ruleis 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.- The shadow tokens and the overlay paint translucent layers over unknown grounds, so no ratio applies to them.
Typography
Display Font: League Gothic (self-hosted WOFF2, ~10KB, OFL 1.1, font-display: swap), falling back to Oswald, Archivo Narrow, then the UI stack. Body / Label Font: the machine's own UI stack (system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial). Serial Font: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas.
Character: an Alternate Gothic revival β the condensed face poster and ticket printing has been set in for a century β over a workhorse UI sans. The display face does the ticket's shouting; the sans does the ticket's work. The webfont is same-origin because this app fetches nothing from anywhere, and a CDN font would be one more party the browser trusts on a page holding vault keys.
Hierarchy
- 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 with0.5remof clearance and1.5rembeneath. One per screen. - Headline / h2 (League Gothic 400, 1.9rem, line-height 1, 0.04em, uppercase): section labels.
2.5remabove,0.75rembelow. - Title / h3 (League Gothic 400, 1.35rem, line-height 1, 0.04em, uppercase): sub-sections and the device ladder's rung headings.
- 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. - Brand overprint (League Gothic, 1.5rem, 0.14em, uppercase): the issuing office's name in the shell bar.
- 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.
- 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.
- 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.
Named Rules
The Caps Are For Labels Rule. Capitals name things; they do not speak sentences. h2 and h3 are section labels and are uppercased; h1 keeps sentence case and takes its character from the face and the rule under it, because this app's title is sometimes a whole sentence, and a whole sentence in condensed capitals is a poster nobody reads and a screen reader may spell out.
The Overprint Prints What It Is Rule. Uppercasing stops at a serial. A build hash the reader will paste and compare renders in its own case ([data-overprint] code), because a page that exists to report a value must not restyle the value into a lie.
The Display Face Stays Big Rule. League Gothic is a poster face. At 11.5px with 0.12em tracking it goes spindly and its condensed forms stop being legible, so none of the caps furniture β labels, buttons, nav, status, the overprint β uses it. That furniture is set in the UI stack and takes its ticket character from case, tracking, weight and the rule it sits on. Recorded cost, accepted in the build: the small caps render in a different typeface per operating system while the display line does not.
Layout
One centred column, one sheet of stock. The framework's tokens.css declares the column at 64rem; CONTRA narrows it to 60rem in contra.css and gives it min-block-size: calc(100vh - 8rem) so the sheet reaches the fold on a short page. Column padding is 1.5rem 1rem 2.5rem. The shell footer matches the column at 60rem.
The sheet has no border of its own. It had cut edges for one round and they were wrong: the console shell already rules the side of the navigation rail, and the page's own border landed about 55px inboard of it, showing two competing sheet edges with a dead strip between them. The rail's rule is the structural one; the sheet is defined by its ground and its rules, which is how a ticket is actually printed.
Spacing is the framework's six-step rhythm β 0.25 / 0.5 / 0.75 / 1 / 1.5 / 2.5rem β and the build uses all six. Vertical rhythm is asymmetric on purpose: more space above a heading than below it. Form rows carry no top margin and 1rem beneath.
The shell is a top bar plus a side rail. Below 800px both fold behind one <details> disclosure with two reveals β the bar's tail via the adjacent-sibling combinator, the rail from the shell root via :has() β written as hide-when-closed, so an engine without :has() degrades to everything visible rather than to nothing. The summary control is drawn at 390px.
Reading measures: prose 68ch, notices 68ch, checkbox and radio sentence labels 68ch, text inputs 32rem.
The @media print block strips the bar, rail, footer and skip link and removes the column cap, so a printed screen comes out black on white with no chrome.
The One Sheet Rule. There are no cards. A card is a second sheet of paper on a counter that only ever holds one. Grouping is done with a rule, a tint block, or a change of stock.
Elevation & Depth
Flat by conviction. Ink on paper has no depth and nothing in this app floats. The shadow tokens exist only because the framework's stylesheet paints with var(--rst-shadow-*) unconditionally and a theme cannot opt out of a property it must declare, so they are set to the least a theme can declare and still be declaring something: a single hairline layer, 0 1px 0. You get an edge, not a lift. Depth, where it is needed at all, is carried by tonal layering β the tint block behind a counterfoil, manila behind a shared vault β and by rules.
Shadow Vocabulary
- 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. - 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. - 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. - 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.
The No Lift Rule. Never add a shadow to make something look raised. If an element must separate from the sheet, give it a rule, a tint, or a perforation. The three shadow tokens are inherited obligations held at their minimum, not an elevation ramp to climb.
Shapes
Cut, not moulded. --rst-radius and --rst-radius-sm are 0, and every control CONTRA draws is a square rectangle: buttons, inputs, the notice stamp, the counterfoil, the manila block. The pill radius (999px) survives only because tokens.css paints status pills and row actions with it; in this world a pill is a rubber stamp, and a stamp is the one round thing on a ticket.
The recurring geometry is the rule: 3px in the ink under the page title, 2px under every field, 1px in the ink over a list, 1px hairlines between rows and above the footer, 3px in the negative foreground across the top of a stamp. Weight encodes rank β title, control, boundary, divider β and the build never substitutes a box for a rule.
The one non-rectilinear mark in the app is the perforation, and it is drawn as real holes rather than as a dashed border.
Components
Buttons
- Shape: square-cut (
0radius), 2px border in the ink. - Primary: a solid rectangle of ink (
{colors.ink}fill,{colors.on-ink}label), 0.78125rem/700 uppercase at 0.14em tracking, padded0.5rem 1.5rem. What a ticket office rubber-stamps. - Hover: fill and border shift to
{colors.ink-strong}. No transition, no lift, no transform. - Focus:
outline: 2px solid {colors.ink}at3pxoffset. The same ring everywhere in the app. - Ruled (secondary): the same rectangle ruled rather than filled β transparent fill, ink label and border; hover washes to
{colors.ink-soft}. Used fortype="button", cancel, edit, delete. - Marginal: row-level edit and delete drop to a 1px border and
0.15rem 0.5remof padding. They are marginal notes on a line item, not stamps. - Disabled: tint-block fill, faint text,
{colors.line-strong}border,cursor: not-allowed.
Inputs / Fields β Device 1, THE FIELD BOX
- 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.2andpadding: 0 0 0.1remso the value sits close under its label β the label names the rule beneath it rather than heading an empty line. Width 100%, capped at32rem. - 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.
- Placeholder:
{colors.text-faint}. - 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
68chmeasure.accent-coloris the vermilion; the control is1.1remsquare. - Textarea:
6remminimum block size, vertical resize only.
Lists
Ruled rows, never cards. A 1px ink rule opens the list, 1px hairlines separate rows, rows are baseline-aligned and wrap from a 12rem flex basis with 0.75rem of vertical padding. Secondary text in the row is muted.
Navigation
Rail links are uppercase 0.78125rem/600 at 0.08em, undecorated, with 0.5rem of vertical padding, underlining on hover. The shell's disclosure summaries take the label voice (0.71875rem/700/0.12em uppercase, square) so a control never arrives in the framework's title case.
Status
The live region reads as a printed endorsement rather than a toast: muted, 0.78125rem, 0.06em, uppercase, with 1.4em of reserved height so a message appearing does not shift the sheet.
Signature components
Device 2 β THE NUMBERING. Anything counted prints tabular in the second colour. Three live instances and no others: [data-code], the handoff claim code, in the display face at clamp(3rem, 12vw, 5rem); [data-rungs] counters, the device ladder, at 2.6rem in the display face, absolutely positioned in a 3.5rem gutter beside each rung; and [rst-steps], the signup step overprint, which is small and therefore prints in {colors.tone-negative-fg}, never in the numbering ink. Section numbers are usually decoration; these carry meaning β the rungs are ordered by what works with the least, and the last one always works.
Device 3 β THE TEAR-LINE. A real perforation: radial-gradient holes, 1.7px solid to 1.8px transparent, repeated at a fixed 12px pitch (--contra-perf, declared once so every tear-line in the app is the same machine), 9px tall, flanking a centred uppercase caption. Exactly one per screen, and only on a genuine client/server boundary β sign-in, where it runs through the middle of the form because that is where the boundary genuinely is, and the vault. Above it what the server holds; below it what only this browser knows. Never decoration, never twice.
Device 4 β THE STAMP. [data-notice] and [data-kind="warning"]: a block on the negative tint with a 3px rule across the top in the negative foreground, 1rem padding, a 68ch measure, and strong text in the negative foreground. This is how a ticket prints a condition of carriage. Never a coloured left border.
Device 5 β THE STOCK CODE. [data-vault][data-shared] prints the entire open vault on manila with 1rem of padding, set from vault.js when the vault is not owned. The whole sheet changes stock; there is no "shared" badge.
The counterfoil. [data-counterfoil] is the tint block above the tear-line carrying the server's half of the ticket β opaque ids, counts, times. It is the only place in the app that changes colour without changing meaning, and the meaning it carries is "this side is theirs".
The overprint. [data-overprint] ends the sheet: small, faint, letterspaced caps under a hairline, aligned to the start, carrying the build serial. A sheet with nothing at the foot is a sheet that just stops.
Motion
The One Authored Moment Rule. The world's native motion is the tear, so there is exactly one animation in the app: the vault opening. The sealed half below the line is revealed once on unlock β clip-path: inset(0 0 100% 0) to inset(0 0 0 0) with opacity, 620ms, cubic-bezier(0.16, 1, 0.3, 1), both β easing the way paper separates, fast at the start and settling at the end. Nothing else animates; there is not one hover transition in contra.css. The keyframes' end state is the visible one, so an engine that ignores the animation shows the content immediately, and prefers-reduced-motion: reduce removes it outright.
Browser surfaces
The parts nobody drew still carry the design: selection is manila under ink, scrollbars are thin in {colors.line-strong} on the tint block, and the caret is the numbering vermilion. On a page whose whole argument is deliberateness, a default that belongs to no design system shows.
Do's and Don'ts
Do:
- Do print the page on one sheet:
{colors.stock}edge to edge, the column at60rem, no border, no card. - Do build every input as the field box β cap label, 2px rule, value sitting on the rule.
- Do keep the second colour large: 24px up, or 18.66px bold. Small red is
{colors.tone-negative-fg}. - Do draw the tear-line only where a real client/server boundary exists, once per screen, at the shared 12px pitch.
- Do group with a rule, a tint block, or a change of stock.
- Do set headings in the display face and keep
h1in sentence case over its 3px ink rule. - 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. - Do let
codeprint its own case and its own tabular figures β a serial is transcribed, not styled. - 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.
Don't:
- 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 the64remcolumn default. CONTRA's overrides go intheme.css(colour, type, shape) andcontra.css(grammar and components), which loads last and wins. - Don't add a card, a rounded corner, or a shadow that reads as a lift. Radius is
0except the status pill, which is a rubber stamp. - 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.
- Don't reach for the category default β deep navy, rounded cards, a lock mark, green-for-safe.
- Don't colour a link or a primary button. The accent is the ink.
- Don't spend manila on anything but a vault owned by somebody else, or vermilion on anything but counting and the irreversible.
- 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.
- Don't add hover transitions or a second animation. One authored moment; everything else is instant.
- Don't put a notice behind a coloured left border; it is a top-ruled stamped block.
Known gaps in the shipped build
Recorded so the next pass inherits the truth rather than rediscovering it. These are defects or accepted costs the build carries; none of them is a rule for a new surface to imitate.
- The caps furniture is in the system UI stack, not the display face. The reasoning is written above the
labelrule incontra.cssand 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. - 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. index.html's prose carries two stale sentences awaiting a copy review. Copy, not visual system; out of scope for this file.