| 1 | /* contra.css — the grammar of the ticket. |
| 2 | |
| 3 | theme.css is colour, type and shape: the tokens tokens.css consumes. |
| 4 | This file is CONTRA's own, and it exists because the app's templates |
| 5 | write bare HTML — <h1>, <form>, <input>, <ul> — rather than |
| 6 | rastrillo's component classes. Without it the world stops at the |
| 7 | palette and every control on the page is a browser default, which is |
| 8 | the visual equivalent of shipping the crypto and skipping the UI. |
| 9 | |
| 10 | Loaded after theme.css, last of the three, so it wins. |
| 11 | |
| 12 | ── The five devices, and where each may appear ─────────────────────── |
| 13 | 1. THE FIELD BOX. A small letterspaced cap label, a rule, and the |
| 14 | value sitting on the rule. Every input in the app is this; there |
| 15 | are no boxed text fields, because a ticket has ruled fields. |
| 16 | 2. THE NUMBERING. Anything counted prints large, tabular, in the |
| 17 | second colour. Item counts, versions, epochs, the handoff code. |
| 18 | Large only — theme.css measures why. |
| 19 | 3. THE TEAR-LINE. Exactly one per screen, on the real boundary: |
| 20 | above it what the server holds, below it what only this browser |
| 21 | knows. Never decoration, never twice. |
| 22 | 4. THE STAMP. Status pills, and the notices that state a cost. |
| 23 | 5. THE STOCK CODE. Manila for a vault shared with you, the way a |
| 24 | cloakroom prints luggage tickets on different stock from coats. |
| 25 | |
| 26 | ── What this file refuses ──────────────────────────────────────────── |
| 27 | No cards. No rounded corners. No texture, no distress, no torn-paper |
| 28 | shadows, no rotated stamps, no fake staples or perforated GIFs |
| 29 | scattered as decoration. Ticket printing turns twee the moment it |
| 30 | gets grain; what keeps it institutional is that every mark is doing a |
| 31 | job. This is a station cloakroom, not a fairground. |
| 32 | ──────────────────────────────────────────────────────────────────── */ |
| 33 | |
| 34 | /* The display voice: League Gothic, an Alternate Gothic revival and the |
| 35 | condensed face poster and ticket printing has been set in for a |
| 36 | century. Self-hosted, same-origin, 10KB — this app fetches nothing |
| 37 | from anywhere, and a webfont from a CDN would be one more party the |
| 38 | browser trusts on a page that holds vault keys. |
| 39 | |
| 40 | OFL 1.1; the licence ships beside the file at static/fonts/OFL.txt. |
| 41 | swap, because a display face that blocks the first paint of an |
| 42 | unlock screen has its priorities wrong. */ |
| 43 | @font-face { |
| 44 | font-family: "League Gothic"; |
| 45 | font-style: normal; |
| 46 | font-weight: 400; |
| 47 | font-display: swap; |
| 48 | src: url("/static/fonts/leaguegothic-latin.woff2") format("woff2"); |
| 49 | } |
| 50 | |
| 51 | :root { |
| 52 | --contra-display: "League Gothic", "Oswald", "Archivo Narrow", var(--rst-font); |
| 53 | |
| 54 | /* The numbering machine's ink. LARGE ONLY — 24px up, or 18.66px bold. |
| 55 | theme.css carries the measurement and the reason. */ |
| 56 | --contra-ink-2: light-dark(#b3301a, #ff7a5c); |
| 57 | |
| 58 | /* Manila: the category code, and it codes one thing. */ |
| 59 | --contra-stock-2: light-dark(#ddc78d, #3a3018); |
| 60 | |
| 61 | /* The perforation's pitch. One place, so every tear-line in the app |
| 62 | is the same machine. */ |
| 63 | --contra-perf: 12px; |
| 64 | } |
| 65 | |
| 66 | /* ── Browser surfaces ───────────────────────────────────────────────── |
| 67 | The parts we did not draw still carry the design. Left alone these |
| 68 | ship with defaults that belong to no design system, and on a page |
| 69 | whose whole argument is deliberateness that shows. */ |
| 70 | |
| 71 | ::selection { |
| 72 | background: var(--contra-stock-2); |
| 73 | color: var(--rst-text); |
| 74 | } |
| 75 | |
| 76 | :root { |
| 77 | scrollbar-color: var(--rst-line-strong) var(--rst-surface-2); |
| 78 | scrollbar-width: thin; |
| 79 | } |
| 80 | |
| 81 | input, |
| 82 | textarea, |
| 83 | select { |
| 84 | caret-color: var(--contra-ink-2); |
| 85 | } |
| 86 | |
| 87 | /* Links are text that underlines, printed rather than coloured. The |
| 88 | offset and thickness are set because the defaults are a browser's |
| 89 | guess at a font we did not pick. */ |
| 90 | a { |
| 91 | color: var(--rst-accent); |
| 92 | text-decoration-thickness: 1px; |
| 93 | text-underline-offset: 0.18em; |
| 94 | } |
| 95 | |
| 96 | a:hover { |
| 97 | text-decoration-thickness: 2px; |
| 98 | } |
| 99 | |
| 100 | /* ── The sheet ────────────────────────────────────────────────────── */ |
| 101 | |
| 102 | /* The page is one piece of stock. tokens.css already centres the column; |
| 103 | this gives it the ticket's cut edge on the wide layout, where there is |
| 104 | ground either side to cut it out of. */ |
| 105 | /* The page is one piece of stock, printed edge to edge. |
| 106 | |
| 107 | It had cut edges of its own for one round. They were wrong: the console |
| 108 | shell already draws a rule down the side of the rail, and the page's |
| 109 | own border landed about 55px inboard of it, so every wide screen showed |
| 110 | two competing sheet edges with a dead strip between them. The rail's |
| 111 | rule is the structural one and it stays; the sheet is defined by its |
| 112 | ground and its rules, which is how a ticket is actually printed. */ |
| 113 | [rst-page] { |
| 114 | max-width: 60rem; |
| 115 | min-block-size: calc(100vh - 8rem); |
| 116 | } |
| 117 | |
| 118 | /* The brand is the ticket's overprint: the issuing office's name, set |
| 119 | in the display face, letterspaced, small. */ |
| 120 | [rst-shell-brand] { |
| 121 | font-family: var(--contra-display); |
| 122 | font-size: 1.5rem; |
| 123 | letter-spacing: 0.14em; |
| 124 | line-height: 1; |
| 125 | text-decoration: none; |
| 126 | text-transform: uppercase; |
| 127 | } |
| 128 | |
| 129 | /* Headings are set like a ticket's title: condensed, uppercase, tight, |
| 130 | and the h1 carries the heaviest rule on the sheet under it. More |
| 131 | space above a heading than below it, throughout. */ |
| 132 | h1, |
| 133 | h2, |
| 134 | h3 { |
| 135 | font-family: var(--contra-display); |
| 136 | font-weight: 400; |
| 137 | letter-spacing: 0.04em; |
| 138 | line-height: 1; |
| 139 | text-transform: uppercase; |
| 140 | text-wrap: balance; |
| 141 | } |
| 142 | |
| 143 | /* Caps are for labels, not for sentences. A ticket prints its field |
| 144 | names in capitals and its title in the display face, and this app's |
| 145 | h1 is sometimes a whole sentence — "A password manager where the |
| 146 | server knows nothing about you" — which in condensed capitals is a |
| 147 | poster nobody reads and a screen reader may spell out. So the title |
| 148 | keeps its sentence case and gets its character from the face and the |
| 149 | rule under it; h2 and h3 are section labels and stay capitalised. */ |
| 150 | h1 { |
| 151 | border-bottom: 3px solid var(--rst-header-rule); |
| 152 | font-size: clamp(2.4rem, 6vw, 3.6rem); |
| 153 | letter-spacing: 0.01em; |
| 154 | margin: 0 0 var(--rst-sp-5); |
| 155 | padding-bottom: var(--rst-sp-2); |
| 156 | text-transform: none; |
| 157 | } |
| 158 | |
| 159 | h2 { |
| 160 | font-size: 1.9rem; |
| 161 | margin: var(--rst-sp-6) 0 var(--rst-sp-3); |
| 162 | } |
| 163 | |
| 164 | h3 { |
| 165 | font-size: 1.35rem; |
| 166 | margin: var(--rst-sp-5) 0 var(--rst-sp-2); |
| 167 | } |
| 168 | |
| 169 | /* A reading measure for prose, because this product explains itself |
| 170 | constantly and the column is sized for forms and lists. */ |
| 171 | [rst-page] > p, |
| 172 | [rst-page] > section > p, |
| 173 | form > p:not(:has(input, textarea, select, button)) { |
| 174 | max-width: 68ch; |
| 175 | } |
| 176 | |
| 177 | /* ── Device 1: the field box ────────────────────────────────────────── |
| 178 | A label printed in small letterspaced caps, a rule, and the value |
| 179 | sitting on the rule. No boxes: a ticket rules its fields, it does not |
| 180 | draw containers round them. |
| 181 | |
| 182 | The rule is the ink rather than the hairline, which is what makes it |
| 183 | a control boundary rather than a divider — 12.6:1 in light, well past |
| 184 | 1.4.11's 3:1. */ |
| 185 | |
| 186 | /* The caps furniture — labels, buttons, nav, status, the overprint — is |
| 187 | set in the machine's own UI stack rather than in the display face, and |
| 188 | that is a decision rather than an omission. League Gothic is a poster |
| 189 | face: at 11.5px with 0.12em tracking it goes spindly and its condensed |
| 190 | forms stop being legible, which is the opposite of what a field label |
| 191 | is for. A workhorse UI sans is what an Operate surface wants at that |
| 192 | size, so the ticket's character comes from case, tracking, weight and |
| 193 | the rules the caps sit on, not from a second self-hosted face. The |
| 194 | cost, accepted: the small caps render in a different typeface per |
| 195 | operating system while the display line does not. */ |
| 196 | label { |
| 197 | color: var(--rst-text-muted); |
| 198 | display: block; |
| 199 | font-size: var(--rst-fs-xs); |
| 200 | font-weight: 600; |
| 201 | letter-spacing: 0.12em; |
| 202 | line-height: 1.2; |
| 203 | margin-bottom: 0; |
| 204 | text-transform: uppercase; |
| 205 | } |
| 206 | |
| 207 | /* A checkbox's label is a sentence, not a field name, so it opts out of |
| 208 | the caps entirely. */ |
| 209 | label:has(input[type="checkbox"]), |
| 210 | label:has(input[type="radio"]) { |
| 211 | color: var(--rst-text); |
| 212 | display: flex; |
| 213 | font-size: var(--rst-fs-base); |
| 214 | font-weight: 400; |
| 215 | gap: var(--rst-sp-2); |
| 216 | letter-spacing: normal; |
| 217 | line-height: 1.5; |
| 218 | max-width: 68ch; |
| 219 | text-transform: none; |
| 220 | } |
| 221 | |
| 222 | input[type="text"], |
| 223 | input[type="email"], |
| 224 | input[type="password"], |
| 225 | input[type="file"], |
| 226 | input:not([type]), |
| 227 | textarea, |
| 228 | select { |
| 229 | background: transparent; |
| 230 | border: 0; |
| 231 | border-bottom: 2px solid var(--rst-line-strong); |
| 232 | border-radius: 0; |
| 233 | color: var(--rst-text); |
| 234 | font: inherit; |
| 235 | inline-size: 100%; |
| 236 | max-inline-size: 32rem; |
| 237 | /* The value sits ON the rule, close under its label: a printed field |
| 238 | is one mark, and two rounds running the label read as a heading over |
| 239 | an empty line rather than as the name of the rule beneath it. The |
| 240 | distance is the input's own text box, so closing it means shortening |
| 241 | that box rather than removing margin there never was. */ |
| 242 | line-height: 1.2; |
| 243 | padding: 0 0 0.1rem; |
| 244 | } |
| 245 | |
| 246 | textarea { |
| 247 | min-block-size: 6rem; |
| 248 | resize: vertical; |
| 249 | } |
| 250 | |
| 251 | input:focus, |
| 252 | textarea:focus, |
| 253 | select:focus { |
| 254 | border-bottom-color: var(--contra-ink-2); |
| 255 | outline: 2px solid var(--rst-accent); |
| 256 | outline-offset: 3px; |
| 257 | } |
| 258 | |
| 259 | input::placeholder, |
| 260 | textarea::placeholder { |
| 261 | color: var(--rst-text-faint); |
| 262 | } |
| 263 | |
| 264 | input[type="checkbox"], |
| 265 | input[type="radio"] { |
| 266 | accent-color: var(--contra-ink-2); |
| 267 | block-size: 1.1rem; |
| 268 | flex: none; |
| 269 | inline-size: 1.1rem; |
| 270 | margin-block-start: 0.15rem; |
| 271 | } |
| 272 | |
| 273 | /* Form paragraphs are the ticket's field rows. */ |
| 274 | form > p { |
| 275 | margin: 0 0 var(--rst-sp-4); |
| 276 | } |
| 277 | |
| 278 | /* ── Buttons ────────────────────────────────────────────────────────── |
| 279 | The primary is a solid rectangle of ink with letterspaced caps: what |
| 280 | a ticket office rubber-stamps. Everything else is the same rectangle |
| 281 | ruled rather than filled. */ |
| 282 | |
| 283 | button, |
| 284 | [rst-btn] { |
| 285 | background: var(--rst-accent); |
| 286 | border: 2px solid var(--rst-accent); |
| 287 | border-radius: 0; |
| 288 | color: var(--rst-on-accent); |
| 289 | cursor: pointer; |
| 290 | font: inherit; |
| 291 | font-size: var(--rst-fs-sm); |
| 292 | font-weight: 700; |
| 293 | letter-spacing: 0.14em; |
| 294 | padding: var(--rst-sp-2) var(--rst-sp-5); |
| 295 | text-transform: uppercase; |
| 296 | } |
| 297 | |
| 298 | button:hover { |
| 299 | background: var(--rst-accent-strong); |
| 300 | border-color: var(--rst-accent-strong); |
| 301 | } |
| 302 | |
| 303 | button:focus-visible { |
| 304 | outline: 2px solid var(--rst-accent); |
| 305 | outline-offset: 3px; |
| 306 | } |
| 307 | |
| 308 | button[type="button"], |
| 309 | button[data-cancel], |
| 310 | button[data-edit], |
| 311 | button[data-delete] { |
| 312 | background: transparent; |
| 313 | color: var(--rst-accent); |
| 314 | } |
| 315 | |
| 316 | button[type="button"]:hover, |
| 317 | button[data-cancel]:hover, |
| 318 | button[data-edit]:hover, |
| 319 | button[data-delete]:hover { |
| 320 | background: var(--rst-accent-soft); |
| 321 | border-color: var(--rst-accent); |
| 322 | } |
| 323 | |
| 324 | /* The row buttons on an item are marginal notes, not stamps. */ |
| 325 | button[data-edit], |
| 326 | button[data-delete] { |
| 327 | border-width: 1px; |
| 328 | padding: 0.15rem var(--rst-sp-2); |
| 329 | } |
| 330 | |
| 331 | button:disabled { |
| 332 | border-color: var(--rst-line-strong); |
| 333 | background: var(--rst-surface-2); |
| 334 | color: var(--rst-text-faint); |
| 335 | cursor: not-allowed; |
| 336 | } |
| 337 | |
| 338 | /* ── Device 2: the numbering ────────────────────────────────────────── |
| 339 | Anything counted prints large, tabular, in the second colour. The |
| 340 | numbering machine only ever counts up, which is the same promise the |
| 341 | vault epoch and the item version make. */ |
| 342 | |
| 343 | /* The step overprint: signup's sequence marker, set as a ticket prints |
| 344 | one — small, letterspaced, tight under the title rule. |
| 345 | |
| 346 | It is NOT device 2, and it spent one build wrongly claiming to be. The |
| 347 | numbering is the second colour, and the second colour is reserved for |
| 348 | two things: a count, and something that cannot be undone. A routine |
| 349 | "step 1 of 3" is neither, and printing it in the negative ink diluted |
| 350 | the one signal this design has for consequence — the review caught |
| 351 | exactly that, and it was right. |
| 352 | |
| 353 | So the marker is neutral ink, and the public pages simply do not carry |
| 354 | the numbering device, because this product counts nothing on them. |
| 355 | Inventing a figure to give the page a red number would be worse than |
| 356 | the absence. */ |
| 357 | [rst-steps] { |
| 358 | color: var(--rst-text-muted); |
| 359 | font-size: var(--rst-fs-xs); |
| 360 | font-variant-numeric: tabular-nums; |
| 361 | font-weight: 700; |
| 362 | letter-spacing: 0.14em; |
| 363 | margin: calc(-1 * var(--rst-sp-3)) 0 var(--rst-sp-4); |
| 364 | text-transform: uppercase; |
| 365 | } |
| 366 | |
| 367 | /* The handoff code IS a claim check: eight symbols, ten minutes, and the |
| 368 | only thing that gets the Account Key across. It prints at the size a |
| 369 | cloakroom prints a stub number, because a person is reading it off one |
| 370 | screen and typing it into another. */ |
| 371 | [data-code] { |
| 372 | color: var(--contra-ink-2); |
| 373 | display: block; |
| 374 | font-family: var(--contra-display); |
| 375 | font-size: clamp(3rem, 12vw, 5rem); |
| 376 | font-variant-numeric: tabular-nums; |
| 377 | letter-spacing: 0.14em; |
| 378 | line-height: 1; |
| 379 | margin: var(--rst-sp-3) 0; |
| 380 | } |
| 381 | |
| 382 | /* The three-rung ladder, numbered by the machine. Section numbers are |
| 383 | usually decoration; these are not. The rungs are in order of what |
| 384 | works with the least, and the last one always works — which is the |
| 385 | whole promise of the screen, and it is carried by the order. */ |
| 386 | [data-rungs] { |
| 387 | counter-reset: rung; |
| 388 | list-style: none; |
| 389 | margin: var(--rst-sp-5) 0; |
| 390 | padding: 0; |
| 391 | } |
| 392 | |
| 393 | [data-rungs] > li { |
| 394 | border-top: 1px solid var(--rst-line); |
| 395 | counter-increment: rung; |
| 396 | padding: var(--rst-sp-4) 0 var(--rst-sp-4) 3.5rem; |
| 397 | position: relative; |
| 398 | } |
| 399 | |
| 400 | [data-rungs] > li::before { |
| 401 | color: var(--contra-ink-2); |
| 402 | content: counter(rung); |
| 403 | font-family: var(--contra-display); |
| 404 | font-size: 2.6rem; |
| 405 | font-variant-numeric: tabular-nums; |
| 406 | inset-block-start: var(--rst-sp-4); |
| 407 | inset-inline-start: 0; |
| 408 | line-height: 0.8; |
| 409 | position: absolute; |
| 410 | } |
| 411 | |
| 412 | [data-rungs] > li > h3 { |
| 413 | margin-block-start: 0; |
| 414 | } |
| 415 | |
| 416 | /* Opaque identifiers are machine-printed serials: the account id you |
| 417 | hand somebody, the build stamp, a handoff code. Monospace here is |
| 418 | measurement, not costume — these are hex and Crockford Base32, and |
| 419 | the reader has to transcribe them by hand. |
| 420 | |
| 421 | A serial is printed on a ruled line, not in a grey box. The box also |
| 422 | stranded the full stop after "Build 3a8ba10." half a space away from |
| 423 | it, which is the kind of thing a chip does to a sentence. */ |
| 424 | code { |
| 425 | border-bottom: 1px solid var(--rst-line-strong); |
| 426 | font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace; |
| 427 | font-size: 0.95em; |
| 428 | font-variant-numeric: tabular-nums; |
| 429 | letter-spacing: 0.06em; |
| 430 | word-break: break-all; |
| 431 | } |
| 432 | |
| 433 | /* ── Device 3: the tear-line ────────────────────────────────────────── |
| 434 | Exactly one per screen, and only on the real boundary. Above it is |
| 435 | what this server holds — opaque ids, counts, times. Below it is what |
| 436 | only this browser can read. |
| 437 | |
| 438 | Every product in this category has this line. Most of them would find |
| 439 | it embarrassing to draw honestly, which is the reason to draw it. |
| 440 | |
| 441 | A real perforation, not a dashed border: round holes at a fixed pitch |
| 442 | with the stock showing through. The caption sits on the line the way |
| 443 | a printed instruction does. |
| 444 | |
| 445 | NARROW FIRST, and that is not a stylistic preference. The first version |
| 446 | of this was a three-part flex row — dots, caption, dots — with the |
| 447 | caption set flex: none. At 390px the caption is wider than the sheet, |
| 448 | so both dot segments collapsed to zero width and the text clipped |
| 449 | mid-word: on a phone, the app's signature device rendered as neither a |
| 450 | perforation nor a sentence, and sat inside the field group breaking the |
| 451 | label-over-rule pairing beside it. |
| 452 | |
| 453 | So the default is stacked — a full-width perforation with the caption |
| 454 | under it, free to wrap — and the row is the enhancement, taken only |
| 455 | once there is room for it. A tear that only works on a desktop is not a |
| 456 | tear; it is a decoration that happens to line up. */ |
| 457 | |
| 458 | [data-tear] { |
| 459 | display: grid; |
| 460 | gap: var(--rst-sp-2); |
| 461 | justify-items: center; |
| 462 | margin: var(--rst-sp-6) 0 var(--rst-sp-5); |
| 463 | } |
| 464 | |
| 465 | [data-tear]::before { |
| 466 | background-image: radial-gradient( |
| 467 | circle at calc(var(--contra-perf) / 2) 50%, |
| 468 | var(--rst-line-strong) 1.7px, |
| 469 | transparent 1.8px |
| 470 | ); |
| 471 | background-repeat: repeat-x; |
| 472 | background-size: var(--contra-perf) 100%; |
| 473 | block-size: 9px; |
| 474 | content: ""; |
| 475 | inline-size: 100%; |
| 476 | } |
| 477 | |
| 478 | [data-tear] span { |
| 479 | color: var(--rst-text-faint); |
| 480 | font-size: var(--rst-fs-xs); |
| 481 | font-weight: 600; |
| 482 | letter-spacing: 0.12em; |
| 483 | text-align: center; |
| 484 | text-transform: uppercase; |
| 485 | text-wrap: balance; |
| 486 | } |
| 487 | |
| 488 | /* Wide enough for the caption to sit on the line it names. */ |
| 489 | @media (min-width: 48rem) { |
| 490 | [data-tear] { |
| 491 | align-items: center; |
| 492 | display: flex; |
| 493 | gap: var(--rst-sp-3); |
| 494 | } |
| 495 | |
| 496 | [data-tear]::before, |
| 497 | [data-tear]::after { |
| 498 | background-image: radial-gradient( |
| 499 | circle at calc(var(--contra-perf) / 2) 50%, |
| 500 | var(--rst-line-strong) 1.7px, |
| 501 | transparent 1.8px |
| 502 | ); |
| 503 | background-repeat: repeat-x; |
| 504 | background-size: var(--contra-perf) 100%; |
| 505 | block-size: 9px; |
| 506 | content: ""; |
| 507 | flex: 1 1 0; |
| 508 | inline-size: auto; |
| 509 | min-inline-size: 0; |
| 510 | } |
| 511 | |
| 512 | [data-tear] span { |
| 513 | flex: 0 1 auto; |
| 514 | min-inline-size: 0; |
| 515 | } |
| 516 | |
| 517 | /* The separator exists only where the caption is one line. Stacked, the |
| 518 | two halves are already two lines and a dot between them would be a |
| 519 | mark with nothing to separate — which is how it came to strand at the |
| 520 | end of the first line when the caption was a single string. */ |
| 521 | [data-tear] span + span::before { |
| 522 | content: "·"; |
| 523 | margin-inline: 0.7em; |
| 524 | } |
| 525 | } |
| 526 | |
| 527 | /* ── Device 4: the stamp ────────────────────────────────────────────── |
| 528 | A notice that states a cost. Not a coloured left border — a stamped |
| 529 | block with a rule across the top, which is how a ticket prints a |
| 530 | condition of carriage. */ |
| 531 | |
| 532 | [data-kind="warning"], |
| 533 | [data-notice] { |
| 534 | background: var(--rst-tone-negative-bg); |
| 535 | border-top: 3px solid var(--rst-tone-negative-fg); |
| 536 | color: var(--rst-text); |
| 537 | margin: var(--rst-sp-5) 0; |
| 538 | max-width: 68ch; |
| 539 | padding: var(--rst-sp-4); |
| 540 | } |
| 541 | |
| 542 | [data-kind="warning"] strong, |
| 543 | [data-notice] strong { |
| 544 | color: var(--rst-tone-negative-fg); |
| 545 | } |
| 546 | |
| 547 | [data-notice] > :first-child, |
| 548 | [data-kind="warning"] > :first-child { |
| 549 | margin-block-start: 0; |
| 550 | } |
| 551 | |
| 552 | [data-notice] > :last-child, |
| 553 | [data-kind="warning"] > :last-child { |
| 554 | margin-block-end: 0; |
| 555 | } |
| 556 | |
| 557 | /* ── Device 5: the stock code ───────────────────────────────────────── |
| 558 | Manila, and it means one thing: this vault belongs to somebody else |
| 559 | and you are on the list. Colour as a category, never as decoration. |
| 560 | |
| 561 | It is set on the open vault's root by vault.js, so a vault that belongs |
| 562 | to somebody else prints its whole contents on different stock — which |
| 563 | is the actual cloakroom rule, not a badge saying "shared". A person can |
| 564 | tell at a glance whose sheet they are writing on. */ |
| 565 | |
| 566 | [data-vault][data-shared] { |
| 567 | background: var(--contra-stock-2); |
| 568 | padding: var(--rst-sp-4); |
| 569 | } |
| 570 | |
| 571 | /* ── Lists: the ticket's line items ─────────────────────────────────── |
| 572 | Ruled rows, not cards. A card would be a second sheet of paper. */ |
| 573 | |
| 574 | [data-items], |
| 575 | [data-members] { |
| 576 | border-top: 1px solid var(--rst-accent); |
| 577 | list-style: none; |
| 578 | margin: var(--rst-sp-4) 0; |
| 579 | max-width: 100%; |
| 580 | padding: 0; |
| 581 | } |
| 582 | |
| 583 | [data-items] > li, |
| 584 | [data-members] > li { |
| 585 | align-items: baseline; |
| 586 | border-bottom: 1px solid var(--rst-line); |
| 587 | display: flex; |
| 588 | flex-wrap: wrap; |
| 589 | gap: var(--rst-sp-2); |
| 590 | padding: var(--rst-sp-3) 0; |
| 591 | } |
| 592 | |
| 593 | [data-items] > li > strong { |
| 594 | flex: 1 1 12rem; |
| 595 | font-weight: 700; |
| 596 | } |
| 597 | |
| 598 | [data-items] > li > span { |
| 599 | color: var(--rst-text-muted); |
| 600 | flex: 1 1 12rem; |
| 601 | } |
| 602 | |
| 603 | /* ── Status ─────────────────────────────────────────────────────────── |
| 604 | A live region that reports what just happened. Set like a printed |
| 605 | endorsement rather than a toast: it belongs to the sheet. */ |
| 606 | |
| 607 | [data-status], |
| 608 | [data-share-status] { |
| 609 | color: var(--rst-text-muted); |
| 610 | font-size: var(--rst-fs-sm); |
| 611 | letter-spacing: 0.06em; |
| 612 | min-block-size: 1.4em; |
| 613 | text-transform: uppercase; |
| 614 | } |
| 615 | |
| 616 | /* ── Navigation ───────────────────────────────────────────────────── */ |
| 617 | |
| 618 | [rst-shell-nav] a { |
| 619 | display: block; |
| 620 | font-size: var(--rst-fs-sm); |
| 621 | font-weight: 600; |
| 622 | letter-spacing: 0.08em; |
| 623 | padding: var(--rst-sp-2) 0; |
| 624 | text-decoration: none; |
| 625 | text-transform: uppercase; |
| 626 | } |
| 627 | |
| 628 | [rst-shell-nav] a:hover { |
| 629 | text-decoration: underline; |
| 630 | } |
| 631 | |
| 632 | /* The disclosures are controls, so they speak the same vocabulary as |
| 633 | every other control rather than arriving in title case from the |
| 634 | framework's defaults. */ |
| 635 | [rst-shell-menu] > summary, |
| 636 | [rst-shell-account] > summary { |
| 637 | border-radius: 0; |
| 638 | font-size: var(--rst-fs-xs); |
| 639 | font-weight: 700; |
| 640 | letter-spacing: 0.12em; |
| 641 | text-transform: uppercase; |
| 642 | } |
| 643 | |
| 644 | /* The overprint that ends the ticket: small, faint, letterspaced, under a |
| 645 | hairline. A sheet with nothing at the foot is a sheet that just stops. */ |
| 646 | [data-overprint] { |
| 647 | border-top: 1px solid var(--rst-line); |
| 648 | color: var(--rst-text-faint); |
| 649 | font-size: var(--rst-fs-xs); |
| 650 | letter-spacing: 0.12em; |
| 651 | margin: 0; |
| 652 | padding: var(--rst-sp-3) 0; |
| 653 | text-align: start; |
| 654 | text-transform: uppercase; |
| 655 | } |
| 656 | |
| 657 | /* The caps stop at the serial. A build hash is an identifier somebody |
| 658 | copies, pastes and compares against `carlos channels`; printing it |
| 659 | uppercase when it is lowercase everywhere else makes the page lie |
| 660 | about a value in the one place the page exists to report it. */ |
| 661 | [data-overprint] code { |
| 662 | text-transform: none; |
| 663 | } |
| 664 | |
| 665 | [rst-shell-foot] { |
| 666 | margin: 0 auto; |
| 667 | max-width: 60rem; |
| 668 | padding: 0 var(--rst-sp-4) var(--rst-sp-5); |
| 669 | width: 100%; |
| 670 | } |
| 671 | |
| 672 | /* ── The counterfoil ────────────────────────────────────────────────── |
| 673 | The server's half of the ticket: everything above the tear, printed on |
| 674 | the tint block the way a counterfoil is. It is the only place in the |
| 675 | app that changes colour without changing meaning, and the meaning it |
| 676 | carries is "this side is theirs". */ |
| 677 | |
| 678 | [data-counterfoil] { |
| 679 | background: var(--rst-surface-2); |
| 680 | padding: var(--rst-sp-4); |
| 681 | } |
| 682 | |
| 683 | [data-counterfoil] > :first-child { |
| 684 | margin-block-start: 0; |
| 685 | } |
| 686 | |
| 687 | [data-counterfoil] > :last-child { |
| 688 | margin-block-end: 0; |
| 689 | } |
| 690 | |
| 691 | /* ── The one authored moment ────────────────────────────────────────── |
| 692 | The world's native motion is the tear. So the single piece of motion |
| 693 | in this app is the vault opening: the sealed half below the line is |
| 694 | revealed once, on unlock, easing out the way paper separates — fast |
| 695 | at the start, settling at the end. |
| 696 | |
| 697 | Nothing else animates. Scattered hover transitions would make this |
| 698 | read as an effect rather than as the moment it is. |
| 699 | |
| 700 | The default is visible: an engine that ignores the animation, or a |
| 701 | reader who asked for less motion, gets the content immediately. */ |
| 702 | |
| 703 | @keyframes contra-tear { |
| 704 | from { |
| 705 | clip-path: inset(0 0 100% 0); |
| 706 | opacity: 0; |
| 707 | } |
| 708 | to { |
| 709 | clip-path: inset(0 0 0 0); |
| 710 | opacity: 1; |
| 711 | } |
| 712 | } |
| 713 | |
| 714 | [data-vault]:not([hidden]) { |
| 715 | animation: contra-tear 620ms cubic-bezier(0.16, 1, 0.3, 1) both; |
| 716 | } |
| 717 | |
| 718 | @media (prefers-reduced-motion: reduce) { |
| 719 | [data-vault]:not([hidden]) { |
| 720 | animation: none; |
| 721 | } |
| 722 | } |
| 723 | |
| 724 | /* ── Print ──────────────────────────────────────────────────────────── |
| 725 | The kit is generated as a PDF in the browser and does not come |
| 726 | through here, but a person who prints a screen should still get |
| 727 | something a cloakroom would recognise: black on white, no chrome. */ |
| 728 | |
| 729 | @media print { |
| 730 | [rst-shell-bar], |
| 731 | [rst-shell-rail], |
| 732 | [rst-shell-foot], |
| 733 | [rst-skip] { |
| 734 | display: none; |
| 735 | } |
| 736 | |
| 737 | [rst-page] { |
| 738 | border: 0; |
| 739 | max-width: none; |
| 740 | } |
| 741 | } |
| 742 | |