carlos / contra Public

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

Plain git — no account needed to clone.

Download

Download this file

1/* left-luggage — CONTRA's theme: ticket stock, black ink, one vermilion
2 for the numbering. Structure lives in tokens.css; this file is colour,
3 type and shape.
4
5 Replaces rastrillo's `plain`, which is the theme you ship on while you
6 work out what the app looks like. It is recorded as a deliberate
7 divergence in contratest/vendored_test.go (vendoredIsMine), so
8 `rastrillo doctor` leaves it alone and the pin test stops treating it
9 as drift.
10
11 ── The world ─────────────────────────────────────────────────────────
12 A station cloakroom. You hand over your bag; they hand you a numbered
13 stub. They never learn what is in it, they do not want your name, and
14 they will not release it without the stub — not because they are
15 unhelpful, but because a cloakroom that gives your bag to anyone with
16 a good story is not a cloakroom. That is this product's argument, and
17 it is older than the category it is arguing with.
18
19 So the material is left-luggage ticketing: coloured stock, black ink,
20 ruled field boxes with small letterspaced caps printed above the rule,
21 a numbering machine that only ever counts up, and one perforation
22 where the ticket tears.
23
24 Six of the product's mechanics are already ticket mechanics rather
25 than metaphors for them:
26
27 the counterfoil, two halves and one number the wrap and the key
28 no article released without the ticket no reset, no override
29 a numbering machine never repeats epoch, item version
30 coloured stock IS the category code owner vs member vault
31 a claim check that expires the 10-minute handoff
32 the perforation client / server
33
34 ── The palette, and why it is this one ───────────────────────────────
35 ONE STOCK, ONE INK, ONE SECOND COLOUR. Ticket printing has no third.
36
37 The ground is eau-de-nil ticket stock, not white and emphatically not
38 cream: cream is where a machine lands when it wants "paper" without
39 deciding anything, and real cloakroom stock is a dyed pale green, blue
40 or manila. The page IS the ticket, so --rst-bg and --rst-surface are
41 the same value. There are no cards here; a card would be a second
42 sheet of paper on a counter that only ever holds one.
43
44 --rst-surface-2 is the printed tint block, the darker band a ticket
45 uses to group a field — not a raised card, a flatter one.
46
47 The ACCENT IS THE INK. Links and primary buttons are black on stock,
48 because in this world the only thing printed in colour is the number.
49 Which means the second colour lands exactly where the product's
50 consequences are: the numbering (--contra-ink-2, in contra.css) and
51 the negative tone. A vermilion this product prints is either counting
52 something or telling you something cannot be undone.
53
54 Manila (--contra-stock-2, contra.css) is the category code, and it
55 codes one thing only: a vault shared with you, printed on different
56 stock the way a cloakroom prints luggage tickets on different stock
57 from coat tickets. Colour that means something categorically, or it
58 does not get used.
59
60 ── Shape ─────────────────────────────────────────────────────────────
61 Square. Tickets are cut, not moulded. --rst-radius is 0 and the pill
62 radius stays only because tokens.css paints status pills with it and a
63 theme cannot opt out of a property it must declare — the pills are the
64 one round thing on the page, and they are round because a pill in this
65 world is a rubber stamp.
66
67 ── Depth ─────────────────────────────────────────────────────────────
68 Ink on paper has no depth, so the shadows are the least a theme can
69 declare and still be declaring something: one hairline layer. Nothing
70 in this app floats. The overlay is the only real darkening, because a
71 modal genuinely is a second sheet laid over the first.
72
73 ── One block, both schemes ───────────────────────────────────────────
74 Every token is declared once, as light-dark(<light>, <dark>), under a
75 single :root that sets color-scheme: light dark. The two rules at the
76 bottom are the whole toggle: setting color-scheme on
77 :root[data-theme=…] re-resolves every light-dark() in the file at
78 once, in both directions. Tokens with one value in both schemes — the
79 font stack, the radii — are written plain.
80
81 light-dark() is a colour function: it may only stand where a <color>
82 is expected. So a shadow token wraps its colour in it and writes the
83 geometry once, outside the call.
84
85 LIGHT is the primary scheme, and that is a decision about a scene
86 rather than a taste: the moment this product is most needed is a
87 person at a desk in daylight with a printer running, holding two
88 sheets of paper they were told can never be reissued. The only
89 artefact CONTRA designs from scratch is printed, and a dark-first
90 console would spend the whole product fighting its own kit. DARK is
91 the same counter at night, and is complete, not an inversion.
92
93 ── WCAG 2.2 AA, measured ─────────────────────────────────────────────
94 Normal text needs 4.5:1; large text and UI component boundaries need
95 3:1. Every pair below was computed from these exact hex values, in
96 both schemes. If you change a token, recompute its rows.
97
98 LIGHT ratio requirement
99 --rst-text on --rst-surface 12.60 4.5:1 text
100 --rst-text on --rst-bg 12.60 4.5:1 text
101 --rst-text on --rst-surface-2 10.50 4.5:1 text
102 --rst-text on --rst-accent-soft 11.34 4.5:1 text
103 --rst-text-muted on --rst-surface 6.67 4.5:1 text
104 --rst-text-muted on --rst-bg 6.67 4.5:1 text
105 --rst-text-muted on --rst-surface-2 5.56 4.5:1 text
106 --rst-text-muted on --rst-accent-soft 6.00 4.5:1 text
107 --rst-text-faint on --rst-surface 5.86 4.5:1 text
108 --rst-text-faint on --rst-bg 5.86 4.5:1 text
109 --rst-text-faint on --rst-surface-2 4.89 4.5:1 text
110 --rst-text-faint on --rst-accent-soft 5.28 4.5:1 text
111 --rst-accent on --rst-surface 12.60 4.5:1 text + focus ring
112 --rst-accent on --rst-bg 12.60 4.5:1 text + focus ring
113 --rst-accent on --rst-surface-2 10.50 4.5:1 text
114 --rst-accent on --rst-accent-soft 11.34 4.5:1 text
115 --rst-on-accent on --rst-accent 16.37 4.5:1 button label
116 --rst-on-accent on --rst-accent-strong 18.69 4.5:1 button label, hover
117 --rst-line-strong on --rst-surface 4.22 3:1 control border
118 --rst-line-strong on --rst-bg 4.22 3:1 control border
119 --rst-line-strong on --rst-surface-2 3.51 3:1 control border
120 --contra-ink-2 on --rst-surface 4.29 3:1 numbering, large
121 --contra-ink-2 on --rst-bg 4.29 3:1 numbering, large
122 --contra-ink-2 on --rst-surface-2 3.57 3:1 numbering, large
123 --contra-ink-2 on --contra-stock-2 3.76 3:1 numbering, large
124 --rst-text on --contra-stock-2 11.06 4.5:1 text on manila
125 --rst-text on negative tint 11.09 4.5:1 stamp body
126 --rst-text-muted on negative tint 5.87 4.5:1 stamp body
127 pill neutral fg on its own tint 6.55 4.5:1 text
128 pill positive fg on its own tint 5.68 4.5:1 text
129 pill warning fg on its own tint 5.84 4.5:1 text
130 pill negative fg on its own tint 5.40 4.5:1 text
131 --rst-on-accent on negative fg (fill) 7.97 4.5:1 danger button label
132
133 DARK ratio requirement
134 --rst-text on --rst-surface 15.06 4.5:1 text
135 --rst-text on --rst-bg 15.06 4.5:1 text
136 --rst-text on --rst-surface-2 13.23 4.5:1 text
137 --rst-text on --rst-accent-soft 11.97 4.5:1 text
138 --rst-text-muted on --rst-surface 8.55 4.5:1 text
139 --rst-text-muted on --rst-bg 8.55 4.5:1 text
140 --rst-text-muted on --rst-surface-2 7.51 4.5:1 text
141 --rst-text-muted on --rst-accent-soft 6.80 4.5:1 text
142 --rst-text-faint on --rst-surface 6.69 4.5:1 text
143 --rst-text-faint on --rst-bg 6.69 4.5:1 text
144 --rst-text-faint on --rst-surface-2 5.87 4.5:1 text
145 --rst-text-faint on --rst-accent-soft 5.32 4.5:1 text
146 --rst-accent on --rst-surface 15.06 4.5:1 text + focus ring
147 --rst-accent on --rst-bg 15.06 4.5:1 text + focus ring
148 --rst-accent on --rst-surface-2 13.23 4.5:1 text
149 --rst-accent on --rst-accent-soft 11.97 4.5:1 text
150 --rst-on-accent on --rst-accent 15.71 4.5:1 button label
151 --rst-on-accent on --rst-accent-strong 18.45 4.5:1 button label, hover
152 --rst-line-strong on --rst-surface 5.06 3:1 control border
153 --rst-line-strong on --rst-bg 5.06 3:1 control border
154 --rst-line-strong on --rst-surface-2 4.45 3:1 control border
155 --contra-ink-2 on --rst-surface 6.90 3:1 numbering, large
156 --contra-ink-2 on --rst-bg 6.90 3:1 numbering, large
157 --contra-ink-2 on --rst-surface-2 6.06 3:1 numbering, large
158 --contra-ink-2 on --contra-stock-2 5.07 3:1 numbering, large
159 --rst-text on --contra-stock-2 11.06 4.5:1 text on manila
160 --rst-text on negative tint 14.09 4.5:1 stamp body
161 --rst-text-muted on negative tint 8.01 4.5:1 stamp body
162 pill neutral fg on its own tint 8.98 4.5:1 text
163 pill positive fg on its own tint 8.77 4.5:1 text
164 pill warning fg on its own tint 9.27 4.5:1 text
165 pill negative fg on its own tint 8.12 4.5:1 text
166 --rst-on-accent on negative fg (fill) 9.05 4.5:1 danger button label
167
168 The tightest text pair is --rst-text-faint on the tint block, 4.89:1
169 in light against a 4.5:1 floor.
170
171 --contra-ink-2 IS THE ONE TOKEN WITH A SIZE RULE, and it is here
172 rather than in contra.css so that it sits with its measurements. At
173 4.29:1 in light it is a LARGE-TEXT colour: 24px and up, or 18.66px
174 bold, which is what "the numbering machine prints big" means in CSS.
175 Small red text is --rst-tone-negative-fg, which clears 4.5:1 on its
176 own tint. Do not reach for the numbering ink at body size — darkening
177 it until it passes at 14px turns a vermilion into a brick and costs
178 the theme its second colour.
179
180 --rst-line is a hairline divider, not a control border, so 1.4.11's
181 3:1 does not apply to it. Control borders use --rst-line-strong.
182
183 --rst-header-rule is not in the table and deliberately so: the page
184 header's rule is decoration, not structure. Here it is the ink, which
185 makes it the heaviest rule on the page — that is the ticket's rule
186 under its title, and it carries no contrast floor.
187
188 --rst-shadow-pop, --rst-shadow-knob, --rst-shadow-lift and
189 --rst-overlay are not in the table: they paint a translucent layer
190 over whatever sits underneath, not text over a fixed background, so a
191 contrast ratio does not apply.
192 ──────────────────────────────────────────────────────────────────── */
193
194:root {
195 color-scheme: light dark;
196
197 /* Type: the machine's own UI stack carries body, labels and controls.
198 The display voice — the condensed gothic a ticket is set in — is a
199 self-hosted face declared in contra.css, which is also where the
200 rule about where it may appear lives. */
201 --rst-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
202
203 /* Shape: cut, not moulded. The pill radius survives because tokens.css
204 paints status pills with it; in this world a pill is a rubber stamp,
205 and a stamp is the one round thing on a ticket. */
206 --rst-radius: 0;
207 --rst-radius-sm: 0;
208 --rst-radius-pill: 999px;
209
210 /* Surfaces and lines. bg and surface are the same value on purpose:
211 the page is one sheet of stock, and a card would be a second sheet
212 on a counter that only ever holds one. */
213 --rst-bg: light-dark(#c8dacf, #131a16);
214 --rst-surface: light-dark(#c8dacf, #131a16);
215 --rst-surface-2: light-dark(#b3c9bb, #1d2621);
216 --rst-line: light-dark(#a3b8ab, #2c3830);
217 --rst-line-strong: light-dark(#56655c, #7d8d83);
218
219 /* The page header's rule, printed in the ink rather than the hairline:
220 on a ticket the rule under the title is the heaviest mark on the
221 sheet. Decorative, so no contrast floor applies. */
222 --rst-header-rule: var(--rst-accent);
223
224 /* Text */
225 --rst-text: light-dark(#121512, #e6efe8);
226 --rst-text-muted: light-dark(#3a4741, #a9b8ae);
227 --rst-text-faint: light-dark(#434f49, #93a399);
228
229 /* Accent: the ink. A link is black text that underlines, a primary
230 button is a black rectangle. The colour on this page is reserved
231 for the number and for what cannot be undone. */
232 --rst-accent: light-dark(#121512, #e6efe8);
233 --rst-accent-strong: light-dark(#000000, #ffffff);
234 --rst-accent-soft: light-dark(#bcd0c3, #232e28);
235 --rst-on-accent: light-dark(#eef3ee, #101512);
236
237 /* Status tones: the rubber stamps. Held at low chroma so the page
238 stays stock-and-ink at a glance, except negative, which is the
239 product's second colour doing its other job. */
240 --rst-tone-neutral-fg: light-dark(#33403a, #c4d2c9);
241 --rst-tone-neutral-bg: light-dark(#b9cec2, #232e28);
242 --rst-tone-positive-fg: light-dark(#1d5134, #9fc9ac);
243 --rst-tone-positive-bg: light-dark(#b6d2be, #16241b);
244 --rst-tone-warning-fg: light-dark(#57430f, #d6c48a);
245 --rst-tone-warning-bg: light-dark(#d5cc9c, #24211a);
246 --rst-tone-negative-fg: light-dark(#8b2310, #f0a293);
247 --rst-tone-negative-bg: light-dark(#e3c2b6, #2a1b18);
248
249 /* Depth: ink on paper has none. One hairline layer, because tokens.css
250 paints with these unconditionally and a theme cannot opt out of a
251 property it must declare. You get an edge, not a lift. */
252 --rst-shadow-pop: 0 1px 0 light-dark(rgba(18, 21, 18, 0.22), rgba(0, 0, 0, 0.55));
253 --rst-shadow-knob: 0 1px 0 light-dark(rgba(18, 21, 18, 0.30), rgba(0, 0, 0, 0.5));
254 --rst-shadow-lift: 0 1px 0 light-dark(rgba(18, 21, 18, 0.12), rgba(0, 0, 0, 0.35));
255 --rst-overlay: light-dark(rgba(18, 21, 18, 0.55), rgba(0, 0, 0, 0.68));
256}
257
258/* The whole toggle. Setting color-scheme re-resolves every light-dark()
259 above, so an explicit choice beats the OS in both directions without
260 restating a colour. */
261:root[data-theme="light"] { color-scheme: light; }
262:root[data-theme="dark"] { color-scheme: dark; }
263