@humanforest/tokens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +67 -0
  2. package/dataviz/dataviz.css +149 -0
  3. package/dataviz/tokens.json +1591 -0
  4. package/dataviz/unovis.css +151 -0
  5. package/deck/deck.css +515 -0
  6. package/deck/deck.json +358 -0
  7. package/geo/london-boroughs.json +1 -0
  8. package/geo/london-dots-coarse.json +1 -0
  9. package/geo/london-dots-fine.json +1 -0
  10. package/geo/london-dots-medium.json +1 -0
  11. package/geo/london-dots-ultra.json +1 -0
  12. package/geo/london-wards.json +1 -0
  13. package/glyphs/glyphs.json +1 -0
  14. package/logos/f-rot.svg +4 -0
  15. package/logos/f.svg +4 -0
  16. package/logos/favicon-c2w.svg +9 -0
  17. package/logos/favicon-console.svg +9 -0
  18. package/logos/favicon-ds.svg +9 -0
  19. package/logos/favicon-fleet.svg +9 -0
  20. package/logos/favicon-flex.svg +9 -0
  21. package/logos/favicon-plus.svg +9 -0
  22. package/logos/favicon-radar.svg +9 -0
  23. package/logos/favicon-tower.svg +9 -0
  24. package/logos/lockup-horizontal.svg +17 -0
  25. package/logos/lockup-vertical.svg +17 -0
  26. package/logos/mark-mono.svg +4 -0
  27. package/logos/mark.svg +7 -0
  28. package/logos/wordmark.svg +9 -0
  29. package/motion/motion.css +104 -0
  30. package/motion/motion.json +85 -0
  31. package/motion/motion.theme.css +16 -0
  32. package/package.json +50 -0
  33. package/scales/ramps.css +182 -0
  34. package/scales/ramps.json +605 -0
  35. package/scales/theme.css +256 -0
  36. package/src/colourEngine.ts +604 -0
  37. package/src/glyphs.ts +127 -0
  38. package/src/logo.ts +50 -0
  39. package/src/subbrands.ts +44 -0
  40. package/src/sublogo.ts +80 -0
  41. package/src/sublogoSvg.ts +134 -0
  42. package/type/type.css +232 -0
  43. package/type/type.deck.css +74 -0
  44. package/type/type.json +548 -0
  45. package/type/type.theme.css +31 -0
package/deck/deck.css ADDED
@@ -0,0 +1,515 @@
1
+ /* Forest deck surface — generated by scripts/deck.ts. Do not edit.
2
+
3
+ The presentation context's token layer: colour, space and shape for a 1920x1080 slide canvas. The
4
+ deck TYPE ramp is a separate artefact (type.deck.css, from scripts/type.ts) and the motion ladder is
5
+ shared with every other surface (motion.css).
6
+
7
+ A deck is a real Forest surface — the bold voice at projector scale — but NOT a UTheme context:
8
+ Nuxt UI components do not render on a slide, so the context tag/manifest/register machinery has
9
+ nothing to govern there. These roles are how the voice is actually delivered.
10
+
11
+ ★ FRAMEWORK-AGNOSTIC. Not one Tailwind at-rule. Slidev compiles through UnoCSS, which has no parser
12
+ for @theme / @utility / @custom-variant and passes them through silently — one at-rule here would
13
+ orphan the file with no warning. Plain custom properties only.
14
+
15
+ Consumers reference --deck-* and never a raw ramp step, so a slide cannot express an off-brand
16
+ colour, an off-ladder corner or a hand-picked px. */
17
+ :root {
18
+ /* ── Spacing ─────────────────────────────────────────────────────────────────────────────
19
+ --deck-grid is the layout unit (margins, gutters, the safe area). The numbered steps are the
20
+ CONTENT rhythm, every one a multiple of 4px like the web scale. */
21
+ --deck-grid: 24px;
22
+ --deck-space-1: calc(var(--deck-grid) / 3); /* 8px */
23
+ --deck-space-2: calc(var(--deck-grid) * 2 / 3); /* 16px */
24
+ --deck-space-3: var(--deck-grid); /* 24px */
25
+ --deck-space-4: calc(var(--deck-grid) * 4 / 3); /* 32px */
26
+ --deck-space-5: calc(var(--deck-grid) * 5 / 3); /* 40px */
27
+ --deck-space-6: calc(var(--deck-grid) * 2.5); /* 60px */
28
+ --deck-space-7: calc(var(--deck-grid) * 10 / 3); /* 80px */
29
+
30
+ /* ── Insets — text for its MEASURE, media for its FRAME, and two more ────────────────────
31
+ Text needs a line length and a margin; a photograph has a hard edge and reads as inset too
32
+ far at the same value a paragraph reads correctly at. Chrome (the running mark, page number,
33
+ section pill) is a third, uniform value — furniture, not content. Display is a fourth: the
34
+ beat layouts set one line at 96–128px, and a margin is read relative to the type it frames.
35
+ Full reasoning above INSET.
36
+ chrome/media SHARE a value (one grid unit) but mean different things: chrome is a margin from
37
+ every edge, media is a margin from the left/right edges only.
38
+ ★ MEDIA'S VERTICAL BOUND, and why it is not one number. A figure's top and bottom follow the
39
+ NOTCH, together: a notched figure rises to --deck-inset-media on both, because the cut is what
40
+ keeps the running mark clear and the mark's own corner and the figure's are then the same
41
+ point; an un-notched one (notch: false, chrome: false, or a media:none second content column)
42
+ takes --deck-inset-text on both, which is the pre-notch geometry the escape hatch is
43
+ documented to restore. What it is NOT is 36 on three sides and 90 on the fourth — that was the
44
+ shipped state on all 86 media figures, and 54px of unexplained bottom-heaviness is not a rule,
45
+ it is a rule applied to half a box. See split.vue's .split-figure. */
46
+ --deck-inset-chrome: calc(var(--deck-grid) * 1.5); /* 36px · corner furniture (mark, page number, section pill) — all four edges */
47
+ --deck-inset-media: calc(var(--deck-grid) * 1.5); /* 36px · figures and illustrations — HORIZONTAL edges only; vertical bound is --deck-inset-media on a notched figure, --deck-inset-text otherwise */
48
+ --deck-inset-text: calc(var(--deck-grid) * 4); /* 96px · text content — all four edges */
49
+ --deck-inset-display: calc(var(--deck-grid) * 5); /* 120px · the beat layouts (cover/quote/statement/divider/end) — HORIZONTAL edges only, sized against 96–128px display type */
50
+
51
+ /* ── Layout anchors and extras — points and gaps, not edge margins ───────────────────────
52
+ task-14b-brief.md. Two things split.vue needs beyond the three insets above:
53
+ anchor-third the text column's fixed vertical START, "so the headings dont jump between
54
+ slides" (Jose). A FRACTION of the canvas, not a hard-coded 360px, so it stays
55
+ correct if the canvas itself ever changes — see split.vue's own comment on
56
+ .split-grid for the one place its 100% basis is actually the full 1080px
57
+ canvas rather than some padded descendant of it.
58
+ heading-top-space extra air above a NON-split heading, so it starts clear of the top edge
59
+ rather than flush against it. One full grid unit — see default.vue.
60
+ heading-bottom-space the gap under a slide's h1, before its first block — base.css. Paired
61
+ with the one above so the air on both sides of a title is retuned in one
62
+ place, and so a deck that budgets from where content starts (apps/deck
63
+ does) reads the number instead of restating it. h2 keeps its own tighter gap:
64
+ it sits INSIDE the content rather than starting it. */
65
+ --deck-anchor-third: 360px; /* 360px · split.vue's text column start — one third of the canvas down */
66
+ --deck-heading-top-space: var(--deck-space-5); /* 40px · extra air above a non-split heading — see default.vue */
67
+ --deck-heading-bottom-space: var(--deck-space-4); /* 32px · the gap under a slide's h1, before its first block — see base.css */
68
+ --deck-split-gutter-same: var(--deck-inset-text); /* 96px · split.vue, same-kind columns only (text|text, media|media) — mixed splits use 0 and pad the content column instead */
69
+
70
+ /* ── The running mark, and the notch it leaves in a panel ────────────────────────────────
71
+ task-15-brief.md. The cut-out is DERIVED from its occupant and never sized beside it: the
72
+ occupant's own box is the input, the notch is that box plus one gap, and changing the mark
73
+ moves both with no second edit. Two readers agree on these because they are tokens rather than
74
+ a number inside one component — slide-top.vue draws the mark, split.vue cuts room for it.
75
+
76
+ The ring is the CHROME INSET again, not a new number: the mark already keeps 36px of clear
77
+ space on the two sides facing the slide's edge, so the same value on the two sides facing into
78
+ the panel gives it a uniform ring and makes it read as seated rather than parked. The occupant
79
+ sits FLUSH in the panel's outer corner (chrome and media share an inset, so the two corners
80
+ coincide), which is why the cut is occupant + ONE gap.
81
+
82
+ --deck-notch-occupant-w/-h are NOT declared here on purpose. Consumers read them as
83
+ var(--deck-notch-occupant-w, var(--deck-chrome-mark)) so the default lives in the fallback: a
84
+ custom property declared at :root resolves ONCE, there, and a slide overriding the occupant
85
+ further down would inherit the stale :root value of anything derived from it. Left undeclared,
86
+ an override on the slide root reaches every reader — which is what lets one mark and two marks
87
+ produce correctly-shaped notches with no code change. */
88
+ --deck-chrome-mark: 72px; /* 72px · the running chrome's Forest mark — and the notch's default occupant */
89
+ --deck-chrome-band: calc(var(--deck-inset-chrome) + var(--deck-chrome-mark)); /* 108px · how deep the running chrome reaches from the canvas edge — inset + mark */
90
+ --deck-chrome-pair-gap: var(--deck-space-2); /* 16px · between the Forest mark and a partner's, in a co-branded lockup (`partner:`) */
91
+ --deck-notch-gap: calc(var(--deck-grid) * 1); /* 24px · the breathing ring cut between occupant and panel — tighter than the slide margin, see scripts/deck.ts */
92
+ --deck-mark-radius: 15.75px; /* 15.75px · the mark's own corner, read from logo.ts's square path — what a co-brand partner is clipped to */
93
+ --deck-mark-ring-width: 3px; /* 3px · matched against the artwork's own ring at 72 */
94
+ --deck-notch-radius: 39.75px; /* 39.75px · concentric with the mark: its 15.75px corner + the 24px ring; split.vue clamps it to half the notch */
95
+
96
+ /* ── Radius contract — one lever, everything derives ─────────────────────────────────────
97
+ Same model and same lever value as the web's --ui-radius. The card step is *6 rather than the
98
+ web's xl/*3 on purpose: a deck is a BOLD surface, and on a 1920px canvas a 24px corner reads as
99
+ a hairline at presenting size. */
100
+ --deck-radius: 0.5rem;
101
+ --deck-radius-item: calc(var(--deck-radius) * 1); /* 8px · code chips, tags, swatches */
102
+ --deck-radius-control: calc(var(--deck-radius) * 1.5); /* 12px · buttons, pills */
103
+ --deck-radius-card: calc(var(--deck-radius) * 6); /* 48px · the slide card */
104
+
105
+ /* Concentric corners: a nested surface = parent radius − parent padding, floored at 8px, so the
106
+ inner curve stays parallel to the outer one. Two steps (48 → 16 → 8), like .forest-card. */
107
+ --deck-card-pad: var(--deck-space-4); /* 32px */
108
+ --deck-radius-nested: max(calc(var(--deck-radius-card) - var(--deck-card-pad)), 0.5rem);
109
+ --deck-radius-nested-2: max(calc(var(--deck-radius-card) - 2 * var(--deck-card-pad)), 0.5rem);
110
+
111
+ /* Fully rounded — the pill/circle end of the ladder, NOT a step on it. Deliberately a large
112
+ constant rather than a multiple of the lever: a pill's corner is "half my own height, whatever
113
+ that turns out to be", which no fixed multiplier expresses. Moving the lever must not turn a
114
+ pill back into a rounded rectangle. Used by the running chrome's page marker and by the small
115
+ chips, both of which read as markers rather than as surfaces. */
116
+ --deck-radius-full: 9999px;
117
+
118
+ /* ── Colour roles — light, and light IS the deck ─────────────────────────────────────────
119
+ Ice Cream (--warm-50) is a named brand pin, not a tint: warm paper, near-black ink, the accent
120
+ doing all of the shouting. Boldness comes from the display tier, the full-bleed accent sections
121
+ and the scale of the type — not from inverting the deck. */
122
+ --deck-bg: var(--warm-50);
123
+ --deck-fg: var(--neutral-900); /* neutral, not forest-950 — text is one ramp, so dimming is lightness and never a hue shift */
124
+ --deck-fg-muted: var(--neutral-600); /* 600, not 500 — the web contrast floor for muted text on light */
125
+ --deck-fg-subtle: var(--neutral-500);
126
+ --deck-accent: var(--forest-700);
127
+ --deck-accent-strong: var(--forest-800);
128
+ --deck-on-accent: #ffffff;
129
+
130
+ /* Named backgrounds and their ink, FOLLOWING the register — what anything that is PART OF the
131
+ deck reads: a panel, a band, a card ground. See PANEL for why the names are relative ('warm',
132
+ not 'bark'). The pinned halves are emitted once, below. */
133
+ --deck-bg-warm: var(--warm-200);
134
+ --deck-on-bg-warm: var(--warm-950);
135
+ --deck-bg-accent: var(--forest-100);
136
+ --deck-on-bg-accent: var(--forest-950);
137
+ --deck-bg-vivid: var(--forest-300);
138
+ --deck-on-bg-vivid: var(--forest-950);
139
+ --deck-bg-neutral: var(--neutral-100);
140
+ --deck-on-bg-neutral: var(--neutral-900);
141
+
142
+ /* The same four names as INK on the page, for anything that draws the tone as a stroke or a label
143
+ rather than as a ground: an outline badge, a rule, a keyline. See PANEL's ★ note on why this is
144
+ the OPPOSITE half from the one directly above. */
145
+ --deck-ink-warm: var(--warm-700);
146
+ --deck-ink-accent: var(--forest-950);
147
+ --deck-ink-vivid: var(--forest-700);
148
+ --deck-ink-neutral: var(--neutral-800);
149
+
150
+ /* ── the MARK a background carries ──────────────────────────────────────────────────────────
151
+ ★★ A SMALL SHAPE DRAWN ON THE BACKGROUND, IN THAT BACKGROUND'S OWN FAMILY — a list bullet
152
+ today, and anything else of that size later. Not the ink pair above, which is the OPPOSITE half
153
+ and exists for drawing this name on the PAGE; not on-bg, which is the body ink and reads as
154
+ text rather than as a mark. This is the liveliest step of the same family that still clears the
155
+ 3:1 a graphical object is held to against the fill it lands on.
156
+
157
+ Chosen by measuring the whole ramp against each fill rather than by picking a rung. Warm wanted
158
+ an orange rather than the bark: on peach nothing lighter than 600 clears the floor (500 is
159
+ 2.66) so 600 it is, and on bark the orange 300 clears at 3.84. The green fields wanted the
160
+ vivid green: on the quiet accent field forest-700 gives 5.45, and on the night step forest-300
161
+ gives 8.51. Vivid itself cannot take a mint on its light half — every light green is invisible
162
+ on forest-300, and 200 measures 1.28 — so it takes 800 there and the mint 200 on its dark half,
163
+ 4.53.
164
+
165
+ MEASURED, mark against its own fill: warm 3.83 light / 3.84 dark, accent 5.45 / 8.51, vivid
166
+ 4.78 / 4.53, neutral 4.18 / 5.74.
167
+
168
+ Named bg-NAME-mark rather than mark-NAME: --deck-mark-radius is the LOGO mark's corner and
169
+ --deck-status-STATE-mark is a semantic state's fill, so the prefix is already spoken for. This
170
+ one belongs to the background, and is named where it lives. */
171
+ --deck-bg-warm-mark: var(--warm-600);
172
+ --deck-bg-accent-mark: var(--forest-700);
173
+ --deck-bg-vivid-mark: var(--forest-800);
174
+ --deck-bg-neutral-mark: var(--neutral-500);
175
+
176
+ /* Chart series alias the published categorical palette in the product's ORDER, so a deck chart and
177
+ a product chart of the same data are the same chart. */
178
+ --deck-series-1: var(--dataviz-categorical-vivid-green);
179
+ --deck-series-2: var(--dataviz-categorical-vivid-amber);
180
+ --deck-series-3: var(--dataviz-categorical-vivid-sky);
181
+ --deck-series-4: var(--dataviz-categorical-vivid-indigo);
182
+ --deck-series-5: var(--dataviz-categorical-vivid-pink);
183
+ --deck-series-6: var(--dataviz-categorical-vivid-red);
184
+
185
+ /* The sequential ramp — one hue, five steps, for a quantity rather than a category. Light register
186
+ runs pale to deep, so more ink means more of whatever is being mapped. */
187
+ --deck-shade-1: var(--dataviz-sequential-forest-2);
188
+ --deck-shade-2: var(--dataviz-sequential-forest-3);
189
+ --deck-shade-3: var(--dataviz-sequential-forest-4);
190
+ --deck-shade-4: var(--dataviz-sequential-forest-6);
191
+ --deck-shade-5: var(--dataviz-sequential-forest-8);
192
+
193
+ /* The Thames takes the palette's own river pair rather than a grey — it is the one feature that
194
+ tells an audience which city they are looking at, a data colour doing a wayfinding job. */
195
+ --deck-dot-river: var(--dataviz-categorical-pairs-river-soft);
196
+
197
+ /* ── The pinned halves — PAIRED with their ink, never emitted alone ──────────────────────
198
+ The same four backgrounds, each half nailed to one register and never re-pointed. This is what
199
+ an OBJECT sitting on the deck reads — a badge, an icon tile, a running pill — where the names
200
+ above are what a SURFACE of the deck reads. A component picks "<name>-<half>" and never
201
+ assembles a fill and an ink from two different rows.
202
+
203
+ ★ THE INK IS "on-bg-<name>", NOT "on-<name>", and the bg- is load-bearing. This file already
204
+ has a --deck-accent role (the brand green for claims and chrome) whose ink is --deck-on-accent,
205
+ so a shortened on-accent here silently redefined it — measured, the role's forest-700 fill
206
+ ended up paired with forest-950 ink. The on- prefix wraps the FULL name of the fill it belongs
207
+ to, which is what on-status-danger and the old on-panel-accent already did.
208
+
209
+ ★★ ONE TABLE, WHICH IS THE POINT. This was CHIP, holding the same literals PANEL held, and the
210
+ two drifted the first time one moved: deep was accent's dark half until that half went to the
211
+ night step, and nothing said so. A name is a background plus a half now, so a value cannot go
212
+ stale in one copy.
213
+
214
+ ⚠ PINNED IS NOT THE SAME AS FOLLOWING, and the caller owns the difference. "accent-dark" on a
215
+ dark slide IS that slide's own ground and disappears — measured 1.00:1. That is not a fault in
216
+ the token; it is what a fixed dark object on a dark page means. The follow tokens above exist
217
+ so a surface never has to make that choice. */
218
+ --deck-bg-warm-light: var(--warm-200);
219
+ --deck-on-bg-warm-light: var(--warm-950);
220
+ --deck-bg-warm-dark: var(--warm-700);
221
+ --deck-on-bg-warm-dark: var(--neutral-0);
222
+ --deck-bg-accent-light: var(--forest-100);
223
+ --deck-on-bg-accent-light: var(--forest-950);
224
+ --deck-bg-accent-dark: var(--forest-950);
225
+ --deck-on-bg-accent-dark: var(--neutral-0);
226
+ --deck-bg-vivid-light: var(--forest-300);
227
+ --deck-on-bg-vivid-light: var(--forest-950);
228
+ --deck-bg-vivid-dark: var(--forest-700);
229
+ --deck-on-bg-vivid-dark: var(--neutral-0);
230
+ --deck-bg-neutral-light: var(--neutral-100);
231
+ --deck-on-bg-neutral-light: var(--neutral-900);
232
+ --deck-bg-neutral-dark: var(--neutral-800);
233
+ --deck-on-bg-neutral-dark: var(--neutral-0);
234
+
235
+ /* Status roles, a SET per state — see the STATUS table. The subtle four are alpha over the
236
+ semantic role, which is stock's own construction; the dark block restates them off the 400
237
+ because a var() inside a custom property is substituted where that property is DECLARED. */
238
+ --deck-status-info-fill: color-mix(in oklab, var(--river-500) 10%, transparent);
239
+ --deck-status-info-ink: var(--river-600);
240
+ --deck-status-info-icon: var(--river-500);
241
+ --deck-status-info-edge: color-mix(in oklab, var(--river-500) 25%, transparent);
242
+ --deck-status-info-solid: var(--river-500);
243
+ --deck-status-info-mark: var(--river-500);
244
+ --deck-on-status-info: #ffffff;
245
+ --deck-status-success-fill: color-mix(in oklab, var(--forest-500) 10%, transparent);
246
+ --deck-status-success-ink: var(--forest-600);
247
+ --deck-status-success-icon: var(--forest-500);
248
+ --deck-status-success-edge: color-mix(in oklab, var(--forest-500) 25%, transparent);
249
+ --deck-status-success-solid: var(--forest-500);
250
+ --deck-status-success-mark: var(--forest-500);
251
+ --deck-on-status-success: #ffffff;
252
+ --deck-status-warning-fill: color-mix(in oklab, var(--amber-500) 10%, transparent);
253
+ --deck-status-warning-ink: var(--amber-600);
254
+ --deck-status-warning-icon: var(--amber-500);
255
+ --deck-status-warning-edge: color-mix(in oklab, var(--amber-500) 25%, transparent);
256
+ --deck-status-warning-solid: var(--amber-500);
257
+ --deck-status-warning-mark: var(--amber-500);
258
+ --deck-on-status-warning: #ffffff;
259
+ --deck-status-danger-fill: color-mix(in oklab, var(--maple-500) 10%, transparent);
260
+ --deck-status-danger-ink: var(--maple-600);
261
+ --deck-status-danger-icon: var(--maple-500);
262
+ --deck-status-danger-edge: color-mix(in oklab, var(--maple-500) 25%, transparent);
263
+ --deck-status-danger-solid: var(--maple-500);
264
+ --deck-status-danger-mark: var(--maple-500);
265
+ --deck-on-status-danger: #ffffff;
266
+ --deck-status-neutral-fill: color-mix(in oklab, var(--neutral-500) 10%, transparent);
267
+ --deck-status-neutral-ink: var(--neutral-600);
268
+ --deck-status-neutral-icon: var(--neutral-500);
269
+ --deck-status-neutral-edge: color-mix(in oklab, var(--neutral-500) 25%, transparent);
270
+ --deck-status-neutral-solid: var(--neutral-500);
271
+ --deck-status-neutral-mark: var(--neutral-500);
272
+ --deck-on-status-neutral: #ffffff;
273
+ }
274
+
275
+ /* ── Dark — punctuation, not the ground ───────────────────────────────────────────────────────
276
+ The deck runs LIGHT. Dark is a per-slide instrument: a hard cut marking a pivot, landing a
277
+ statement, or letting a chart sit on a black ground.
278
+
279
+ Bound to TWO selectors: `.dark` is the whole-deck toggle (Slidev stamps it on <html>), and
280
+ `.deck-dark` is a single slide opting in from frontmatter. */
281
+ .dark,
282
+ .deck-dark {
283
+ --deck-bg: var(--forest-950);
284
+ --deck-fg: var(--neutral-0);
285
+ --deck-fg-muted: var(--neutral-400);
286
+ --deck-fg-subtle: var(--neutral-500);
287
+ --deck-accent: var(--forest-300);
288
+ --deck-accent-strong: var(--forest-200);
289
+ --deck-on-accent: var(--forest-950);
290
+
291
+ /* The same panel grounds, on the other side of the register — the half that did not exist. */
292
+ --deck-bg-warm: var(--warm-700);
293
+ --deck-on-bg-warm: var(--neutral-0);
294
+ --deck-bg-accent: var(--forest-950);
295
+ --deck-on-bg-accent: var(--neutral-0);
296
+ --deck-bg-vivid: var(--forest-700);
297
+ --deck-on-bg-vivid: var(--neutral-0);
298
+ --deck-bg-neutral: var(--neutral-800);
299
+ --deck-on-bg-neutral: var(--neutral-0);
300
+ --deck-ink-warm: var(--warm-200);
301
+ --deck-ink-accent: var(--forest-100);
302
+ --deck-ink-vivid: var(--forest-300);
303
+ --deck-ink-neutral: var(--neutral-100);
304
+ --deck-bg-warm-mark: var(--warm-300);
305
+ --deck-bg-accent-mark: var(--forest-300);
306
+ --deck-bg-vivid-mark: var(--forest-200);
307
+ --deck-bg-neutral-mark: var(--neutral-400);
308
+ --deck-dot-river: var(--dataviz-categorical-pairs-river-deep);
309
+
310
+ /* ★ THE RAMP REVERSES ON A DARK GROUND, because "more" is a bigger distance FROM the page and the
311
+ page moved. Deep forest on near-black is the step that disappears, exactly as pale forest is the
312
+ one that disappears on paper — so the same five steps run the other way and the reading ("more
313
+ ink means more") survives the register change. */
314
+ --deck-shade-1: var(--dataviz-sequential-forest-8);
315
+ --deck-shade-2: var(--dataviz-sequential-forest-6);
316
+ --deck-shade-3: var(--dataviz-sequential-forest-4);
317
+ --deck-shade-4: var(--dataviz-sequential-forest-3);
318
+ --deck-shade-5: var(--dataviz-sequential-forest-2);
319
+ }
320
+
321
+ /* ── The design system's own dual-mode tokens, answered in the DECK's register ────────────────
322
+ ★★ WITHOUT THIS BLOCK A CHART ON A DARK SLIDE PAINTS THE LIGHT PALETTE, and nothing anywhere
323
+ says so. The series and the Thames alias published dataviz tokens rather than restating them —
324
+ which is the point, so a deck chart and a product chart of the same data are the same chart. But
325
+ those tokens flip on .dark, and a dark SLIDE is .deck-dark: Slidev stamps .dark on <html>
326
+ only when the whole deck is dark. Measured on the built deck — html.dark false, no .dark
327
+ ancestor anywhere in the document — so on a .deck-dark slide the alias resolved to the LIGHT
328
+ value and the dark half of a dual-mode palette was unreachable.
329
+
330
+ It is the same shape as the shiki defect this theme already carries a note about: html.dark and
331
+ .deck-dark are different things, and code that assumes one covers the other is wrong quietly.
332
+
333
+ What it cost, measured against the deck's dark ground (forest-950):
334
+
335
+ series light (what was drawn) dark (what should be)
336
+ green 4.72 8.70
337
+ indigo 2.39 ← under 3:1 5.00
338
+ amber 4.84 9.04
339
+ pink 3.34 5.60
340
+ sky 3.78 7.02
341
+
342
+ Indigo sat below the 3:1 floor a graphical object is held to, and no gate could see it: the
343
+ audit's contrast pass measures TEXT, and a chart fill is not text.
344
+
345
+ ★★ THE FIX IS BOTH HALVES, AND THE SECOND ONE IS NOT OPTIONAL. Re-pointing the source token
346
+ alone looks sufficient and is not: a var() inside a custom property is substituted where that
347
+ property is DECLARED, and --deck-series-N is declared on :root, so its alias resolves against
348
+ :root's value no matter what this block says. Measured on the built deck after the source
349
+ re-point alone — --deck-series-4 (indigo) still read #4f39f6, the LIGHT indigo, on a .deck-dark slide.
350
+ The ROLE has to be re-declared here too, which is what re-resolves its alias in this register.
351
+ (--deck-dot-river needed nothing: it is already declared in the dark role block above, on the
352
+ same element, so its alias re-resolved on its own. That asymmetry is the whole rule — a role
353
+ declared only on :root is frozen there; one declared here is not.)
354
+
355
+ Re-pointing the token as well is still worth doing: it keeps the re-declared role pointing at
356
+ the PUBLISHED token rather than at a hex, so the palette stays the single source, and anything
357
+ reading a dataviz token directly in this register gets the right value.
358
+
359
+ Values come from colourEngine's own tables, the same source scripts/dataviz.ts generates from, so
360
+ there is no second copy of the palette to drift. packages/slidev-theme/test/deckRegister.static.test.ts
361
+ asserts this block covers every dual-mode dataviz token deck.css references.
362
+
363
+ .dark needs none of this: dataviz.css already answers it there. */
364
+ .deck-dark {
365
+ --dataviz-categorical-vivid-green: #05df72;
366
+ --dataviz-categorical-vivid-amber: #fabc00;
367
+ --dataviz-categorical-vivid-sky: #00bbfd;
368
+ --dataviz-categorical-vivid-indigo: #7d87ff;
369
+ --dataviz-categorical-vivid-pink: #fb64b6;
370
+ --dataviz-categorical-vivid-red: #ff6568;
371
+ --dataviz-categorical-pairs-forest-deep: #0abb7f;
372
+ --dataviz-categorical-pairs-forest-soft: #99e9bc;
373
+ --dataviz-categorical-pairs-warm-deep: #f48519;
374
+ --dataviz-categorical-pairs-warm-soft: #ffd6a6;
375
+ --dataviz-categorical-pairs-river-deep: #43a2ff;
376
+ --dataviz-categorical-pairs-river-soft: #b2d9ff;
377
+
378
+ /* The semantic mark, one rung lighter — see STATUS_MARK_DARK for the measurement. */
379
+ --deck-status-info-mark: var(--river-400);
380
+ --deck-status-success-mark: var(--forest-400);
381
+ --deck-status-warning-mark: var(--amber-400);
382
+ --deck-status-danger-mark: var(--maple-400);
383
+ --deck-status-neutral-mark: var(--neutral-400);
384
+
385
+ /* ★★ AND THE SUBTLE FOUR WITH IT, restated rather than inherited. They are color-mix results
386
+ built off the mark, and a custom property computes where it is DECLARED — re-pointing the mark
387
+ above would leave a fill declared at :root holding the 500 it was substituted with there. Same
388
+ trap the derived surfaces and the series roles record. This is the whole of what stock gets for
389
+ free from a dark: class never appearing in its theme: the token moves, so the variant moves. */
390
+ --deck-status-info-fill: color-mix(in oklab, var(--river-400) 10%, transparent);
391
+ --deck-status-info-ink: var(--river-400);
392
+ --deck-status-info-icon: var(--river-400);
393
+ --deck-status-info-edge: color-mix(in oklab, var(--river-400) 25%, transparent);
394
+ --deck-status-success-fill: color-mix(in oklab, var(--forest-400) 10%, transparent);
395
+ --deck-status-success-ink: var(--forest-400);
396
+ --deck-status-success-icon: var(--forest-400);
397
+ --deck-status-success-edge: color-mix(in oklab, var(--forest-400) 25%, transparent);
398
+ --deck-status-warning-fill: color-mix(in oklab, var(--amber-400) 10%, transparent);
399
+ --deck-status-warning-ink: var(--amber-400);
400
+ --deck-status-warning-icon: var(--amber-400);
401
+ --deck-status-warning-edge: color-mix(in oklab, var(--amber-400) 25%, transparent);
402
+ --deck-status-danger-fill: color-mix(in oklab, var(--maple-400) 10%, transparent);
403
+ --deck-status-danger-ink: var(--maple-400);
404
+ --deck-status-danger-icon: var(--maple-400);
405
+ --deck-status-danger-edge: color-mix(in oklab, var(--maple-400) 25%, transparent);
406
+ --deck-status-neutral-fill: color-mix(in oklab, var(--neutral-400) 10%, transparent);
407
+ --deck-status-neutral-ink: var(--neutral-400);
408
+ --deck-status-neutral-icon: var(--neutral-400);
409
+ --deck-status-neutral-edge: color-mix(in oklab, var(--neutral-400) 25%, transparent);
410
+
411
+ /* The solid panel and its ink, as ONE PAIR — see STATUS_SOLID_DARK. The fill lifts so the block
412
+ separates from a near-black slide; the ink turns over because white cannot survive the lighter
413
+ fill it just moved to. */
414
+ --deck-status-info-solid: var(--river-400);
415
+ --deck-on-status-info: var(--river-950);
416
+ --deck-status-success-solid: var(--forest-400);
417
+ --deck-on-status-success: var(--forest-950);
418
+ --deck-status-warning-solid: var(--amber-400);
419
+ --deck-on-status-warning: var(--amber-950);
420
+ --deck-status-danger-solid: var(--maple-400);
421
+ --deck-on-status-danger: var(--maple-950);
422
+ --deck-status-neutral-solid: var(--neutral-400);
423
+ --deck-on-status-neutral: var(--neutral-950);
424
+
425
+ /* ★★ AND THE SERIES ROLES THEMSELVES, not only the token they alias. A var() inside a custom
426
+ property is substituted where that property is DECLARED — --deck-series-N is declared at :root,
427
+ so its alias resolves against :root and re-pointing the source here never reaches it. Measured
428
+ on the built deck after the source re-point alone: --deck-series-4 (indigo) still read #4f39f6, the LIGHT
429
+ indigo, on a .deck-dark slide. Re-declaring the role here is what re-resolves the alias in this
430
+ register. It still points at the published token rather than a hex, so there is no second copy
431
+ of the palette; the token beside it is what makes that reference land on the dark value. */
432
+ --deck-series-1: var(--dataviz-categorical-vivid-green);
433
+ --deck-series-2: var(--dataviz-categorical-vivid-amber);
434
+ --deck-series-3: var(--dataviz-categorical-vivid-sky);
435
+ --deck-series-4: var(--dataviz-categorical-vivid-indigo);
436
+ --deck-series-5: var(--dataviz-categorical-vivid-pink);
437
+ --deck-series-6: var(--dataviz-categorical-vivid-red);
438
+ }
439
+
440
+ /* ── The brand-pairs opt-in ───────────────────────────────────────────────────────────────────
441
+ A slide-level class, written beside `class: deck-dark` in frontmatter, that re-points the series
442
+ roles to the published BRAND pairs: three brand hues, each contributing a deep and a soft tone.
443
+ Nothing in a chart component knows about it — a declaration on the slide element beats :root's
444
+ inherited value, which is the same cascade rule the dark register already relies on.
445
+
446
+ ★ FAMILY ORDER, exactly as the product's own pairsVar() walks it (packages/charts/src/
447
+ useChartPalette.ts), so consecutive slots are the SAME hue. That is the whole semantics: a pair
448
+ says "these two series are two views of one thing". It also means a pairs chart wants an EVEN
449
+ series count — at five, slot 5 is river-deep with its soft tone out of reach, and the last series
450
+ reads as a lone hue rather than half a pair.
451
+
452
+ ★★ THE CEILING, WRITTEN DOWN, because it is severe and it is invisible to every gate this repo
453
+ has. On the LIGHT register the pairs do not clear the 3:1 a graphical mark is held to. Measured
454
+ against the deck's own grounds — bare cream / --deck-surface / the since-folded 8% bg-subtle:
455
+
456
+ forest-deep 3.25 / 2.90 / 2.69 forest-soft 1.74 / 1.55 / 1.44
457
+ warm-deep 3.46 / 3.09 / 2.87 warm-soft 1.75 / 1.56 / 1.45
458
+ river-deep 3.50 / 3.12 / 2.90 river-soft 1.84 / 1.64 / 1.53
459
+
460
+ The soft tones are the deck's lowest-contrast marks by a distance — below the 2.39 that got the
461
+ vivid indigo re-pointed one register over. Even the DEEP tones only clear 3:1 on bare cream, and
462
+ Bars draws its track in --deck-surface. So: pairs belong on a `deck-dark` slide, where all six
463
+ measure 4.72-11.37. That is not a workaround; a saturated brand chart on a dark ground is one of
464
+ the three things the dark register exists for.
465
+
466
+ ★ The cost on dark is COLOUR-VISION, not contrast, and it is why this is a small instrument
467
+ rather than a replacement for the vivid five. Cross-family the soft tones converge — forest-soft
468
+ against warm-soft is worst-case-CVD dE00 5, where vivid-5 holds 8. Within a pair the separation
469
+ is carried by lightness (a constant +17 L*) and survives every dichromacy at 13-18. Read that as
470
+ the instruction it is: pairs is at its best as TWO series from ONE family, and degrades as it is
471
+ asked to carry more unrelated categories.
472
+
473
+ NOT RE-WINDOWED for the deck. Taking each pair a rung darker clears contrast on cream but costs
474
+ the brand: warm-700 is #8a462e, a brown, and warm-500 is the only warm step that is both above
475
+ 3:1 and still Acer orange. The published pairs are brand ramp steps exactly (light 500/300, dark
476
+ 400/200); a deck-local re-derivation would be a second palette, which is the thing this file is
477
+ organised to avoid. */
478
+ /* ★★ TWO SELECTORS, ONE TABLE, AND THE SECOND IS NOT REDUNDANT. A custom property substitutes its
479
+ var() where it is DECLARED, so on `.deck-pairs` alone every slot resolves against the slide
480
+ element — and a `deck-dark` PANEL inside that slide inherits the finished LIGHT colour, however
481
+ correctly the source token re-points beneath it. The panel would draw the light pairs on a dark
482
+ ground, which is the pairing this whole block exists to make unreachable.
483
+
484
+ Re-declaring under `.deck-pairs .deck-dark` makes the substitution happen again inside the
485
+ panel, where `.deck-dark` has already re-pointed --dataviz-categorical-pairs-* to their dark
486
+ values. Same trap, same fix, as the derived surfaces below and the dataviz aliases above. */
487
+ .deck-pairs,
488
+ .deck-pairs .deck-dark {
489
+ --deck-series-1: var(--dataviz-categorical-pairs-forest-deep);
490
+ --deck-series-2: var(--dataviz-categorical-pairs-forest-soft);
491
+ --deck-series-3: var(--dataviz-categorical-pairs-warm-deep);
492
+ --deck-series-4: var(--dataviz-categorical-pairs-warm-soft);
493
+ --deck-series-5: var(--dataviz-categorical-pairs-river-deep);
494
+ --deck-series-6: var(--dataviz-categorical-pairs-river-soft);
495
+ }
496
+
497
+ /* ── Surfaces and borders — DERIVED from the ground, not picked off a ramp ────────────────────
498
+ Each level is the page mixed with a little ink — the same mechanism as .forest-card's l1/l2/l3.
499
+ It obeys "surfaces step AWAY from the page: light DARKER, dark LIGHTER" automatically, because the
500
+ ink flips with the mode; and it re-tunes itself the moment --deck-bg moves, which a ramp step
501
+ cannot (the warm ramp has nothing between 50 and 100, a real peach).
502
+
503
+ ★★ Declared on EVERY mode boundary, NOT just :root. A custom property computes ONCE where it is
504
+ declared and then inherits that fixed colour — so on :root alone a `class: deck-dark` slide would
505
+ re-point --deck-bg and still inherit the LIGHT surface resolved at :root. Same trap, same fix, as
506
+ forest.css's card levels. */
507
+ :root,
508
+ .dark,
509
+ .deck-dark {
510
+ --deck-surface: color-mix(in oklab, var(--deck-bg), var(--deck-fg) 5%); /* L1 — a card on the page */
511
+ --deck-surface-strong: color-mix(in oklab, var(--deck-bg), var(--deck-fg) 10%); /* L2 — a card on a card. Was surface-raised: a name claiming a direction the ladder has none of */
512
+ --deck-border: color-mix(in oklab, var(--deck-bg), var(--deck-fg) 14%);
513
+ --deck-border-strong: color-mix(in oklab, var(--deck-bg), var(--deck-fg) 26%); /* the heavier rule — an axis against its gridlines, an edge against a hatch */
514
+ --deck-dot-land: color-mix(in oklab, var(--deck-bg), var(--deck-fg) 34%); /* the map dot grid — has to read as a SHAPE across a room */
515
+ }