@humanforest/slidev-theme 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 (124) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +577 -0
  3. package/assetUrl.js +38 -0
  4. package/components/Alert.vue +209 -0
  5. package/components/AreaBox.vue +152 -0
  6. package/components/AreaChart.vue +145 -0
  7. package/components/Avatar.vue +267 -0
  8. package/components/AvatarGroup.vue +94 -0
  9. package/components/BadgeStrip.vue +203 -0
  10. package/components/Bars.vue +207 -0
  11. package/components/BoroughShape.vue +127 -0
  12. package/components/BrowserFrame.vue +484 -0
  13. package/components/CalendarGrid.vue +480 -0
  14. package/components/Card.vue +70 -0
  15. package/components/Carousel.vue +266 -0
  16. package/components/ChartFrame.vue +426 -0
  17. package/components/ChordChart.vue +264 -0
  18. package/components/ComposedChart.vue +187 -0
  19. package/components/Contents.vue +86 -0
  20. package/components/DataTable.vue +244 -0
  21. package/components/DeckMap.vue +409 -0
  22. package/components/DeviceFrame.vue +331 -0
  23. package/components/DonutChart.vue +103 -0
  24. package/components/DottedMap.vue +126 -0
  25. package/components/Endorsement.vue +76 -0
  26. package/components/Grid.vue +169 -0
  27. package/components/GroupedBarChart.vue +141 -0
  28. package/components/IconTile.vue +116 -0
  29. package/components/Kpi.vue +188 -0
  30. package/components/LineChart.vue +133 -0
  31. package/components/Logo.vue +203 -0
  32. package/components/LondonMap.vue +348 -0
  33. package/components/MarkPair.vue +109 -0
  34. package/components/Marquee.vue +263 -0
  35. package/components/NestedDonutChart.vue +295 -0
  36. package/components/Placeholder.vue +88 -0
  37. package/components/SankeyChart.vue +223 -0
  38. package/components/ScatterChart.vue +142 -0
  39. package/components/StackedBarChart.vue +143 -0
  40. package/components/StatCard.vue +134 -0
  41. package/components/StatusTrack.vue +334 -0
  42. package/components/Timeline.vue +249 -0
  43. package/components/TimelineChart.vue +329 -0
  44. package/components/TreemapChart.vue +267 -0
  45. package/components/backgrounds.js +135 -0
  46. package/components/boroughGeometry.ts +59 -0
  47. package/components/chartData.ts +277 -0
  48. package/components/chartTip.ts +201 -0
  49. package/components/codeTheme.ts +239 -0
  50. package/components/deckPalette.ts +157 -0
  51. package/components/dottedMapGeometry.js +60 -0
  52. package/components/iconSlugs.js +55 -0
  53. package/components/iconUrl.js +51 -0
  54. package/components/mermaidTheme.ts +972 -0
  55. package/components/motionDuration.ts +41 -0
  56. package/components/tableRules.ts +257 -0
  57. package/confidential-mark.vue +151 -0
  58. package/custom-nav-controls.vue +79 -0
  59. package/endorsements/b-corp-black.svg +30 -0
  60. package/endorsements/b-corp-white.svg +30 -0
  61. package/global-top.vue +1662 -0
  62. package/icons/forest/bike-asterisk.svg +21 -0
  63. package/icons/forest/bike-down.svg +19 -0
  64. package/icons/forest/bike-off.svg +20 -0
  65. package/icons/forest/bike-up.svg +19 -0
  66. package/icons/forest/bike-x.svg +19 -0
  67. package/icons/forest/bike.svg +18 -0
  68. package/icons/forest/mark-fill.svg +6 -0
  69. package/icons/forest/mark.svg +6 -0
  70. package/icons/forest/parking.svg +3 -0
  71. package/icons/forest/star-fill.svg +4 -0
  72. package/icons/forest/star.svg +4 -0
  73. package/icons/forest/traffic-light-caution.svg +8 -0
  74. package/icons/forest/traffic-light-go.svg +8 -0
  75. package/icons/forest/traffic-light-lit.svg +8 -0
  76. package/icons/forest/traffic-light-stop.svg +8 -0
  77. package/icons/forest/traffic-light.svg +8 -0
  78. package/icons/lucide/battery.svg +1 -0
  79. package/icons/lucide/bike.svg +1 -0
  80. package/icons/lucide/chart-line.svg +1 -0
  81. package/icons/lucide/check.svg +1 -0
  82. package/icons/lucide/circle-check.svg +1 -0
  83. package/icons/lucide/clock.svg +1 -0
  84. package/icons/lucide/code.svg +1 -0
  85. package/icons/lucide/component.svg +1 -0
  86. package/icons/lucide/frame.svg +1 -0
  87. package/icons/lucide/layout-grid.svg +1 -0
  88. package/icons/lucide/map-pin.svg +1 -0
  89. package/icons/lucide/map.svg +1 -0
  90. package/icons/lucide/palette.svg +1 -0
  91. package/icons/lucide/table.svg +1 -0
  92. package/icons/lucide/triangle-alert.svg +1 -0
  93. package/icons/lucide/type.svg +1 -0
  94. package/icons/lucide/wrench.svg +1 -0
  95. package/layouts/bleed.vue +317 -0
  96. package/layouts/cover.vue +1514 -0
  97. package/layouts/default.vue +187 -0
  98. package/layouts/divider.vue +286 -0
  99. package/layouts/end.vue +384 -0
  100. package/layouts/quote.vue +218 -0
  101. package/layouts/split.vue +1875 -0
  102. package/layouts/stack.vue +944 -0
  103. package/layouts/statement.vue +67 -0
  104. package/manifest/chrome.ts +36 -0
  105. package/manifest/components.ts +49 -0
  106. package/manifest/index.ts +57 -0
  107. package/manifest/layouts.ts +37 -0
  108. package/manifest/manifest.json +6514 -0
  109. package/manifest/presets.ts +635 -0
  110. package/notch.js +458 -0
  111. package/notchPath.js +188 -0
  112. package/package.json +77 -0
  113. package/section.js +110 -0
  114. package/setup/mermaid-renderer.ts +2120 -0
  115. package/setup/mermaid.ts +30 -0
  116. package/setup/shiki.ts +44 -0
  117. package/setup/shortcuts.ts +28 -0
  118. package/slide-bottom.vue +305 -0
  119. package/slide-top.vue +268 -0
  120. package/styles/base.css +2611 -0
  121. package/styles/index.ts +71 -0
  122. package/styles/tokens.css +144 -0
  123. package/uno.config.ts +231 -0
  124. package/useNotchCuts.js +200 -0
@@ -0,0 +1,426 @@
1
+ <script setup>
2
+ /*
3
+ The ceremony every deck chart shares: the room-scale Unovis type ramp, the legend, and the
4
+ empty state. The MARK never comes through here — each chart component mounts its own
5
+ @unovis/vue mark in the default slot, exactly the way packages/charts' FChartFrame keeps mark
6
+ props out of the frame. Wrap ceremony, never the chart.
7
+
8
+ ★ THE TYPE RAMP IS THE POINT. Unovis ships 12px axis tick labels and a 9px legend bullet, which
9
+ are correct on a dashboard and invisible from the back of a room. The `--vis-*` block below
10
+ re-points every one of them at the deck type roles (--type-deck-*), so a chart on a 1920x1080
11
+ canvas carries 36px axis labels — the same tier Bars.vue already sets its category labels at, and
12
+ unconditionally WCAG large text, so the 3:1 floor is the one that governs the muted ink.
13
+
14
+ ★★ DECLARED ON `.deck-chart`, NOT ON `:root`, and both halves of that matter.
15
+
16
+ 1. Unovis injects its OWN `:root` defaults at RUNTIME, after this sheet has loaded, so a `:root`
17
+ block here loses on order (packages/tokens/dataviz/unovis.css fights the same battle with a
18
+ `:root:root` specificity bump). A declaration on the chart's own element sidesteps the
19
+ contest entirely: custom properties inherit, and a value declared ON the element always beats
20
+ one inherited from an ancestor, whatever either selector's specificity.
21
+ 2. A custom property computes ONCE where it is declared. Declared at `:root`, every
22
+ `var(--deck-fg-muted)` below would freeze at the LIGHT value and a `class: deck-dark` slide
23
+ would draw light-mode axis ink on a dark ground — the same island trap deck.css's own derived
24
+ surfaces carry a three-selector fix for. Declared here, the reference resolves at a node that
25
+ is INSIDE the `.deck-dark` subtree, so it flips with the slide and no JS runs at all.
26
+
27
+ Colours reach the marks as `var()` REFERENCES rather than resolved values for the same reason:
28
+ CSS re-resolves them continuously, so a theme flip repaints with no remount, no repaint key and no
29
+ cached-palette bug to work around.
30
+
31
+ EVERY CHART CONTAINER SETS `:duration="0"` — the chart is finished the moment its slide is.
32
+ Slidev pre-renders neighbouring slides, so an entry animation runs while nobody is looking and is
33
+ already over (or worse, caught mid-flight) by the time the slide is on screen; and the deck's
34
+ reduced-motion floor suppresses it for part of the audience anyway, so the animated version is
35
+ never the one everyone sees. A chart that draws itself in is a dashboard idea.
36
+
37
+ ★ THE GRID IS DELIBERATELY FINER THAN THE LABELS, and it is the library's decision, not a defect
38
+ to chase. @unovis/ts's Axis builds its grid from `this._getNumTicks() * 2` (components/axis/
39
+ index.js, `_buildGrid`), so four labelled ticks paint seven or eight grid lines — minor rules
40
+ between the labelled ones. Read back from the live DOM, the axis holds two sets of `.tick` nodes:
41
+ four carrying the labels with their tick MARK suppressed, and a second set with empty text whose
42
+ lines span the plot. The only way to make the two agree is to pass explicit `tickValues` (the same
43
+ `_buildGrid` prefers them over its doubled count when they are set), which means computing the
44
+ domain and a nice-number ladder here instead of letting the library scale itself. Not worth it:
45
+ the minor rules are pale (--deck-border) and give the eye a halfway reference. Recorded so the
46
+ next reader does not spend an afternoon hunting a bug that is not there.
47
+ */
48
+ defineProps({
49
+ /** Resolved series for the legend — `[{ key, name, color }]`, as `chartData.ts` returns them. */
50
+ series: { type: Array, default: () => [] },
51
+ /**
52
+ * Plot height in px. The deck default fills a content column under a heading; a chart that owns a
53
+ * whole slide wants more.
54
+ *
55
+ * ★ 0 MEANS FILL — take whatever the parent gives, rather than a number the slide had to guess.
56
+ * The same convention DeckMap's own `height` already uses, and it is the library's behaviour
57
+ * rather than an invention: Unovis' container resolves `config.height || containerHeight`, so a
58
+ * falsy height makes it MEASURE its box, and it keeps a ResizeObserver on that box to re-fit.
59
+ *
60
+ * ⚠ THE CELL HAS TO HAVE A HEIGHT TO GIVE. A percentage resolves against a definite ancestor, so
61
+ * a frame in an auto-sized grid row fills nothing — put it in a row that stretches.
62
+ */
63
+ height: { type: Number, default: 0 },
64
+ /** Draw the series legend. Off for a chart that labels its marks directly. */
65
+ legend: { type: Boolean, default: true },
66
+ /**
67
+ * Where the legend sits: one of `top-left`, `top-center`, `top-right`, `bottom-left`,
68
+ * `bottom-center`, `bottom-right`.
69
+ *
70
+ * Two axes in one name, because they are two different decisions. The EDGE is about reading
71
+ * order — a legend above is read before the marks and belongs to a chart whose colours need
72
+ * introducing; one below is a caption, read after the picture has been looked at. The ALIGNMENT
73
+ * is about the column the chart sits in: centred under a plot that fills the frame, and pulled to
74
+ * an edge when the chart shares a row with copy and a centred key would float in the gutter.
75
+ *
76
+ * `bottom-center` is the default because a caption is the common case.
77
+ *
78
+ * This replaced a two-value `top`/`bottom` enum in which the alignment was BAKED IN — `top` was
79
+ * always start-aligned and `bottom` always centred — so half of the six were unreachable and the
80
+ * two that existed could not be told apart from their own names.
81
+ */
82
+ legendPosition: {
83
+ type: String,
84
+ default: 'bottom-center',
85
+ validator: (p) => ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'].includes(p),
86
+ },
87
+ /** When set, the frame draws this sentence in place of the chart — the honest empty state. Null renders the mark. */
88
+ message: { type: String, default: null },
89
+ });
90
+ </script>
91
+
92
+ <template>
93
+ <div class="deck-chart" :class="{ 'is-fill': !height }">
94
+ <!-- The empty state is a real WELL, not a bare line of text: a chart that has nothing to say
95
+ should still occupy the space the chart would have, so a slide does not reflow around
96
+ missing data and an audience can see that something was meant to be there. -->
97
+ <div v-if="message" class="deck-chart-empty" :style="height ? { height: `${height}px` } : undefined">
98
+ {{ message }}
99
+ </div>
100
+ <template v-else>
101
+ <div class="deck-chart-plot">
102
+ <slot :height="height" />
103
+ </div>
104
+ <!-- The legend sits AFTER the plot in the DOM and is lifted by `order` when it belongs above.
105
+ Source order is the caption case, which is the default, so the common reading order is
106
+ the one that needs no reordering at all.
107
+
108
+ Two classes come out of one prop name: the edge decides source order, the alignment
109
+ decides the row. Split rather than six classes so the CSS below stays two small tables. -->
110
+ <div
111
+ v-if="legend && series.length"
112
+ class="deck-chart-legend"
113
+ :class="[
114
+ legendPosition.startsWith('top') ? 'is-top' : 'is-bottom',
115
+ `is-${legendPosition.slice(legendPosition.indexOf('-') + 1)}`,
116
+ ]"
117
+ >
118
+ <!-- Hand-drawn rather than <VisBulletLegend>: the library's legend is a flex row of spans
119
+ sized off --vis-legend-*, which works, but a deck legend also has to survive being read
120
+ at distance and wrap cleanly inside a half-slide column. Owning the markup is what lets
121
+ the bullet be a real disc at deck scale and the label take the deck's own body tier. -->
122
+ <span v-for="s in series" :key="s.key" class="deck-chart-key">
123
+ <span class="deck-chart-bullet" :style="{ background: s.color }" />
124
+ {{ s.name }}
125
+ </span>
126
+ </div>
127
+ </template>
128
+ </div>
129
+ </template>
130
+
131
+ <style scoped>
132
+ .deck-chart {
133
+ display: flex;
134
+ flex-direction: column;
135
+ gap: var(--deck-space-3);
136
+ width: 100%;
137
+
138
+ /* ── The Unovis adapter, at room scale ──────────────────────────────────────────────────── */
139
+ --vis-font-family: var(--font-sans);
140
+ --vis-axis-font-family: var(--font-sans);
141
+ --vis-legend-font-family: var(--font-sans);
142
+
143
+ /* Series colours. SIX: --deck-series-1…6 alias the published categorical palette
144
+ in the product's order, and the palette's ceiling is six. Leaving --vis-color5 and up
145
+ undefined is deliberate — a chart that somehow reaches for a sixth gets Unovis' own off-brand
146
+ default rather than a Forest colour that was never designed, and chartData.ts's
147
+ `over-capacity` state stops it before it renders at all. */
148
+ --vis-color0: var(--deck-series-1);
149
+ --vis-color1: var(--deck-series-2);
150
+ --vis-color2: var(--deck-series-3);
151
+ --vis-color3: var(--deck-series-4);
152
+ --vis-color4: var(--deck-series-5);
153
+ --vis-color5: var(--deck-series-6);
154
+
155
+ /* Axis ink and rules. The grid is the deck's own border role, so it reads as structure rather
156
+ than as data — one step of ink on the page, no more. */
157
+ --vis-axis-grid-color: var(--deck-border);
158
+ --vis-axis-tick-color: var(--deck-border);
159
+ --vis-axis-domain-color: var(--deck-border-strong);
160
+ --vis-axis-grid-line-width: 2px;
161
+ --vis-axis-tick-line-width: 2px;
162
+ --vis-axis-domain-line-width: 2px;
163
+
164
+ /* Tick labels take the small body tier (36px) — the same tier Bars.vue's category labels use,
165
+ and the smallest thing on this deck that is still a sentence rather than a caption. */
166
+ --vis-axis-tick-label-color: var(--deck-fg-muted);
167
+ --vis-axis-tick-label-font-size: var(--type-deck-body-sm-size);
168
+ --vis-axis-tick-label-weight: var(--type-deck-body-sm-weight);
169
+ /* An axis TITLE is a label role: same size, bold — the two-weight system has nothing between. */
170
+ --vis-axis-label-color: var(--deck-fg);
171
+ --vis-axis-label-font-size: var(--type-deck-label-size);
172
+ --vis-axis-label-weight: var(--type-deck-label-weight);
173
+
174
+ /* The donut's centre. Title tier for the figure, small body for what it counts. */
175
+ --vis-donut-central-label-font-size: var(--type-deck-title-size);
176
+ --vis-donut-central-label-font-weight: var(--type-deck-title-weight);
177
+ --vis-donut-central-label-font-family: var(--font-sans);
178
+ --vis-donut-central-label-text-color: var(--deck-fg);
179
+ --vis-donut-central-sub-label-font-size: var(--type-deck-body-sm-size);
180
+ --vis-donut-central-sub-label-font-weight: var(--type-deck-body-sm-weight);
181
+ --vis-donut-central-sub-label-font-family: var(--font-sans);
182
+ --vis-donut-central-sub-label-text-color: var(--deck-fg-muted);
183
+
184
+ /*
185
+ ★★ THESE WERE INERT. Every declaration below sat inside the `:deep([class*='-central-label'])`
186
+ rule further down, so they were declared on the central LABEL — a text node that is a sibling of
187
+ the marks, not an ancestor of them. Custom properties inherit downwards only, so the arcs, bars,
188
+ scatter points and areas never saw one of them and drew Unovis' own defaults for the whole life
189
+ of this file, while the comments beside them described a treatment nobody was getting.
190
+
191
+ READ BACK FROM THE LIVE DOM before the move, on the specimen's donut slide: the frame reported
192
+ an EMPTY `--vis-donut-segment-stroke-color`, the label reported the cream, and the arc itself
193
+ was stroked `oklab(0.782…)` at 2px — the library default, not `--deck-bg` at 4px. Scatter and
194
+ area read 1px and 0px against the 3px and 4px written here.
195
+
196
+ Nothing about the values changed in moving them; only where they are declared.
197
+ */
198
+
199
+ /*
200
+ ── THE CROSSHAIR ─────────────────────────────────────────────────────────────────────────────
201
+
202
+ The circle sits ON the series it reports, so it takes that series' colour as its fill and the
203
+ chart's own ground as its ring — the same treatment the donut segment gets, and the reason it
204
+ reads as a mark lifted off the line rather than a bead threaded onto it. Unovis rings it at 1px,
205
+ which at this size is a hairline around a 9px dot.
206
+
207
+ The line itself is a guide, not a mark: it takes the border's weight, not a series colour.
208
+ */
209
+ --vis-crosshair-circle-stroke-color: var(--deck-bg);
210
+ --vis-crosshair-circle-stroke-width: 4;
211
+ --vis-crosshair-line-stroke-color: var(--deck-border-strong);
212
+ --vis-crosshair-line-stroke-width: 2;
213
+
214
+ /*
215
+ ── THE TOOLTIP ───────────────────────────────────────────────────────────────────────────────
216
+
217
+ ⚠ ITS VARIABLES ARE NOT HERE, and cannot be. The panel is mounted on `document.body`, so nothing
218
+ declared on this element reaches it — custom properties inherit downwards. They live on `:root`
219
+ in base.css, beside the note explaining why the body is the only container that can position a
220
+ tooltip correctly on a slide the deck scales.
221
+ */
222
+
223
+ /* The track behind the arcs. Our slices always sum to the whole, so a visible track can only
224
+ ever surface in the gaps — where any opaque colour is wrong the moment the slide is dark. */
225
+ --vis-donut-background-color: transparent;
226
+ /* The arc separator is the SLIDE GROUND, so segments read as separated rather than outlined. */
227
+ --vis-donut-segment-stroke-color: var(--deck-bg);
228
+ --vis-donut-segment-stroke-width: 4px;
229
+
230
+ /* Marks. The stroke that lifts a bar off its neighbour is the ground, same reasoning as the
231
+ donut's; the scatter carries one too so overlapping points stay countable. */
232
+ --vis-stacked-bar-stroke-color: var(--deck-bg);
233
+ --vis-stacked-bar-stroke-width: 3px;
234
+ --vis-grouped-bar-stroke-color: var(--deck-bg);
235
+ --vis-grouped-bar-stroke-width: 0px;
236
+ --vis-scatter-stroke-color: var(--deck-bg);
237
+ --vis-scatter-stroke-width: 3px;
238
+ --vis-scatter-fill-opacity: 0.85;
239
+ /* Unovis paints a scatter's point labels in its OWN default #5b5f6d, which nothing here ever
240
+ re-pointed — the block above sets the mark's stroke, width and fill, not its label. Measured
241
+ against the well's own edge (--deck-border resolves to #dad5cf) that is 4.36:1, under the 4.5
242
+ floor a 12px/500 label is held to; --deck-fg-muted takes it to 5.32:1 there and 7.42:1 on the
243
+ plot's own ground. */
244
+ --vis-scatter-point-label-text-color-dark: var(--deck-fg-muted);
245
+ --vis-area-stroke-width: 4px;
246
+ }
247
+
248
+ /*
249
+ ★ THE CENTRE BLOCK IS NOT CENTRED, and it cannot be from inside Unovis. The library places the
250
+ figure and its sub-label at dy -0.55em and +0.55em — of their OWN font sizes, which the deck
251
+ deliberately makes different (title 60px over body-sm 36px). Equal fractions of unequal sizes are
252
+ unequal offsets, so the pair hangs above the ring's true centre by half their difference.
253
+
254
+ MEASURED on apps/deck's donut slides, ring centre against the union of the two text
255
+ boxes: 16.8px high, or 0.7 of the 24px the two roles differ by. Written as that subtraction so the
256
+ correction follows the roles if either is retuned; the 0.7 is the empirical part, true at the one
257
+ size pair this deck ships. Residual after it: 1.9px, which is glyph-box noise rather than offset.
258
+
259
+ ⚠ MEASURE THIS WITH THE FONTS LOADED. The first reading said 12px and was taken mid-swap, on
260
+ fallback metrics — correcting to it left the block visibly high and the second measurement
261
+ disagreed with the first by 40%.
262
+
263
+ Both lines carry a `-central-label` suffix (the prefix is an emotion hash and changes per build),
264
+ so the selector matches on the suffix and moves the pair together — correcting one would re-open
265
+ the gap between them.
266
+ */
267
+ /*
268
+ ★★ THE CHART DOCTRINE HAD A HOLE IN IT, AND THE HOLE WAS A CSS TRANSITION. Every container in this
269
+ theme sets :duration="0" and the comment at the top of this file says why — "the chart is finished
270
+ the moment its slide is". That prop governs Unovis' own data animation. It does not reach the
271
+ opacity transition Unovis puts on axis tick labels, so charts animated anyway, in the one way
272
+ nobody had thought to switch off.
273
+
274
+ What it looked like, and why it read as a bug rather than a flourish: Slidev mounts every slide, so
275
+ a chart is laid out long before it is shown — at 0x0. With no width every x tick collapses onto the
276
+ same point, Unovis' overlap-hider does exactly its job and keeps the FIRST label while hiding the
277
+ rest at opacity 0. Arrive on the slide, the container finally has width, the hider recomputes, and
278
+ the five it had hidden fade back in over ~436ms while the first one — never hidden, so nothing to
279
+ animate — is simply there. One label pops, the others drift in behind it. Measured on slide 82
280
+ arriving from 81: Camden opacity 1 at frame zero, the other five 0 -> 1 at 436ms.
281
+
282
+ A full page load hides this completely: the container has width from the start, every label begins
283
+ hidden, and they all fade together. It only appears when moving slide to slide, which is the only
284
+ way a deck is ever actually shown.
285
+
286
+ The transition goes, not the opacity — a label that genuinely overlaps at real width should still
287
+ be hidden, because that is the hider doing useful work. Only the drift between the two states is
288
+ removed, which is what :duration="0" was always meant to say.
289
+
290
+ Substring-matched on the emotion class for the reason the central-label rule below is: the hash in
291
+ css-6qjh0y-tick-label-hideable is generated and must never be written down.
292
+ */
293
+ :deep([class*='-tick-label']) {
294
+ transition: none;
295
+ }
296
+
297
+ :deep([class*='-central-label']) {
298
+ /* ★ `translate`, NOT `transform`. Unovis positions these texts with a transform ATTRIBUTE, and a
299
+ CSS `transform` property replaces it outright rather than composing — the label left the ring
300
+ entirely and landed at the plot's origin. `translate` is its own property and composes after. */
301
+ translate: 0 calc((var(--type-deck-title-size) - var(--type-deck-body-sm-size)) * 0.7);
302
+ }
303
+
304
+ .deck-chart-plot {
305
+ width: 100%;
306
+ }
307
+
308
+ /*
309
+ ★★ FILL MODE, AND IT IS THREE RULES BECAUSE THE HEIGHT HAS TO BE HANDED DOWN A CHAIN. The frame
310
+ takes the cell, the plot takes what the frame has left once the legend is laid out, and the
311
+ container Unovis renders takes the plot — a percentage stops at the first ancestor with no
312
+ definite height, so skipping any one of the three leaves the chart measuring zero and falling back
313
+ to the library's 150px default.
314
+
315
+ `min-height: 0` on the plot because it is a flex item: without it a flex child refuses to shrink
316
+ below its content, and a chart that overflows its cell pushes the legend off the frame instead of
317
+ giving up its own space.
318
+ */
319
+ .deck-chart.is-fill {
320
+ height: 100%;
321
+ }
322
+ .deck-chart.is-fill .deck-chart-plot {
323
+ flex: 1;
324
+ min-height: 0;
325
+ }
326
+ .deck-chart.is-fill .deck-chart-plot > :deep(*),
327
+ .deck-chart.is-fill .deck-chart-empty {
328
+ height: 100%;
329
+ }
330
+ /*
331
+ ★★ AND ONE STEP DEEPER FOR THE TWO MARKS THAT WRAP THEIR CONTAINER. Unovis reads
332
+ `config.height || containerHeight`, and containerHeight is the clientHeight of the element IT was
333
+ handed — so the chain has to reach that element, not merely the plot's first child. Ten of the
334
+ twelve charts put the container there directly; ChordChart and TimelineChart each wrap theirs in a
335
+ div of their own, so the wrapper took the 100% and the container inside it measured zero and fell
336
+ back to the library's 300px default: measured on the chord at a 559px plot, svg height="300",
337
+ unmoved by a window resize because nothing about its own box had changed.
338
+
339
+ Named rather than matched with a universal descendant: `:deep(*)` would also reach inside the SVG,
340
+ and a 100% height on a <g> is not nothing.
341
+ */
342
+ .deck-chart.is-fill .deck-chart-plot > :deep(.deck-chord),
343
+ .deck-chart.is-fill .deck-chart-plot > :deep(.deck-timeline),
344
+ .deck-chart.is-fill .deck-chart-plot > :deep(.deck-chord) > *,
345
+ .deck-chart.is-fill .deck-chart-plot > :deep(.deck-timeline) > * {
346
+ height: 100%;
347
+ }
348
+
349
+ .deck-chart-legend {
350
+ display: flex;
351
+ flex-wrap: wrap;
352
+ /* ROW gap space-1, column gap unchanged. Only the wrapped case is affected — a legend that fits on
353
+ one line never uses a row gap at all — and that is exactly the case that overflows: on
354
+ deck-template slide 40 four keys wrap 3 + 1, putting the second row's baseline 4px past the 96px
355
+ text inset. 8px buys twice the overrun.
356
+
357
+ ★ NOT `.deck-chart`'s own gap, which is the obvious one-line fix and the wrong one. That gap is
358
+ the plot-to-legend rhythm LondonMap deliberately mirrors ("a map key and a chart key read as
359
+ the same furniture"), and a4fe8cd resized two maps rather than move it. Trimming it here would
360
+ undo that from the other side. This is the legend's own internal wrap spacing — a different
361
+ measurement that no other component copies. */
362
+ gap: var(--deck-space-1) var(--deck-space-5);
363
+ font-size: var(--type-deck-body-sm-size);
364
+ line-height: var(--type-deck-body-sm-line-height);
365
+ color: var(--deck-fg);
366
+ }
367
+ /* The EDGE. Source order is the caption case, so only `top` reorders — the legend sits after the
368
+ plot in the DOM, which is the reading order a screen reader gets either way. */
369
+ .deck-chart-legend.is-top {
370
+ order: -1;
371
+ }
372
+ /* The ALIGNMENT, as its own table. These used to be welded to the edge — `top` implied
373
+ start-aligned and `bottom` implied centred — which made four of the six arrangements
374
+ unreachable and the other two impossible to name honestly. */
375
+ .deck-chart-legend.is-left {
376
+ justify-content: flex-start;
377
+ }
378
+ .deck-chart-legend.is-center {
379
+ justify-content: center;
380
+ }
381
+ .deck-chart-legend.is-right {
382
+ justify-content: flex-end;
383
+ }
384
+ .deck-chart-key {
385
+ display: inline-flex;
386
+ align-items: center;
387
+ gap: var(--deck-space-2);
388
+ white-space: nowrap;
389
+ }
390
+ .deck-chart-bullet {
391
+ /* A real disc at reading distance. The library's 9px bullet is a speck beside 36px type. */
392
+ width: 0.6em;
393
+ height: 0.6em;
394
+ border-radius: var(--deck-radius-full);
395
+ flex: none;
396
+ }
397
+
398
+ .deck-chart-empty {
399
+ display: flex;
400
+ align-items: center;
401
+ justify-content: center;
402
+ text-align: center;
403
+ padding: var(--deck-space-5);
404
+ background: var(--deck-surface);
405
+ border-radius: var(--deck-radius-nested);
406
+ color: var(--deck-fg-muted);
407
+ font-size: var(--type-deck-body-size);
408
+ line-height: var(--type-deck-body-line-height);
409
+ }
410
+
411
+ /*
412
+ ★ THE TOOLTIP'S SWATCH IS THE ONE THING IN THE HOUSE TEMPLATE THAT IS NOT A CUSTOM PROPERTY.
413
+ Every size, colour and padding in `forestTipFrame` is themeable — the dot's 8px width and height
414
+ are written inline, and only its border-radius is a variable. Eight pixels beside 36px type is a
415
+ speck, so it is matched to the crosshair's own circle instead.
416
+
417
+ Selected by the `aspect-ratio` in that inline style, which is the stable half of the contract: the
418
+ swatch is the only element the template gives one to. `!important` is required because the value
419
+ it overrides is inline, which no stylesheet rule outranks by specificity or source order.
420
+ */
421
+ :deep([class*='tooltip'] span[style*='aspect-ratio']) {
422
+ width: 18px !important;
423
+ height: 18px !important;
424
+ border-radius: 9px !important;
425
+ }
426
+ </style>