{ "schemaVersion": 2, "generatedAt": "2026-09-05T00:00:00Z", "title": "Design System: CONTRA", "extensions": { "colorMeta": { "stock": { "role": "neutral", "displayName": "Eau-de-Nil Ticket Stock", "canonical": "light-dark(#c8dacf, #131a16)", "note": "The page itself. --rst-bg and --rst-surface are the same value: one sheet, no cards.", "tonalRamp": ["#0d1310", "#16201a", "#243329", "#3b5346", "#5b7d6c", "#87a996", "#c8dacf", "#e6f0ea"] }, "stock-tint": { "role": "neutral", "displayName": "Printed Tint Block", "canonical": "light-dark(#b3c9bb, #1d2621)", "note": "The band a ticket uses to group a field. Carries the counterfoil and disabled controls.", "tonalRamp": ["#0c110e", "#141d17", "#213026", "#354c3d", "#527360", "#7c9d88", "#b3c9bb", "#dfeae3"] }, "stock-manila": { "role": "tertiary", "displayName": "Manila Stock", "canonical": "light-dark(#ddc78d, #3a3018)", "note": "The category code. Codes exactly one thing: a vault owned by somebody else.", "tonalRamp": ["#221a06", "#33280c", "#4b3c14", "#6a561f", "#8e762f", "#b59a4f", "#ddc78d", "#f2e7c6"] }, "ink": { "role": "primary", "displayName": "Cloakroom Ink", "canonical": "light-dark(#121512, #e6efe8)", "note": "The accent IS the ink. Text, links, primary button fill, the 3px title rule.", "tonalRamp": ["#080a08", "#121512", "#20261f", "#333d31", "#4d5b4a", "#7b8b78", "#b4c0b1", "#e6efe8"] }, "ink-strong": { "role": "primary", "displayName": "Full Strike", "canonical": "light-dark(#000000, #ffffff)", "note": "Hover state for anything filled with ink.", "tonalRamp": ["#000000", "#1a1a1a", "#333333", "#4d4d4d", "#808080", "#b3b3b3", "#e6e6e6", "#ffffff"] }, "ink-soft": { "role": "primary", "displayName": "Ink Wash", "canonical": "light-dark(#bcd0c3, #232e28)", "note": "The wash a ruled (secondary) button takes on hover.", "tonalRamp": ["#0d1210", "#161f1a", "#233028", "#38493f", "#557061", "#82a08f", "#bcd0c3", "#e4eee8"] }, "on-ink": { "role": "primary", "displayName": "Label On Ink", "canonical": "light-dark(#eef3ee, #101512)", "note": "The stamped label printed on a solid ink rectangle. 16.37:1 in light.", "tonalRamp": ["#0a0f0c", "#131a15", "#213028", "#35483d", "#546b5e", "#849a8d", "#bfd0c5", "#eef3ee"] }, "ink-2": { "role": "secondary", "displayName": "Numbering Vermilion", "canonical": "light-dark(#b3301a, #ff7a5c)", "note": "LARGE ONLY: 24px up, or 18.66px bold. 4.29:1 on stock in light. Also the caret colour in every field.", "tonalRamp": ["#2c0c04", "#4a1409", "#6d1e0e", "#8f2614", "#b3301a", "#d8542f", "#ff7a5c", "#ffbba9"] }, "text-muted": { "role": "neutral", "displayName": "Muted Print", "canonical": "light-dark(#3a4741, #a9b8ae)", "note": "Field labels, secondary row text, the status endorsement.", "tonalRamp": ["#0e1210", "#1b221e", "#293430", "#3a4741", "#586a61", "#7e9187", "#a9b8ae", "#dbe4de"] }, "text-faint": { "role": "neutral", "displayName": "Faint Print", "canonical": "light-dark(#434f49, #93a399)", "note": "Tear-line caption, placeholders, the footer overprint. Tightest pair in the build at 4.89:1 on the tint block.", "tonalRamp": ["#101413", "#1d2320", "#2c3531", "#434f49", "#5f6f67", "#7e8f86", "#93a399", "#cfdad4"] }, "line": { "role": "neutral", "displayName": "Hairline Divider", "canonical": "light-dark(#a3b8ab, #2c3830)", "note": "A divider, not a control border. 1.4.11's 3:1 does not apply to it.", "tonalRamp": ["#0e1310", "#18211c", "#2c3830", "#3f5145", "#5c7565", "#809b8a", "#a3b8ab", "#d6e2da"] }, "line-strong": { "role": "neutral", "displayName": "Control Rule", "canonical": "light-dark(#56655c, #7d8d83)", "note": "The control boundary: field underlines, perforation holes, the serial's rule. 4.22:1 light / 5.06:1 dark.", "tonalRamp": ["#111614", "#1e2621", "#2f3a34", "#425049", "#56655c", "#7d8d83", "#a6b4ab", "#d8e0db"] }, "tone-negative-fg": { "role": "secondary", "displayName": "Consequence Red", "canonical": "light-dark(#8b2310, #f0a293)", "note": "Small red. The step overprint, the stamp's strong text, the danger fill. Clears 4.5:1 where the numbering ink cannot.", "tonalRamp": ["#2b0a04", "#48120a", "#6a1b0c", "#8b2310", "#b34a33", "#d4715c", "#f0a293", "#fbd6cd"] }, "tone-negative-bg": { "role": "secondary", "displayName": "Stamp Tint", "canonical": "light-dark(#e3c2b6, #2a1b18)", "note": "The ground of a notice stamp.", "tonalRamp": ["#1a100e", "#2a1b18", "#3f2a25", "#5d403a", "#8a6157", "#b78b7e", "#e3c2b6", "#f5e5df"] }, "tone-positive-fg": { "role": "neutral", "displayName": "Endorsement Green", "canonical": "light-dark(#1d5134, #9fc9ac)", "note": "Status pill only. Held at low chroma; never means 'safe' on its own.", "tonalRamp": ["#08170e", "#0f2c1c", "#173f28", "#1d5134", "#2f7a4f", "#5aa377", "#9fc9ac", "#d6ecdf"] }, "tone-positive-bg": { "role": "neutral", "displayName": "Endorsement Tint", "canonical": "light-dark(#b6d2be, #16241b)", "tonalRamp": ["#0a120d", "#16241b", "#22392a", "#356045", "#4f8a63", "#7db089", "#b6d2be", "#e2f0e7"] }, "tone-warning-fg": { "role": "neutral", "displayName": "Caution Ochre", "canonical": "light-dark(#57430f, #d6c48a)", "tonalRamp": ["#1b1405", "#2e2308", "#42320b", "#57430f", "#7d6320", "#a88a3f", "#d6c48a", "#efe5c4"] }, "tone-warning-bg": { "role": "neutral", "displayName": "Caution Tint", "canonical": "light-dark(#d5cc9c, #24211a)", "tonalRamp": ["#161409", "#24211a", "#38321f", "#544c2d", "#7a7043", "#a79c67", "#d5cc9c", "#efead0"] }, "tone-neutral-fg": { "role": "neutral", "displayName": "Rubber Stamp Ink", "canonical": "light-dark(#33403a, #c4d2c9)", "tonalRamp": ["#0d110f", "#1a211d", "#33403a", "#465a51", "#63806f", "#8ea698", "#c4d2c9", "#e8efeb"] }, "tone-neutral-bg": { "role": "neutral", "displayName": "Rubber Stamp Tint", "canonical": "light-dark(#b9cec2, #232e28)", "tonalRamp": ["#0c110f", "#161f1a", "#232e28", "#374a40", "#547163", "#82a191", "#b9cec2", "#e3ede7"] } }, "typographyMeta": { "display": { "displayName": "Ticket Title", "purpose": "The h1, one per screen, in sentence case over a 3px ink rule. League Gothic, self-hosted WOFF2, OFL 1.1." }, "headline": { "displayName": "Section Label", "purpose": "h2. Uppercase, condensed, 2.5rem above and 0.75rem below." }, "title": { "displayName": "Sub-Section Label", "purpose": "h3, including the device ladder's rung headings. Uppercase, condensed." }, "numbering": { "displayName": "The Numbering", "purpose": "Anything counted: the handoff claim code at clamp(3rem,12vw,5rem), rung counters at 2.6rem. Tabular, in the vermilion, large only." }, "body": { "displayName": "Ticket Body", "purpose": "System UI stack at 14px/1.5, prose capped at 68ch." }, "label": { "displayName": "Caps Furniture", "purpose": "Field names, buttons, nav, status, disclosure summaries, the footer overprint. Deliberately NOT the display face: League Gothic goes spindly at 11.5px." }, "serial": { "displayName": "Machine Serial", "purpose": "Opaque ids, build stamps and handoff codes a person transcribes by hand. Underlined on a rule, never in a grey box; keeps its own case." } }, "shadows": [ { "name": "pop-edge", "value": "0 1px 0 light-dark(rgba(18,21,18,0.22), rgba(0,0,0,0.55))", "purpose": "The framework's popovers and menus. An edge, not a lift." }, { "name": "knob-edge", "value": "0 1px 0 light-dark(rgba(18,21,18,0.30), rgba(0,0,0,0.5))", "purpose": "Switch and slider knobs." }, { "name": "lift-edge", "value": "0 1px 0 light-dark(rgba(18,21,18,0.12), rgba(0,0,0,0.35))", "purpose": "The framework's lightest lift, held at one hairline." }, { "name": "overlay", "value": "light-dark(rgba(18,21,18,0.55), rgba(0,0,0,0.68))", "purpose": "Modal backdrop. The only real darkening: a modal genuinely is a second sheet laid over the first." } ], "motion": [ { "name": "tear", "value": "contra-tear 620ms cubic-bezier(0.16, 1, 0.3, 1) both", "purpose": "The one authored moment in the app: the vault opening, revealed once on unlock via clip-path inset(0 0 100% 0) to inset(0 0 0 0) with opacity. Nothing else animates; there is not one hover transition in contra.css. Removed entirely under prefers-reduced-motion: reduce." } ], "focus": [ { "name": "control-ring", "value": "outline: 2px solid var(--rst-accent); outline-offset: 3px", "purpose": "Buttons and fields in CONTRA's own grammar. The ring is the ink, 12.60:1 on stock in light." }, { "name": "framework-ring", "value": "outline: 2px solid var(--rst-accent); outline-offset: 2px", "purpose": "The framework's :focus-visible inside any rst- component, contributed at zero specificity via :where()." }, { "name": "field-underline-focus", "value": "border-bottom-color: var(--contra-ink-2)", "purpose": "A focused field's 2px rule turns to the numbering vermilion; the caret is vermilion at all times." } ], "breakpoints": [ { "name": "shell-fold", "value": "800px", "purpose": "Below this the top bar's tail and the side rail fold behind one
disclosure with two reveals." }, { "name": "summary-draw", "value": "390px", "purpose": "The width the single disclosure summary is drawn at." }, { "name": "column", "value": "60rem", "purpose": "CONTRA's page column, narrowed from the framework's 64rem default." }, { "name": "print", "value": "@media print", "purpose": "Strips bar, rail, footer and skip link and removes the column cap: black on white, no chrome." } ], "deviceRules": [ { "name": "The Field Box", "selector": "label + input/textarea/select", "rule": "A small letterspaced cap label sits directly on a 2px --rst-line-strong rule with the value on the rule. No boxed text fields anywhere in the app." }, { "name": "The Numbering", "selector": "[data-code], [data-rungs], [rst-steps]", "rule": "Anything counted prints tabular in the second colour. --contra-ink-2 is LARGE ONLY (24px up, or 18.66px bold) because it measures 4.29:1 in light; small red is --rst-tone-negative-fg, which is what [rst-steps] uses." }, { "name": "The Tear-Line", "selector": "[data-tear]", "rule": "A real perforation drawn as radial-gradient holes at the shared --contra-perf 12px pitch. Exactly one per screen, only on a genuine client/server boundary (sign-in and the vault). Never decoration, never twice." }, { "name": "The Stamp", "selector": "[data-notice], [data-kind=\"warning\"]", "rule": "A block on the negative tint with a 3px rule across the top. Never a coloured left border." }, { "name": "The Stock Code", "selector": "[data-vault][data-shared]", "rule": "Manila, coding exactly one thing: a vault that belongs to somebody else. Set on the open vault's root by vault.js; the whole sheet changes stock rather than gaining a badge." }, { "name": "The Counterfoil", "selector": "[data-counterfoil]", "rule": "The tint block above the tear carrying the server's half of the ticket. The only place in the app that changes colour without changing meaning." }, { "name": "The Overprint", "selector": "[data-overprint]", "rule": "Small faint letterspaced caps under a hairline, carrying the build serial. The serial itself keeps its own case." } ], "contrastFloors": { "binding": "WCAG 2.2 AA. Normal text 4.5:1; large text and UI component boundaries 3:1. The complete measured table for both schemes lives in the header of internal/contra/static/theme.css and is recomputed whenever a token changes.", "tightestTextPair": "--rst-text-faint on --rst-surface-2, 4.89:1 in light against a 4.5:1 floor", "exemptions": [ "--rst-line is a hairline divider, not a control border; 1.4.11's 3:1 does not apply. Control borders use --rst-line-strong.", "--rst-header-rule is decorative (the ticket's rule under its title) and carries no floor.", "The shadow tokens and the overlay paint translucent layers over unknown grounds, so no ratio applies." ] }, "sourceOfTruth": { "theme.css": "CONTRA's own token layer: every --rst-* declared once as light-dark(, ) under a single :root, with :root[data-theme=light|dark] as the whole toggle. Replaces rastrillo's vendored `plain` theme; the divergence is recorded in internal/contratest/vendored_test.go's vendoredIsMine map.", "contra.css": "CONTRA's own grammar: the five devices, the display face, browser-surface theming, and the one authored motion moment. Loaded last of the three, so it wins.", "tokens.css": "VENDORED from the rastrillo framework and pinned byte-identical by a test. Do not edit. It supplies the six spacing steps, the type scale (--rst-fs-base 0.875rem / --rst-fs-sm 0.78125rem / --rst-fs-xs 0.71875rem), the 64rem column default that contra.css narrows to 60rem, and the rst--prefixed component rules." } }, "components": [ { "name": "Primary Button", "kind": "button", "refersTo": "button-primary", "description": "A solid rectangle of ink with letterspaced caps: what a ticket office rubber-stamps. The default action on every screen.", "html": "", "css": ".ds-btn-primary { background: var(--rst-accent, #121512); color: var(--rst-on-accent, #eef3ee); border: 2px solid var(--rst-accent, #121512); border-radius: 0; cursor: pointer; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.78125rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; padding: 0.5rem 1.5rem; } .ds-btn-primary:hover { background: var(--rst-accent-strong, #000); border-color: var(--rst-accent-strong, #000); } .ds-btn-primary:focus-visible { outline: 2px solid var(--rst-accent, #121512); outline-offset: 3px; } .ds-btn-primary:disabled { background: var(--rst-surface-2, #b3c9bb); border-color: var(--rst-line-strong, #56655c); color: var(--rst-text-faint, #434f49); cursor: not-allowed; }" }, { "name": "Ruled Button", "kind": "button", "refersTo": "button-ruled", "description": "The same rectangle ruled rather than filled. Cancel, edit, delete and any non-primary action.", "html": "", "css": ".ds-btn-ruled { background: transparent; color: var(--rst-accent, #121512); border: 2px solid var(--rst-accent, #121512); border-radius: 0; cursor: pointer; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.78125rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; padding: 0.5rem 1.5rem; } .ds-btn-ruled:hover { background: var(--rst-accent-soft, #bcd0c3); border-color: var(--rst-accent, #121512); } .ds-btn-ruled:focus-visible { outline: 2px solid var(--rst-accent, #121512); outline-offset: 3px; }" }, { "name": "Field Box", "kind": "input", "refersTo": "field-input", "description": "Device 1. A small letterspaced cap label directly on a 2px rule, with the value sitting on the rule. There are no boxed text fields in this app.", "html": "

", "css": ".ds-field { margin: 0 0 1rem; } .ds-field-label { display: block; color: var(--rst-text-muted, #3a4741); font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.71875rem; font-weight: 600; letter-spacing: 0.12em; line-height: 1.2; margin-bottom: 0; text-transform: uppercase; } .ds-field-input { background: transparent; border: 0; border-bottom: 2px solid var(--rst-line-strong, #56655c); border-radius: 0; color: var(--rst-text, #121512); caret-color: var(--contra-ink-2, #b3301a); font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.875rem; line-height: 1.2; padding: 0 0 0.1rem; inline-size: 100%; max-inline-size: 32rem; } .ds-field-input::placeholder { color: var(--rst-text-faint, #434f49); } .ds-field-input:focus { border-bottom-color: var(--contra-ink-2, #b3301a); outline: 2px solid var(--rst-accent, #121512); outline-offset: 3px; }" }, { "name": "Tear-Line", "kind": "custom", "refersTo": null, "description": "Device 3. A real perforation at a 12px pitch, drawn once per screen and only where a genuine client/server boundary exists. Above it what the server holds; below it what only this browser knows.", "html": "
goes to the server ↑  ·  never leaves this page ↓
", "css": ".ds-tear { align-items: center; display: flex; gap: 0.75rem; margin: 2.5rem 0 1.5rem; } .ds-tear::before, .ds-tear::after { content: ''; flex: 1; block-size: 9px; background-repeat: repeat-x; background-size: var(--contra-perf, 12px) 100%; background-image: radial-gradient(circle at calc(var(--contra-perf, 12px) / 2) 50%, var(--rst-line-strong, #56655c) 1.7px, transparent 1.8px); } .ds-tear span { color: var(--rst-text-faint, #434f49); flex: none; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.71875rem; font-weight: 600; letter-spacing: 0.12em; text-align: center; text-transform: uppercase; }" }, { "name": "Notice Stamp", "kind": "card", "refersTo": "notice-stamp", "description": "Device 4. A block on the negative tint under a 3px rule: how a ticket prints a condition of carriage. Never a coloured left border.", "html": "

There is no reset. If you lose both your passphrase and your Emergency Kit, nobody can open this vault, including us.

", "css": ".ds-stamp { background: var(--rst-tone-negative-bg, #e3c2b6); border-top: 3px solid var(--rst-tone-negative-fg, #8b2310); color: var(--rst-text, #121512); margin: 1.5rem 0; max-width: 68ch; padding: 1rem; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.875rem; line-height: 1.5; } .ds-stamp > :first-child { margin-block-start: 0; } .ds-stamp > :last-child { margin-block-end: 0; } .ds-stamp strong { color: var(--rst-tone-negative-fg, #8b2310); }" }, { "name": "The Numbering", "kind": "custom", "refersTo": "numbering-code", "description": "Device 2. The handoff claim check: eight symbols, ten minutes, printed at the size a cloakroom prints a stub number because a person reads it off one screen and types it into another.", "html": "

Claim codeK7M4-2QXB

", "css": ".ds-numbering-wrap { margin: 0; } .ds-numbering-label { display: block; color: var(--rst-text-muted, #3a4741); font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.71875rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; } .ds-numbering { color: var(--contra-ink-2, #b3301a); display: block; font-family: 'League Gothic', 'Oswald', 'Archivo Narrow', system-ui, sans-serif; font-size: clamp(3rem, 12vw, 5rem); font-variant-numeric: tabular-nums; letter-spacing: 0.14em; line-height: 1; margin: 0.75rem 0; }" }, { "name": "Numbered Rungs", "kind": "custom", "refersTo": null, "description": "The device ladder: ruled rows with machine-printed counters in the second colour, ordered by what works with the least. The last rung always works, and the order carries that promise.", "html": "
  1. Use a passkey

    Fastest, if this browser has one.

  2. Approve from another device

    Ten minutes, one code.

  3. Type your Emergency Kit

    Always works.

", "css": ".ds-rungs { counter-reset: rung; list-style: none; margin: 1.5rem 0; padding: 0; } .ds-rungs > li { border-top: 1px solid var(--rst-line, #a3b8ab); counter-increment: rung; padding: 1rem 0 1rem 3.5rem; position: relative; } .ds-rungs > li::before { color: var(--contra-ink-2, #b3301a); content: counter(rung); font-family: 'League Gothic', 'Oswald', 'Archivo Narrow', system-ui, sans-serif; font-size: 2.6rem; font-variant-numeric: tabular-nums; inset-block-start: 1rem; inset-inline-start: 0; line-height: 0.8; position: absolute; } .ds-rungs h3 { font-family: 'League Gothic', 'Oswald', 'Archivo Narrow', system-ui, sans-serif; font-size: 1.35rem; font-weight: 400; letter-spacing: 0.04em; line-height: 1; margin: 0 0 0.5rem; text-transform: uppercase; } .ds-rungs p { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.875rem; line-height: 1.5; margin: 0; }" }, { "name": "Counterfoil", "kind": "card", "refersTo": "counterfoil", "description": "The server's half of the ticket, printed on the tint block above the tear-line. The only place in the app that changes colour without changing meaning, and the meaning is 'this side is theirs'.", "html": "

Held by this server

18 items · epoch 4 · last written 12 March

", "css": ".ds-counterfoil { background: var(--rst-surface-2, #b3c9bb); padding: 1rem; color: var(--rst-text, #121512); font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.875rem; line-height: 1.5; } .ds-counterfoil > :first-child { margin-block-start: 0; } .ds-counterfoil > :last-child { margin-block-end: 0; } .ds-counterfoil h2 { font-family: 'League Gothic', 'Oswald', 'Archivo Narrow', system-ui, sans-serif; font-size: 1.9rem; font-weight: 400; letter-spacing: 0.04em; line-height: 1; margin-bottom: 0.75rem; text-transform: uppercase; }" }, { "name": "Ruled List", "kind": "card", "refersTo": null, "description": "The ticket's line items. Ruled rows opened by an ink rule and separated by hairlines; never cards, because a card would be a second sheet of paper.", "html": "
  • github.compaul@tito.io
  • BankSealed · version 12
", "css": ".ds-list { border-top: 1px solid var(--rst-accent, #121512); list-style: none; margin: 1rem 0; padding: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.875rem; } .ds-list > li { align-items: baseline; border-bottom: 1px solid var(--rst-line, #a3b8ab); display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.75rem 0; } .ds-list > li > strong { flex: 1 1 12rem; font-weight: 700; } .ds-list > li > span { color: var(--rst-text-muted, #3a4741); flex: 1 1 12rem; }" }, { "name": "Navigation Link", "kind": "nav", "refersTo": "nav-link", "description": "Rail links in the caps voice, undecorated until hover.", "html": "", "css": ".ds-nav a { color: var(--rst-accent, #121512); display: block; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.78125rem; font-weight: 600; letter-spacing: 0.08em; padding: 0.5rem 0; text-decoration: none; text-transform: uppercase; } .ds-nav a:hover { text-decoration: underline; } .ds-nav a:focus-visible { outline: 2px solid var(--rst-accent, #121512); outline-offset: 3px; }" }, { "name": "Status Pill", "kind": "chip", "refersTo": "status-pill", "description": "The framework's status pill, kept because a pill in this world is a rubber stamp. The one round thing on the sheet.", "html": "Sealed", "css": ".ds-pill { align-items: center; background: var(--rst-tone-neutral-bg, #b9cec2); border-radius: 999px; color: var(--rst-tone-neutral-fg, #33403a); display: inline-flex; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.71875rem; font-weight: 600; gap: 0.35rem; line-height: 1.6; padding: 0.1rem 0.6rem; }" }, { "name": "Footer Overprint", "kind": "custom", "refersTo": "footer-overprint", "description": "The mark that ends the sheet: small faint letterspaced caps under a hairline, carrying the build serial. The serial itself keeps its own case, because the page exists to report that value accurately.", "html": "

Build 3a8ba10.

", "css": ".ds-overprint { border-top: 1px solid var(--rst-line, #a3b8ab); color: var(--rst-text-faint, #434f49); font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 0.71875rem; letter-spacing: 0.12em; margin: 0; padding: 0.75rem 0; text-align: start; text-transform: uppercase; } .ds-overprint code { border-bottom: 1px solid var(--rst-line-strong, #56655c); font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, Consolas, monospace; font-size: 0.95em; font-variant-numeric: tabular-nums; letter-spacing: 0.06em; text-transform: none; word-break: break-all; }" } ], "narrative": { "northStar": "Left Luggage", "overview": "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.\n\nThe 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.\n\nDensity 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(, ) under a single :root, and :root[data-theme=light|dark] setting color-scheme is the whole toggle.", "keyCharacteristics": [ "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 (0 radius); 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." ], "rules": [ { "name": "The One Stock, One Ink, One Second Colour Rule", "body": "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.", "section": "colors" }, { "name": "The Colour Means A Category Rule", "body": "Manila codes a vault owned by somebody else, and nothing else, ever. A colour that does not carry a category does not get used.", "section": "colors" }, { "name": "The Large-Only Rule", "body": "--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.", "section": "colors" }, { "name": "The Caps Are For Labels Rule", "body": "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.", "section": "typography" }, { "name": "The Overprint Prints What It Is Rule", "body": "Uppercasing stops at a serial. A build hash the reader will paste and compare renders in its own case, because a page that exists to report a value must not restyle the value into a lie.", "section": "typography" }, { "name": "The Display Face Stays Big Rule", "body": "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.", "section": "typography" }, { "name": "The One Sheet Rule", "body": "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.", "section": "layout" }, { "name": "The No Lift Rule", "body": "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.", "section": "elevation" }, { "name": "The One Authored Moment Rule", "body": "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) — 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.", "section": "components" } ], "dos": [ "Do print the page on one sheet: the stock edge to edge, the column at 60rem, 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 the negative foreground.", "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 h1 in 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 code print its own case and its own tabular figures — a serial is transcribed, not styled.", "Do declare any new token once as light-dark(, ) under the single :root, so both schemes stay complete and the toggle keeps working." ], "donts": [ "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.", "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.", "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." ] } }