| 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 | |