| 1 | /* day — rastrillo's default theme: an everyday blue on white and grey. |
| 2 | Structure lives in tokens.css; this file is colour, type and shape. |
| 3 | rastrillo new writes it once as static/theme.css. |
| 4 | |
| 5 | The look: the ordinary, personable interface. A white page, a plain |
| 6 | grey text scale, one friendly blue that means "this does something", |
| 7 | and the conventional green/amber/red for status. Nothing here is |
| 8 | trying to be memorable — day is the theme an app ships on while it |
| 9 | works out what it wants to look like, and the one that never gets in |
| 10 | the way of the content. Semantic status colours are functional and |
| 11 | never the accent, so a pill can never be mistaken for a link. |
| 12 | |
| 13 | ── One block, both schemes ─────────────────────────────────────────── |
| 14 | Every token is declared once, as light-dark(<light>, <dark>), under a |
| 15 | single :root that sets color-scheme: light dark. The browser picks the |
| 16 | half that matches the resolved scheme, so there is no second copy of |
| 17 | the palette to keep in sync and no way to author dark half-done. The |
| 18 | two rules at the bottom are the whole toggle: setting color-scheme on |
| 19 | :root[data-theme=…] re-resolves every light-dark() in the file at |
| 20 | once, in both directions, which is why an explicit choice beats the OS |
| 21 | without restating a single colour. |
| 22 | |
| 23 | Tokens with one value in both schemes — the font stack, the radii — |
| 24 | are written plain, because light-dark(x, x) says nothing twice. |
| 25 | |
| 26 | light-dark() is a colour function: it may only stand where a <color> |
| 27 | is expected. So a shadow token wraps its colour in it and writes the |
| 28 | geometry once, outside the call — `0 8px 24px light-dark(a, b)`, never |
| 29 | `light-dark(0 8px 24px a, …)`, which no browser would accept as a |
| 30 | box-shadow. |
| 31 | |
| 32 | ── The shape axis ──────────────────────────────────────────────────── |
| 33 | Radius and shadow are the theme's now, not tokens.css's: a theme that |
| 34 | can only change colour cannot change how an app feels. day is soft and |
| 35 | unremarkable — 8px cards, 6px controls, shadows with a wide blur and |
| 36 | little alpha, the depth of paper resting on paper. |
| 37 | |
| 38 | ── WCAG 2.2 AA, measured ───────────────────────────────────────────── |
| 39 | Normal text needs 4.5:1; large text and UI component boundaries need |
| 40 | 3:1. Every pair below was computed from these exact hex values, in |
| 41 | both schemes. If you change a token, recompute its rows. |
| 42 | |
| 43 | LIGHT ratio requirement |
| 44 | --rst-text on --rst-surface 16.91 4.5:1 text |
| 45 | --rst-text on --rst-bg 16.91 4.5:1 text |
| 46 | --rst-text on --rst-surface-2 15.77 4.5:1 text |
| 47 | --rst-text on --rst-accent-soft 15.21 4.5:1 text |
| 48 | --rst-text-muted on --rst-surface 7.56 4.5:1 text |
| 49 | --rst-text-muted on --rst-bg 7.56 4.5:1 text |
| 50 | --rst-text-muted on --rst-surface-2 7.05 4.5:1 text |
| 51 | --rst-text-muted on --rst-accent-soft 6.80 4.5:1 text |
| 52 | --rst-text-faint on --rst-surface 5.29 4.5:1 text |
| 53 | --rst-text-faint on --rst-bg 5.29 4.5:1 text |
| 54 | --rst-text-faint on --rst-surface-2 4.93 4.5:1 text |
| 55 | --rst-text-faint on --rst-accent-soft 4.76 4.5:1 text |
| 56 | --rst-accent on --rst-surface 5.28 4.5:1 text + focus ring |
| 57 | --rst-accent on --rst-bg 5.28 4.5:1 text + focus ring |
| 58 | --rst-accent on --rst-surface-2 4.93 4.5:1 text |
| 59 | --rst-accent on --rst-accent-soft 4.75 4.5:1 text |
| 60 | --rst-on-accent on --rst-accent 5.28 4.5:1 button label |
| 61 | --rst-on-accent on --rst-accent-strong 7.18 4.5:1 button label, hover |
| 62 | --rst-line-strong on --rst-surface 3.63 3:1 control border |
| 63 | --rst-line-strong on --rst-bg 3.63 3:1 control border |
| 64 | --rst-line-strong on --rst-surface-2 3.39 3:1 control border |
| 65 | pill neutral fg on its own tint 9.02 4.5:1 text |
| 66 | pill positive fg on its own tint 6.49 4.5:1 text |
| 67 | pill warning fg on its own tint 6.15 4.5:1 text |
| 68 | pill negative fg on its own tint 5.30 4.5:1 text |
| 69 | --rst-on-accent on negative fg (fill) 6.47 4.5:1 danger button label |
| 70 | |
| 71 | DARK ratio requirement |
| 72 | --rst-text on --rst-surface 13.74 4.5:1 text |
| 73 | --rst-text on --rst-bg 15.32 4.5:1 text |
| 74 | --rst-text on --rst-surface-2 14.63 4.5:1 text |
| 75 | --rst-text on --rst-accent-soft 13.02 4.5:1 text |
| 76 | --rst-text-muted on --rst-surface 7.56 4.5:1 text |
| 77 | --rst-text-muted on --rst-bg 8.43 4.5:1 text |
| 78 | --rst-text-muted on --rst-surface-2 8.05 4.5:1 text |
| 79 | --rst-text-muted on --rst-accent-soft 7.16 4.5:1 text |
| 80 | --rst-text-faint on --rst-surface 6.33 4.5:1 text |
| 81 | --rst-text-faint on --rst-bg 7.06 4.5:1 text |
| 82 | --rst-text-faint on --rst-surface-2 6.74 4.5:1 text |
| 83 | --rst-text-faint on --rst-accent-soft 6.00 4.5:1 text |
| 84 | --rst-accent on --rst-surface 6.95 4.5:1 text + focus ring |
| 85 | --rst-accent on --rst-bg 7.75 4.5:1 text + focus ring |
| 86 | --rst-accent on --rst-surface-2 7.40 4.5:1 text |
| 87 | --rst-accent on --rst-accent-soft 6.59 4.5:1 text |
| 88 | --rst-on-accent on --rst-accent 7.86 4.5:1 button label |
| 89 | --rst-on-accent on --rst-accent-strong 10.29 4.5:1 button label, hover |
| 90 | --rst-line-strong on --rst-surface 4.26 3:1 control border |
| 91 | --rst-line-strong on --rst-bg 4.75 3:1 control border |
| 92 | --rst-line-strong on --rst-surface-2 4.53 3:1 control border |
| 93 | pill neutral fg on its own tint 8.44 4.5:1 text |
| 94 | pill positive fg on its own tint 10.18 4.5:1 text |
| 95 | pill warning fg on its own tint 9.62 4.5:1 text |
| 96 | pill negative fg on its own tint 7.85 4.5:1 text |
| 97 | --rst-on-accent on negative fg (fill) 8.99 4.5:1 danger button label |
| 98 | |
| 99 | The tightest pair in the set is --rst-accent on --rst-accent-soft in |
| 100 | light, at 4.75:1 against a 4.5:1 floor. Put a token on a surface not |
| 101 | listed above and you are guessing; recompute first. |
| 102 | |
| 103 | --rst-line is a hairline divider between rows, not a control border or |
| 104 | a meaningful graphic, so 1.4.11's 3:1 does not apply to it. Control |
| 105 | borders use --rst-line-strong, which does clear 3:1. |
| 106 | |
| 107 | --rst-header-rule is not in the table either, and deliberately so |
| 108 | (design doc §6-v2.2): the page header's rule is decoration, not |
| 109 | structure — the heading's size, weight and spacing carry the |
| 110 | structure — so no contrast floor applies to it and it is not in |
| 111 | ui/contrast_test.go's pair table. Do not argue it up to 3:1. |
| 112 | |
| 113 | Pagination chips are the one deliberate exception: they are bordered |
| 114 | with --rst-line rather than --rst-line-strong, because a page strip |
| 115 | ringed in hard outlines shouts louder than the list it belongs to. |
| 116 | 1.4.11 is satisfied without that border — each chip's meaning is |
| 117 | carried by its text label at 4.5:1 or better, and the current page |
| 118 | adds a filled background, a heavier weight and aria-current on top. |
| 119 | |
| 120 | --rst-shadow-pop, --rst-shadow-knob, --rst-shadow-lift and |
| 121 | --rst-overlay are not in the table: they paint a translucent shadow or |
| 122 | scrim over whatever sits underneath, not text over a fixed |
| 123 | background, so a contrast ratio does not apply. The scrim deepens in |
| 124 | dark, matching --rst-shadow-pop's own jump: black reads lighter |
| 125 | against a dark backdrop, so the overlay needs more alpha to still |
| 126 | read as a dimming layer. |
| 127 | ──────────────────────────────────────────────────────────────────── */ |
| 128 | |
| 129 | :root { |
| 130 | color-scheme: light dark; |
| 131 | |
| 132 | /* Type: the family only. Sizes and spacing are structural and stay in |
| 133 | tokens.css. */ |
| 134 | --rst-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; |
| 135 | |
| 136 | /* Shape: soft, ordinary, unremarkable. */ |
| 137 | --rst-radius: 8px; |
| 138 | --rst-radius-sm: 6px; |
| 139 | --rst-radius-pill: 999px; |
| 140 | |
| 141 | /* Surfaces and lines */ |
| 142 | --rst-bg: light-dark(#ffffff, #111418); |
| 143 | --rst-surface: light-dark(#ffffff, #1a1f26); |
| 144 | --rst-surface-2: light-dark(#f6f7f8, #15191f); |
| 145 | --rst-line: light-dark(#e5e7eb, #2a3038); |
| 146 | --rst-line-strong: light-dark(#7f8792, #79828e); |
| 147 | |
| 148 | /* The page header's rule, derived rather than authored: the accent |
| 149 | folded into the line at 18%, so a theme that changes its accent |
| 150 | gets a matching rule with no second value to keep in step. day's |
| 151 | is barely there — a hairline you would call grey until you put it |
| 152 | beside plain's. Decorative: it carries no contrast floor. */ |
| 153 | --rst-header-rule: color-mix(in oklab, var(--rst-accent) 18%, var(--rst-line)); |
| 154 | |
| 155 | /* Text */ |
| 156 | --rst-text: light-dark(#1a1d21, #e8eaed); |
| 157 | --rst-text-muted: light-dark(#4b5563, #a8b0ba); |
| 158 | --rst-text-faint: light-dark(#656c78, #98a1ac); |
| 159 | |
| 160 | /* Accent */ |
| 161 | --rst-accent: light-dark(#2464e0, #7ba7ff); |
| 162 | --rst-accent-strong: light-dark(#1a4fc0, #a3c0ff); |
| 163 | --rst-accent-soft: light-dark(#eef3fe, #17233a); |
| 164 | --rst-on-accent: light-dark(#ffffff, #0b1220); |
| 165 | |
| 166 | /* Status tones */ |
| 167 | --rst-tone-neutral-fg: light-dark(#374151, #c3c9d1); |
| 168 | --rst-tone-neutral-bg: light-dark(#eef0f2, #262c34); |
| 169 | --rst-tone-positive-fg: light-dark(#166534, #6ee7a8); |
| 170 | --rst-tone-positive-bg: light-dark(#dcfce7, #10281c); |
| 171 | --rst-tone-warning-fg: light-dark(#854d0e, #f0c46a); |
| 172 | --rst-tone-warning-bg: light-dark(#fef3c7, #2c2110); |
| 173 | --rst-tone-negative-fg: light-dark(#b91c1c, #f79aa0); |
| 174 | --rst-tone-negative-bg: light-dark(#fee2e2, #331819); |
| 175 | |
| 176 | /* Depth */ |
| 177 | --rst-shadow-pop: 0 8px 24px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.5)); |
| 178 | --rst-shadow-knob: 0 1px 2px rgba(0, 0, 0, 0.3); |
| 179 | --rst-shadow-lift: 0 1px 2px rgba(0, 0, 0, 0.08); |
| 180 | --rst-overlay: light-dark(rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.6)); |
| 181 | } |
| 182 | |
| 183 | /* The whole toggle. Setting color-scheme re-resolves every light-dark() |
| 184 | above, so an explicit choice beats the OS in both directions without |
| 185 | restating a colour. */ |
| 186 | :root[data-theme="light"] { color-scheme: light; } |
| 187 | :root[data-theme="dark"] { color-scheme: dark; } |
| 188 | |