@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,1514 @@
1
+ <script setup>
2
+ /*
3
+ The opener. Light ground, one Mohr shout at the top of the ramp, the full lockup signing it —
4
+ on a slide the running chrome is not already signing. See `signs` below.
5
+
6
+ `eyebrow` and `meta` are the editorial frame around the title: the kicker that says what KIND
7
+ of thing this is, and the quiet line that says who and when. Both optional, both props rather
8
+ than markup so the frontmatter reads as a title block.
9
+ */
10
+ import { Comment, Fragment, Text, computed, inject, ref, useSlots } from 'vue';
11
+ import { useNotchCuts } from '../useNotchCuts.js';
12
+ import { injectionCurrentPage, injectionFrontmatter } from '@slidev/client/constants.ts';
13
+ import { chromeShows, coBrand, BEAT_GROUND, BEAT_GROUNDS, COVER_PANEL_SHAPED, COVER_PHOTO_GROUND } from '../notch.js';
14
+ import deckTokens from '@humanforest/tokens/deck/deck.json';
15
+ import { MARK_SIZE, MARK_CORNER, MARK_RING } from '@humanforest/tokens/logo';
16
+ import { assetUrl } from '../assetUrl.js';
17
+
18
+ const props = defineProps({
19
+ /** The kicker over the title — "Q3 review", "Design system", "Board pack". */
20
+ eyebrow: { type: String, default: '' },
21
+ /** The quiet line under it — presenter, team, date. */
22
+ meta: { type: String, default: '' },
23
+ /**
24
+ * A single pill under the title, for the line that is a CLAIM rather than a subtitle — a
25
+ * positioning line, a strapline, the thing a cover says about the product. Empty by default and
26
+ * absent when empty: a cover that does not make a claim draws no pill.
27
+ *
28
+ * ★ IT IS A `BadgeStrip` OF ONE, not a pill this layout draws for itself. The deck already has a
29
+ * badge — its ground comes from the named-pair table, its label is the overline, its cap and
30
+ * ellipsis are the ones BadgeStrip documents at length — and a second implementation here would
31
+ * be a pill that drifts from every other pill in the deck. One string rather than the strip's
32
+ * array because a cover makes one claim; a row of them is a content slide.
33
+ */
34
+ badge: { type: String, default: '' },
35
+ /** Horizontal position of the title block: ragged left (default), or centred on the slide. */
36
+ align: { type: String, default: 'start', validator: (a) => ['start', 'center'].includes(a) },
37
+ /**
38
+ * Draws the opener on the DARK register — the deck's dark ground with light ink — the same
39
+ * `deck-dark` class `default`, `split` and `stack` take. Composes with `background`, pinning a
40
+ * named pair's dark half exactly as it does on a panel.
41
+ *
42
+ * ★ THREE STATES, NOT TWO. Unset is not `false`: it means the author has said nothing about the
43
+ * ground, which is what lets the layout supply its own — see BEAT_GROUND. `false` is a
44
+ * decision (light, whatever the variant would have chosen) and pins it.
45
+ */
46
+ dark: { type: Boolean, default: null },
47
+ /**
48
+ * Which ground the opener sits on: `auto` — the default — is the brand's own, the vivid pair's
49
+ * dark half, whatever the variant; `paper` puts it on the deck's page ground instead. `dark`
50
+ * composes with both.
51
+ *
52
+ * ★★ TWO VALUES, NOT SIX, AND THAT IS THE POINT OF A COVER. The four named pairs — warm, accent,
53
+ * vivid, neutral — are for PANELS: a plane on a page, one of a set, read against the slide around
54
+ * it. A cover has no slide around it. It is the one place a deck states its identity, and an
55
+ * identity that arrives on a different colour each time is not one; the brand opens on its own
56
+ * green or on the page, and the variant is what chooses between them.
57
+ *
58
+ * ⚠ THE PAIRS ARE STILL REACHED, JUST NOT NAMED HERE. `auto` resolves to the vivid pair's dark
59
+ * half — see BEAT_GROUND — so a cover does land on a named ground; it does not get to pick which
60
+ * one. `default`, `split`, `stack` and `Card` all still take the full table.
61
+ *
62
+ * ★★ THE VARIANTS OF A COVER ARE ITS GROUND AND ITS REGISTER, not a private enum. An opener is
63
+ * one composition — kicker, rule, shout, credit — and what makes one deck's opener differ from
64
+ * another's is the colour it opens on. Spelling that as `variant: 'bold' | 'quiet' | …` would put
65
+ * a second, cover-only vocabulary next to the one every other ground-bearing layout already uses,
66
+ * and the two would drift the first time a background was added. base.css keys the ink, the
67
+ * eyebrow, the paragraph tiers and the bullet on the `bg-*` classes GENERICALLY, so a cover
68
+ * carrying them is correct on every ground without a rule of its own.
69
+ */
70
+ background: {
71
+ type: String,
72
+ default: 'auto',
73
+ /* The constant, not a restated pair: notch.js owns this list because the chrome reads it too,
74
+ and `end.background` validates against the same export. */
75
+ validator: (b) => BEAT_GROUNDS.includes(b),
76
+ },
77
+ /**
78
+ * WHAT SHAPE the opener is, as opposed to what colour. Six compositions, and every one of them
79
+ * takes every `background` and both registers — the two props are orthogonal by construction,
80
+ * which is the whole reason the ground is not baked into the variant names.
81
+ *
82
+ * · `plain` — type alone on the ground. The opener with nothing to look at but the words.
83
+ * · `inset` — the title in a panel floating on the ground, air all round it.
84
+ * · `mark` — the Forest mark oversized as the composition, the title over it.
85
+ * · `poster` — a photograph edge to edge, the identity stacked over a scrim at the right.
86
+ * · `mosaic` — the title on a field, beside a staggered grid of pictures bleeding off the edge.
87
+ *
88
+ */
89
+ variant: {
90
+ type: String,
91
+ default: 'plain',
92
+ validator: (v) => ['plain', 'inset', 'poster', 'mosaic'].includes(v),
93
+ },
94
+ /**
95
+ * Whether the opener carries Forest's certifications in its corner. On by default; `false` is how
96
+ * a deck that is not making those claims says so.
97
+ *
98
+ * ★ PLURAL, because the corner is a SET even while it holds one mark. apps/docs calls that page
99
+ * endorsements, `Endorsement` keeps its own `mark` list open for Verra, and a deck turning the
100
+ * corner off is turning off whatever it would have held rather than one named certification.
101
+ *
102
+ * ★ A BOOLEAN, LIKE `chrome`, AND IT WAS AN ENUM FOR ONE COMMIT. The argument for
103
+ * `'b-corp' | 'none'` was that `Endorsement.vue` keeps its own `mark` list open for Verra, so
104
+ * naming the mark now would save a rename later — which is a guess about a mark whose artwork is
105
+ * still pending approval, and might never belong on a cover at all. This theme's lever for "draw
106
+ * this piece of furniture or do not" is a boolean everywhere else (`chrome`, `notch`, `bleed`),
107
+ * and a cover suppressing its certification is that, not a choice between certifications. If one
108
+ * ever has to be chosen here, widening this is a smaller cost than carrying the guess.
109
+ *
110
+ * ⚠ ONLY `mosaic` DRAWS ONE, so this is a lever on that variant rather than on the layout — the
111
+ * same shape `image`/`alt` (inset) and `images` (mosaic) already have. It suppresses
112
+ * an endorsement; it does not add one to a composition that has no place for it.
113
+ */
114
+ endorsements: { type: Boolean, default: true },
115
+ /**
116
+ * The picture, for `inset` and `poster`. Slidev's own key, so `extractImagesUsage()`
117
+ * preloads it. On `inset` it is also the switch: with one, the panel splits into a pair.
118
+ */
119
+ image: { type: String, default: '' },
120
+ /** Alt text for that picture — and the label the fallback `<Placeholder>` carries when there is none. */
121
+ alt: { type: String, default: '' },
122
+ /**
123
+ * The pictures for `mosaic`, as URLs. A LIST rather than one `image`, because the composition is
124
+ * the plurality — a single photograph in a mosaic is an inset with extra steps.
125
+ *
126
+ * ★ Nine is the drawn ceiling and the CSS says so rather than the caller: the wedge is four tiles,
127
+ * then three, two and one, and one of those ten cells is the mark. A tenth picture would need a
128
+ * fifth row, and a five-wide top row reaches back across the wordmark.
129
+ */
130
+ images: { type: Array, default: () => [] },
131
+ })
132
+
133
+ /*
134
+ ★ THE CLASS IS LOOKED UP, NOT INTERPOLATED. A validator is stripped in production, so an invented
135
+ `variant` would mint an unstyled `is-<whatever>` class and render an unstyled slide rather than
136
+ falling back — the same trap bleed.vue's SCRIM_CLASS records for its own enum.
137
+ */
138
+ /*
139
+ ★★ `background` PAINTS THE SLIDE ON A FIELD COMPOSITION AND THE PANEL ON A PANEL-SHAPED ONE, which
140
+ is not two meanings for one prop — it is the one meaning the rest of the theme already uses. On
141
+ `default` the background takes the whole slide; on `split` and `stack` it takes the panel and the
142
+ band, and the slide keeps its paper. Which of those a cover is depends on its variant, so the prop
143
+ follows the composition rather than the layout.
144
+
145
+ `inset` is the panel-shaped one: it draws a plane ON the slide, and a plane is what the named
146
+ pairs are for. The rest are fields, and take the ground.
147
+
148
+ ★★ AND `dark` FOLLOWS `background` ONTO THE PANEL, which it did not at first. It landed on the ROOT
149
+ either way, so an inset on the default ground came out as a dark SLIDE carrying a vivid panel —
150
+ the cream page gone, on the one variant whose whole composition is a plane floating on that page.
151
+ Both props describe the same thing, so both land in the same place: on a field the slide, on a
152
+ panel the panel. The page keeps its paper.
153
+
154
+ ★ IT WAS TWO. `band` — the title in a full-width band across the foot of the slide — was drawn and
155
+ removed: `stack` already makes that composition, with a figure above the band rather than bare
156
+ ground, and a cover is the one slide in a deck that should not look like a content layout with the
157
+ content missing. An array of one is kept rather than collapsed to an equality, because the next
158
+ panel-shaped variant joins it here rather than rewriting the predicate.
159
+ */
160
+ /* The list lives in notch.js: the chrome has to answer the same question — a panel-shaped cover's
161
+ PAGE is cream however its panel is painted — and cannot read this file. */
162
+ const PANEL_SHAPED = COVER_PANEL_SHAPED;
163
+
164
+ /*
165
+ ── THE VARIANT MAY CARRY A GROUND, AND ONLY WHEN THE AUTHOR NAMED NEITHER ────────────────────
166
+
167
+ ★★ THIS IS A DEFAULT, NOT A COUPLING, and the note above is still true: `background` and `dark`
168
+ remain orthogonal to `variant` — every composition still takes all ten grounds, and a slide that
169
+ names one gets it. What this table decides is what a slide gets when it names NOTHING, which
170
+ previously could only be the deck's paper for all seven.
171
+
172
+ The mosaic is why. It is the one composition whose ink is a field of photographs, and the brand
173
+ draws that field on dark green; on cream it reads as a contact sheet pinned to a wall. A variant
174
+ whose commonest correct ground is not the paper had no way to say so.
175
+
176
+ ⚠ THE TRIGGER IS "THE AUTHOR SAID NOTHING", not "the value equals the old default". `background`
177
+ gained an `auto` member and `dark` a null third state for exactly this: `background: 'paper'` is a
178
+ DECISION to open on paper and must beat the table, and it could not if unset and `paper` were the
179
+ same value. That is also why the two are read together — `dark: true` alone on a mosaic means the
180
+ deck's own dark register, not the vivid pair.
181
+ */
182
+ /* The table lives in notch.js — the chrome has to answer the same question and cannot read the
183
+ classes this resolution lands as. See darkRegister() there. */
184
+ /*
185
+ ★★ THE GROUND IS CLAMPED BEFORE IT IS USED, the same way VARIANT_CLASS clamps the composition, and
186
+ for the same reason stated one screen up: Vue strips `validator:` from production builds, so a name
187
+ that is not in the enum arrives here intact and is interpolated into a class.
188
+
189
+ MEASURED on this deck with `background: 'whatever'` written into slide 170's frontmatter by hand —
190
+ the root came back `is-mosaic bg-whatever deck-own-ground`. Two failures, and the second is the
191
+ one that hides: `bg-whatever` matches no rule so the slide paints the deck's paper and LOOKS like
192
+ `paper`, while `deck-own-ground` rides along and switches off the subtitle's muted tier and the
193
+ credit's tier, which are both scoped off it. A misspelt ground silently flattens the ink hierarchy
194
+ of a slide that otherwise looks correct.
195
+
196
+ It falls back to `auto` — the prop's own default — rather than to `paper`, so a mistyped ground
197
+ gives the composition the ground it would have chosen for itself instead of a bare page. Same
198
+ choice VARIANT_CLASS makes in falling back to `is-plain`.
199
+ */
200
+ // The enum, shared with notch.js so a misspelt ground clamps the same way in both places.
201
+ const stated = computed(() => (BEAT_GROUNDS.includes(props.background) ? props.background : 'auto'));
202
+
203
+ const ground = computed(() => {
204
+ /*
205
+ ★★ ONE VARIANT DOES NOT TAKE A GROUND AT ALL. A poster's ground is the PHOTOGRAPH, edge to edge,
206
+ with a black scrim over it — so `background` has nothing to paint and `dark` has nothing to
207
+ invert. Drawn once per named ground it gave four slides that were pixel-identical apart from
208
+ their captions, which is how this was caught. Light ink on a dark picture, fixed; notch.js's
209
+ darkRegister() returns the same answer for the same slide so the confidentiality marking does
210
+ not come back dark on a dark photograph.
211
+ */
212
+ if (COVER_PHOTO_GROUND.includes(props.variant)) return { background: 'paper', dark: true };
213
+ const unstated = stated.value === 'auto' && props.dark === null;
214
+ const g = unstated ? BEAT_GROUND : {};
215
+ return {
216
+ background: g.background ?? (stated.value === 'auto' ? 'paper' : stated.value),
217
+ dark: g.dark ?? (props.dark === true),
218
+ };
219
+ });
220
+
221
+ /*
222
+ ★★ WHICH VARIANTS SIGN FROM THE COLUMN RATHER THAN THE CORNER. A corner mark needs something in
223
+ the frame to sit against — a panel edge, a field of tiles. `plain` has nothing there: it is type
224
+ alone on the ground, and a lockup pushed to the far corner of an otherwise empty slide reads as a
225
+ stray. In the column it leads the title block, which is where a lockup goes on a page with nothing
226
+ else on it.
227
+
228
+ `mosaic` is absent because it signs with the wordmark as its title, and `inset` keeps the corner —
229
+ a panel edge is exactly what a corner mark is for.
230
+ */
231
+ const COLUMN_SIGNED = ['plain'];
232
+
233
+ const VARIANT_CLASS = {
234
+ plain: 'is-plain', inset: 'is-inset', poster: 'is-poster', mosaic: 'is-mosaic',
235
+ };
236
+
237
+ /*
238
+ ★★ SINCE 2026-09-06 THE CHROME IS OFF FOR EVERY COVER BY DEFAULT — `CHROMELESS_LAYOUTS` in
239
+ notch.js — so the collision this predicate was written to resolve can no longer happen on its own.
240
+ `signs` stays anyway, and is not dead code: `chrome: true` on a slide's own frontmatter is still a
241
+ supported override, and a cover that takes it must not draw the mark twice. What follows is the
242
+ measurement that put the predicate here.
243
+
244
+ ★ THE CORNER BELONGS TO THE CHROME, so the lockup only signs a slide the chrome is not already
245
+ signing. Same shape as split.vue's and stack.vue's notch, and the same shared predicate: a layout
246
+ that puts something in a chrome corner has to ask whether the chrome is there, because the chrome
247
+ is forbidden from asking which layout it is on (frontmatter.static.test.ts, "names no layout").
248
+
249
+ What it fixes, on a cover slide that is not page 1: the lockup sat at right/bottom
250
+ --deck-space-6 and the page marker at --deck-inset-chrome, and the two boxes overlapped by 14px
251
+ (measured, apps/deck-layouts slide 3: lockup right edge 932, page-dot left edge 918). It went
252
+ unseen because a forest-700 pill on cream reads as its own object and the eye forgives a few px
253
+ of overlap with a dark wordmark; a two-ring halo briefly shipped on the pills and made it
254
+ obvious, which is how it was found. The halo has since been reverted. The overlap never depended
255
+ on it — it is as old as the corner.
256
+
257
+ ⚠ THE OVERLAP IS THE SMALLER HALF OF IT. When the chrome shows it draws the Forest mark
258
+ top-right at the SAME 72px this lockup uses (slide-top.vue, tied to this file's `:size` by
259
+ frontmatter.static.test.ts), so such a slide carried the mark twice — once as the running
260
+ identity, once as a signature on an opener that, by definition, is not the opening. Moving the
261
+ lockup to another corner would have kept both marks and only relocated the argument; every corner
262
+ is a chrome corner (section pill top-left, mark top-right, confidentiality bottom-left, page
263
+ marker bottom-right).
264
+
265
+ NOT `chrome: false`, which is the other way this could have gone. That key hides the running
266
+ chrome wholesale — including the confidentiality watermark, which slide-bottom.vue deliberately
267
+ exempts from the page-1 gate precisely so a title slide can still carry its marking. Making
268
+ `chrome: false` the enforced rule for covers would have traded this collision for the loss of
269
+ that, on every cover in every deck.
270
+ */
271
+ const panelShaped = computed(() => PANEL_SHAPED.includes(props.variant));
272
+
273
+ const frontmatter = inject(injectionFrontmatter, {});
274
+ // injectionCurrentPage, not useNav().currentPage — the /print and /overview routes carry no `:no`
275
+ // param and useNav() reports 1 for every slide at once, which would restore the lockup on every
276
+ // page of an exported PDF. slide-top.vue records the measured failure.
277
+ const currentPage = inject(injectionCurrentPage, computed(() => 1));
278
+ const signs = computed(() => !chromeShows(frontmatter, currentPage.value));
279
+
280
+ /*
281
+ ── THE INSET PANEL CUTS ITSELF FOR THE LOCKUP ─────────────────────────────────────────────────
282
+
283
+ ★★ THE CUT IS `useNotchCuts`, NOT A SECOND IMPLEMENTATION OF IT. That composable measures RUNNING
284
+ CHROME, and a cover has none by construction — so the first attempt here measured the lockup by
285
+ hand, and reproduced both of the bugs its own source documents: `getBoundingClientRect()` returns
286
+ SCREEN pixels on a transform-scaled slide while `clip-path: path()` speaks CSS pixels, and
287
+ `getPropertyValue('--deck-notch-gap')` returns the literal text `calc(24px * 1)`, whose
288
+ `parseFloat` is NaN. The cut came out at the wrong scale with a zero ring.
289
+
290
+ The composable now takes extra occupant selectors instead. One implementation of the shape, two
291
+ kinds of thing that can sit in it.
292
+ */
293
+ /*
294
+ ── THE MOSAIC IS A BRAND COVER, NOT A TITLE COVER ─────────────────────────────────────────────
295
+
296
+ ★★ THE WORDMARK IS THE TITLE HERE. Every other variant opens with a typeset shout; this one opens
297
+ with the identity itself on a field of the product, which is the composition the brand's own
298
+ covers use. So the display line is the wordmark, the accent rule goes (the wordmark IS the
299
+ gesture), and the corner lockup goes with it — a slide signed by a 700px wordmark does not also
300
+ need a 72px one in the corner.
301
+
302
+ ⚠ ONE CELL OF THE STACK IS THE MARK, NOT A PHOTOGRAPH, so the identity reads twice — once as the
303
+ word, once as the glyph. The caller supplies NINE pictures for ten cells; the tenth is the
304
+ layout's.
305
+
306
+ ★ IT TAKES A CELL NOTHING CROPS, AND THE SECOND ROW'S MIDDLE ONE RATHER THAN ITS FIRST. The
307
+ wedge's top row runs off the top edge and its last column off the right, which leaves the second
308
+ row's two inner tiles. The middle of the three sits under the body of the wedge instead of on its
309
+ leading diagonal, so the glyph reads as part of the field rather than as a label pinned to its
310
+ corner — and it is further from the wordmark, which stops the two identities crowding each other. A cut photograph is the composition; a cut identity
311
+ is a mistake, which is what an earlier two-column arrangement produced.
312
+ */
313
+ /*
314
+ ── A CO-BRANDED COVER PUTS THE PARTNER IN THE GRID ────────────────────────────────────────────
315
+
316
+ ★★ `companion:` DREW NOTHING ON A COVER UNTIL NOW. The key is read by notch.js's coBrand() and
317
+ rendered by slide-top.vue beside the Forest mark in the running chrome — and a cover is in
318
+ CHROMELESS_LAYOUTS, so a co-branded opener silently had no co-brand on it. The mosaic has the one
319
+ place on a cover that is already a mark on a card, so that is where the partner goes.
320
+
321
+ ⚠ AND ONLY WHILE THE WORDMARK IS DRAWING. On a titled mosaic the mark tile is the ONLY Forest
322
+ identity on the slide — the wordmark has been replaced by the deck's own title — so swapping it
323
+ for a partner's would leave a co-branded cover carrying somebody else's mark and none of ours.
324
+ With the wordmark showing, "forest" is set in type at 813px and the tile is free to be the
325
+ partner's. Same rule the corner lockup follows through `signs`, pointed the other way: never draw
326
+ our mark twice, and never draw it zero times.
327
+
328
+ ★ `coBrand`, NOT THE RAW FRONTMATTER KEY, so the trimming and the null-when-absent contract are
329
+ the ones slide-top already relies on. The pixel budget it also returns belongs to the notch and is
330
+ unused here — a cover has no cut to spend against, and this tile is sized by the grid.
331
+
332
+ ★★ AND IT IS AN ORDINARY CELL, NOT A FRAMED ONE. The first cut gave it a pale plate and
333
+ `object-fit: contain` — reasoning that a partner's file might be dark ink, light ink or full
334
+ colour, so it needed a safe ground. Drawn, that was a tile inside a tile: the artwork these decks
335
+ co-brand with is ALREADY a mark on its own square, exactly as ours is, so a second frame around it
336
+ is the same mistake the Forest mark's own cell made before it. It fills the cell like a
337
+ photograph, from the same rule.
338
+
339
+ ⚠ WHICH MAKES THE ASSET A CONTRACT: `cover` crops, so `companion:` here wants a square-ish mark on
340
+ its own ground. A wide wordmark would lose its ends. The running chrome contains rather than crops
341
+ because it sizes the pair to a measured budget; this tile has a fixed square to fill.
342
+ */
343
+ const companion = computed(() => coBrand(frontmatter, deckTokens.chrome));
344
+ const companionLabel = computed(() => frontmatter?.companionName || 'Companion mark');
345
+
346
+ /*
347
+ ★★ A CO-BRANDED SPLIT INSET SIGNS TOP-RIGHT, WHERE THE RUNNING CHROME SIGNS. slide-top.vue puts
348
+ the mark and its companion in that corner on every slide that carries one; a cover suppresses the
349
+ chrome, so the pair is drawn here instead — same corner, same order, same shared height, so a
350
+ co-branded opener and the co-branded slides after it are the same object in the same place.
351
+
352
+ The bottom-right lockup gives way to it rather than joining it: a cover that signs twice is a
353
+ cover that signs once too often, and a pair seated in a notch cut for one mark would either
354
+ overflow the seat or shrink a partner's artwork to fit — which is not ours to shrink.
355
+
356
+ Only on the split inset. Everywhere else the corner is free and the lockup keeps it.
357
+ */
358
+ const insetPair = computed(() => insetSplit.value && !!companion.value);
359
+ /* The chrome's own mark size, so a cover's pair and the running chrome's are the same object at the
360
+ same proportions — coBrand computes its notch budget from this number. The GAP is no longer stated
361
+ here: MarkPair derives it from the size, which is what lets the poster's 120px pair keep the
362
+ chrome's proportions instead of holding a 72px gap between two larger tiles. */
363
+ const PAIR_MARK_PX = deckTokens.chrome.markPx;
364
+ /*
365
+ ★ THE POSTER'S OWN TWO SIZES, larger than the chrome's. A corner pair is furniture at 72px; this
366
+ one is the composition, read from the back of a room over a photograph. The wordmark leads it at
367
+ 697px — the mark is a tile beside a partner's, and the NAME is what a poster says.
368
+
369
+ Both are stated rather than derived because the relationship between them is the design: at the
370
+ chrome's 72 the tiles read as a caption above the wordmark, and at the wordmark's own scale they
371
+ compete with it.
372
+ */
373
+ const POSTER_MARK_PX = 160;
374
+ /*
375
+ ⚠ SIZED TO THE COLUMN, NOT CHOSEN. The wordmark is the widest thing in the stack and the stack is
376
+ a third of the frame: at 360 it drew 697px across a 576px column and hung over the photograph on
377
+ both sides. `lockup-vertical` and this both size from the MARK, so the number that fits is the
378
+ column's width scaled by the wordmark's own aspect — 296 puts it at 573.
379
+ */
380
+ const POSTER_WORDMARK_PX = 296;
381
+ /* The composed lockup's MARK height. NOT tied to POSTER_MARK_PX: the pair grew and this did not,
382
+ because `lockup-vertical` sizes a wordmark from its mark too — matching them would have grown the
383
+ solo form's wordmark past the one the co-branded stack states. */
384
+ const POSTER_LOCKUP_PX = 200;
385
+
386
+ const MOSAIC_CELLS = 10;
387
+ const MOSAIC_MARK_AT = 5;
388
+
389
+ /*
390
+ ★★ THE OTHER EIGHT CELLS ARE THE MARK'S SHAPE WITH A PHOTOGRAPH WHERE THE TREE IS. The mark is a
391
+ rounded square with a thick ring hugging its edge; a picture beside it at some other radius and no
392
+ ring reads as a different kind of object, and the grid stops being one set. So the three numbers
393
+ come off the artwork rather than out of a stylesheet:
394
+
395
+ ring 19 / 434.82 = 4.3696% of the tile, as padding
396
+ outer 95.0854 / 434.82 = 21.8678% of the tile
397
+ inner (95.0854 - 19) / (434.82 - 38) = 19.1738%
398
+
399
+ ⚠ THE INNER RADIUS IS A PERCENTAGE OF THE INNER BOX, NOT OF THE TILE. A percentage
400
+ `border-radius` on the picture resolves against the PICTURE's own width, and the picture is the
401
+ tile less two rings — 91.26% of it. Written as the tile's 17.4981% the corner comes out 4% shallow
402
+ and the ring looks thicker at the corners than along the edges, which is the exact tell that the
403
+ two shapes are not concentric.
404
+
405
+ ★ THE MARK ITSELF TAKES NONE OF THIS. It draws its own square and its own ring — putting it in a
406
+ ringed cell gave a tile inside a tile with a halo between them.
407
+ */
408
+ const RING = `${(MARK_RING / MARK_SIZE) * 100}%`;
409
+ const RADIUS_OUTER = `${(MARK_CORNER / MARK_SIZE) * 100}%`;
410
+ const RADIUS_INNER = `${((MARK_CORNER - MARK_RING) / (MARK_SIZE - 2 * MARK_RING)) * 100}%`;
411
+ const mosaicCells = computed(() => {
412
+ const pics = props.images.slice(0, MOSAIC_CELLS - 1);
413
+ return Array.from({ length: MOSAIC_CELLS }, (_, i) =>
414
+ i === MOSAIC_MARK_AT ? 'mark' : pics[i > MOSAIC_MARK_AT ? i - 1 : i] ?? '');
415
+ });
416
+
417
+ /*
418
+ ★★ THE REGISTER ALONE DECIDES THE COLOURWAY, and that is a fact about deck.json rather than a
419
+ simplification. Every named pair's LIGHT half has dark ink (warm-950, forest-950, neutral-900) and
420
+ every DARK half has `--neutral-0` — so "is the ink light" and "is this the dark register" are the
421
+ same question, on all four pairs and on the deck's own paper. A per-background table here would
422
+ restate that and drift from it.
423
+
424
+ The mark is swapped, never tinted: see Endorsement.vue on why there is no colour prop.
425
+ */
426
+ const endorsementTone = computed(() => (ground.value.dark ? 'reverse' : 'positive'));
427
+
428
+ /*
429
+ ── THE MOSAIC'S ONE INK, AND THE ONE GROUND IT CANNOT BE ──────────────────────────────────────
430
+
431
+ ★★ THE RINGS ARE --forest-950 EVERYWHERE EXCEPT ON A GROUND THAT IS ITSELF --forest-950. There is
432
+ exactly one such ground and it is reached two ways: the deck's dark register, whose --deck-bg IS
433
+ forest-950, and `accent`'s dark half, which is the same colour by construction (deck.json). A
434
+ pinned ring there is a 1.00:1 edge on its own fill — the tiles lose their shape and the mark loses
435
+ its outline.
436
+
437
+ On that ground the rings go to --neutral-0 — all of them at once, the eight photo rings and the
438
+ mark's own, because a field where only some reversed would read as two materials.
439
+
440
+ ★ THE BADGE FOLLOWS THE SAME SHAPE OF RULE ONE STEP LIGHTER. It is `vivid-dark` — forest-700
441
+ carrying white, the same green as the mark's own square — everywhere except on a ground that is
442
+ ITSELF forest-700, where it takes `accent-dark` (forest-950) instead. Measured fill-against-ground
443
+ on the three grounds this variant actually draws:
444
+
445
+ paper (warm-50) vivid-dark 6.17:1 accent-dark 14.80:1
446
+ vivid dark (forest-700) vivid-dark 1.00:1 ← accent-dark 2.40:1
447
+ deck dark (forest-950) vivid-dark 2.40:1 accent-dark 1.00:1
448
+
449
+ One row disqualifies the pill on its own ground, and it is a different row for each of the two
450
+ candidates — which is why the badge and the rings need separate conditions rather than one shared
451
+ "is it dark". `accent-light` (forest-100) was tried on the darkest ground first and is 13.07:1
452
+ there: a near-white lozenge that outshouts the wordmark above it.
453
+
454
+ Named as a half of a published pair either way, so the pill cannot land on a fill/ink combination
455
+ the palette does not sanction.
456
+ */
457
+ /*
458
+ ── A TITLE IN THE BODY TAKES THE WORDMARK'S PLACE ─────────────────────────────────────────────
459
+
460
+ ★★ THE SLOT IS THE SWITCH, because a `title` PROP is not available: `title` is one of Slidev's
461
+ reserved frontmatter keys (packages/slidev-theme/test/props.ts, RESERVED_ON_EVERY_SLIDE) and is
462
+ stripped before frontmatter reaches a layout — a prop named that would render its default forever.
463
+ Writing `# Ride the city` in the body is also how every other cover in this theme gets its title,
464
+ so the mosaic reads the same key the rest of the deck already uses.
465
+
466
+ ⚠ AND IT IS CHECKED AT RENDER TIME, NOT IN A COMPUTED. Calling a slot function inside a computed
467
+ warns ("Slot invoked outside of the render function") and caches a result that a re-rendered slot
468
+ invalidates. A plain function called from the template runs where slots are meant to be read.
469
+
470
+ What counts as content: not comments, not whitespace-only text, and Fragments are walked rather
471
+ than trusted — markdown gives this slot a Fragment whose children are the real answer, so a
472
+ shallow `.length` check is true for an EMPTY body and the wordmark would never draw.
473
+ */
474
+ const slots = useSlots();
475
+ const renderable = (nodes) => nodes.some((n) => {
476
+ if (n.type === Comment) return false;
477
+ if (n.type === Text) return String(n.children).trim() !== '';
478
+ if (n.type === Fragment) return renderable(n.children ?? []);
479
+ return true;
480
+ });
481
+ const hasHeadline = () => renderable(slots.default?.() ?? []);
482
+
483
+ const onDarkestGround = computed(
484
+ () => ground.value.dark && (ground.value.background === 'paper' || ground.value.background === 'accent'),
485
+ );
486
+ const mosaicInk = computed(() => (onDarkestGround.value ? 'var(--neutral-0)' : 'var(--forest-950)'));
487
+ /* The badge's own disqualifying ground is the vivid pair's dark half — forest-700, which is the
488
+ pill's own fill. Nothing else in the table is that colour. */
489
+ const onVividGround = computed(() => ground.value.dark && ground.value.background === 'vivid');
490
+ const badgeBackground = computed(() => (onVividGround.value ? 'accent-dark' : 'vivid-dark'));
491
+
492
+ /*
493
+ ── THE INSET IS ONE PANEL OR TWO, AND `image` IS WHAT DECIDES ─────────────────────────────────
494
+
495
+ ★★ NO NEW PROP AND NO NEW VARIANT. An inset opener with a picture beside it is the same
496
+ composition with something in the other half, not a seventh shape — so it reads the `image` key
497
+ `bleed` already takes, and an author who has no picture gets the single panel this
498
+ variant has always drawn. One less enum value to document, and the fallback is the old behaviour
499
+ rather than an empty box.
500
+
501
+ ★ THE PICTURE TAKES THE RIGHT-HAND HALF, which is the side the lockup signs from and the side
502
+ `bleed` reads from, and the side a reader's eye lands on last. A `flip` would be a second prop
503
+ earning its keep on one composition.
504
+ */
505
+ const insetSplit = computed(() => props.variant === 'inset' && !!props.image);
506
+
507
+ const panelEl = ref(null);
508
+ const mediaEl = ref(null);
509
+ /*
510
+ ⚠ TWO PANELS MEAN TWO CUTS, ONE EACH, RATHER THAN ONE CUT AIMED AT A CORNER. The first attempt
511
+ pointed a single `useNotchCuts` at whichever panel owned the bottom-RIGHT, because that is where
512
+ the lockup signs from. It forgot the other corner: the confidentiality marking sits bottom-LEFT,
513
+ which is the CONTENT panel in a split — so a co-branded, marked cover had its picture correctly
514
+ notched and the marking sitting on a square corner beside it.
515
+
516
+ `occupantFor` already answers "does this thing touch this box" and returns null when it does not,
517
+ so an instance per panel needs no per-corner logic: the content panel seats the marking (and the
518
+ lockup too, when there is no picture to take it), and the picture seats the lockup.
519
+
520
+ ★ `enabled` DROPS `signs`. That predicate is about the LOCKUP — whether this cover signs itself —
521
+ and the marking is drawn from `confidential:` regardless of it. Gating the cut on `signs` meant a
522
+ cover with `chrome: true` and a marking got no seat for the one thing still in the corner.
523
+ */
524
+ const { clip: panelClip } = useNotchCuts(panelEl, {
525
+ enabled: () => props.variant === 'inset',
526
+ occupants: ['.cover-logo'],
527
+ });
528
+ const { clip: mediaClip } = useNotchCuts(mediaEl, {
529
+ enabled: () => insetSplit.value,
530
+ occupants: ['.cover-logo', '.cover-pair'],
531
+ });
532
+
533
+ </script>
534
+
535
+ <template>
536
+ <div
537
+ class="slidev-layout cover relative h-full overflow-hidden"
538
+ :class="[
539
+ VARIANT_CLASS[variant] ?? 'is-plain',
540
+ insetSplit ? 'has-media' : '',
541
+ align === 'center' ? 'is-center' : '',
542
+ ground.background !== 'paper' && !panelShaped ? `bg-${ground.background} deck-own-ground` : '',
543
+ ground.dark && !panelShaped ? 'deck-dark' : '',
544
+ ]"
545
+ >
546
+ <!--
547
+ The mosaic. Placeholders when a deck has not supplied pictures, for the same reason the two
548
+ photographic variants fall back to one: a variant that renders as nothing cannot be told from
549
+ a variant that does not work.
550
+ -->
551
+ <div
552
+ v-if="variant === 'mosaic'"
553
+ class="cover-mosaic"
554
+ aria-hidden="true"
555
+ :style="{ '--mosaic-ring': RING, '--mosaic-radius': RADIUS_OUTER, '--mosaic-radius-inner': RADIUS_INNER, '--mosaic-ink': mosaicInk }"
556
+ >
557
+ <template v-for="(cell, i) of mosaicCells" :key="i">
558
+ <!-- The partner's mark takes the tile only while the wordmark is carrying the identity. -->
559
+ <div v-if="cell === 'mark' && companion && !hasHeadline()" class="cover-mosaic-cell">
560
+ <img :src="assetUrl(companion.src)" :alt="companionLabel">
561
+ </div>
562
+ <!-- The mark is a tile of the grid, at the size the grid gives every other tile. -->
563
+ <Logo v-else-if="cell === 'mark'" variant="mark" :size="MARK_SIZE" class="cover-mosaic-mark" />
564
+ <div v-else class="cover-mosaic-cell">
565
+ <img v-if="cell" :src="assetUrl(cell)" alt="">
566
+ <Placeholder v-else ratio="1:1" label="" />
567
+ </div>
568
+ </template>
569
+ </div>
570
+
571
+ <div
572
+ ref="panelEl"
573
+ class="cover-content relative z-10"
574
+ :class="[
575
+ panelShaped && ground.background !== 'paper' ? `bg-${ground.background} deck-own-ground` : '',
576
+ panelShaped && ground.dark ? 'deck-dark' : '',
577
+ ]"
578
+ :style="panelClip ? { clipPath: panelClip } : undefined"
579
+ >
580
+ <!--
581
+ ★★ THE MOSAIC'S TAIL IS WRAPPED AND THE OTHER VARIANTS' IS NOT, which is a real difference
582
+ rather than duplication for its own sake. On this variant the WORDMARK is the thing that has
583
+ to sit on the slide's centre line — it is the title — and a flex column centres the whole
584
+ block, so the wordmark rode 119px high with the credit and the credential making up the
585
+ difference below it. Centring one child means the others cannot share its row, so they get a
586
+ box of their own and the column becomes a three-row grid (see the stylesheet).
587
+
588
+ Every other variant centres its block, which is correct for them: their title is typeset,
589
+ the rule sits above it and the credit below, and the group is the composition.
590
+ -->
591
+ <!--
592
+ ★★ THE BADGE CHANGES SIDES WITH THE DISPLAY LINE, because it changes JOB with it. Under the
593
+ wordmark it is a strapline — the claim the identity is making — and it reads after the name.
594
+ Above a title it is a kicker: the date, the series, the kind of thing this deck is, which a
595
+ reader wants BEFORE the title it qualifies. Same pill, same slot, two positions, and the
596
+ slide picks between them by whether it wrote a heading.
597
+ -->
598
+ <template v-if="variant === 'mosaic'">
599
+ <!-- Whichever of the two the slide has: its own title, or the identity. Never both. -->
600
+ <div v-if="hasHeadline()" class="cover-title">
601
+ <p v-if="eyebrow" class="deck-eyebrow">{{ eyebrow }}</p>
602
+ <BadgeStrip v-if="badge" :items="[badge]" :background="badgeBackground" class="cover-badge" />
603
+ <slot />
604
+ </div>
605
+ <!--
606
+ ★ 420 IS THE MARK'S HEIGHT, NOT THE WORDMARK'S WIDTH — `size` sets the scale of every form
607
+ from the mark, so this draws 813px across in a 978px measure, leaving 165 of it. Two other
608
+ values were drawn and looked at: 360 (697px) sat too small against the wedge beside it, and
609
+ 480 (929px) matched the specimen's longest title line but crowded the measure to 49px and
610
+ read as a lockup filling its box rather than a mark placed in one.
611
+ -->
612
+ <Logo v-else variant="wordmark" :size="420" class="cover-wordmark" />
613
+ <div class="cover-body">
614
+ <template v-if="!hasHeadline()">
615
+ <p v-if="eyebrow" class="deck-eyebrow">{{ eyebrow }}</p>
616
+ <BadgeStrip v-if="badge" :items="[badge]" :background="badgeBackground" class="cover-badge" />
617
+ </template>
618
+ <p v-if="meta" class="cover-meta">{{ meta }}</p>
619
+ </div>
620
+ </template>
621
+ <!--
622
+ The poster's identity stack: the mark (with a partner beside it when there is one), the
623
+ wordmark at the size a cover states it, then whatever the slide wrote as its title. The
624
+ wordmark leads and the title is subordinate — the opposite of the mosaic's titled form,
625
+ because this composition IS the identity over a photograph.
626
+ -->
627
+ <template v-else-if="variant === 'poster'">
628
+ <!--
629
+ ★★ THE COMPOSED LOCKUP WHEN IT IS OURS ALONE. Stacking a mark over a wordmark by hand is
630
+ re-deriving `lockup-vertical`, which already centres the mark over the wordmark's midline
631
+ and holds the canonical gap — Logo.vue's own note records that a hand-copied gap made the
632
+ vertical lockup 1.7x too airy. One element, the brand's own proportions.
633
+
634
+ A companion cannot go through it: the lockup is one object and the partner's tile belongs
635
+ BESIDE the mark, not beside the pair. So that case draws the two tiles and the wordmark
636
+ under them, which is the same arrangement at the same shared height.
637
+ -->
638
+ <Logo v-if="!companion" variant="lockup-vertical" :size="POSTER_LOCKUP_PX" class="cover-lockup" />
639
+ <template v-else>
640
+ <MarkPair
641
+ class="cover-pair"
642
+ :src="assetUrl(companion.src)"
643
+ :label="companionLabel"
644
+ :size="POSTER_MARK_PX"
645
+ />
646
+ <Logo variant="wordmark" :size="POSTER_WORDMARK_PX" class="cover-wordmark" />
647
+ </template>
648
+ <!-- ★ NO `meta` CREDIT HERE. Every other variant sets its title block against the ground and
649
+ can carry a line under it; this one is an identity over a photograph, and a credit line
650
+ below the title is a fourth thing competing in a stack that already has three. -->
651
+ <slot />
652
+ <!--
653
+ ★★ THE CERTIFICATION IS PART OF THE STACK HERE, NOT A CORNER OF THE SLIDE. Every other
654
+ variant sets its words against the ground and can spare its own bottom-left corner for a
655
+ credential; this one hangs the whole identity on one axis at the right, and a mark pinned
656
+ to the slide's corner reads as belonging to the photograph rather than to the stack.
657
+
658
+ In the column it needs no arithmetic: `align-items: center` is what centres it, so it
659
+ follows the stack whatever the wordmark's size — which a percentage from the slide's edge
660
+ could not do.
661
+ -->
662
+ <Endorsement
663
+ v-if="endorsements"
664
+ mark="b-corp"
665
+ :tone="endorsementTone"
666
+ :size="136"
667
+ class="cover-endorsement-stacked"
668
+ />
669
+ </template>
670
+ <template v-else>
671
+ <!-- The signature, for the variants with no frame to sit a corner mark against. See
672
+ COLUMN_SIGNED. -->
673
+ <Logo v-if="signs && COLUMN_SIGNED.includes(variant)" variant="lockup" :size="72" class="cover-signature" />
674
+ <p v-if="eyebrow" class="deck-eyebrow">{{ eyebrow }}</p>
675
+ <!-- The rule is the brand's opening gesture: a short, heavy accent bar, not a hairline. -->
676
+ <div class="cover-rule" />
677
+ <slot />
678
+ <BadgeStrip v-if="badge" :items="[badge]" class="cover-badge" />
679
+ <p v-if="meta" class="cover-meta">{{ meta }}</p>
680
+ </template>
681
+ </div>
682
+
683
+ <!--
684
+ ★ THE REF IS ON A WRAPPER ELEMENT, NOT ON <Logo>. A ref on a component gives a component
685
+ instance, and `$el` is only an element when that component has a single root — which is not a
686
+ thing this layout should have to know about another component. The wrapper is always an
687
+ element, so the measurement below cannot silently read a comment node and give up.
688
+ -->
689
+ <!-- The inset's other half. Same radius and same inset as the panel beside it, so the two read
690
+ as one object split rather than a panel with a picture stuck on. -->
691
+ <div
692
+ v-if="insetSplit"
693
+ ref="mediaEl"
694
+ class="cover-inset-media"
695
+ :style="mediaClip ? { clipPath: mediaClip } : undefined"
696
+ >
697
+ <img :src="assetUrl(image)" :alt="alt">
698
+ </div>
699
+
700
+ <!--
701
+ The certification, in the slide's own bottom-left corner rather than in the title block. A
702
+ credential is not part of the claim above it: it belongs where a reader looks for one, and on
703
+ this composition that corner is the only piece of furniture on the slide.
704
+ -->
705
+ <Endorsement
706
+ v-if="variant === 'mosaic' && endorsements"
707
+ mark="b-corp"
708
+ :tone="endorsementTone"
709
+ :size="136"
710
+ class="cover-endorsement"
711
+ />
712
+
713
+ <!-- Forest first, then the partner, at one shared height — FSubLogo's rule for a pair, and the
714
+ corner slide-top.vue signs from. The picture's notch seats it. -->
715
+ <MarkPair
716
+ v-if="insetPair"
717
+ class="cover-pair cover-pair-seat"
718
+ :src="assetUrl(companion.src)"
719
+ :label="companionLabel"
720
+ :size="PAIR_MARK_PX"
721
+ />
722
+
723
+ <!-- The poster's photograph, edge to edge, with the scrim drawn over it by `.is-poster::after`. -->
724
+ <div v-if="variant === 'poster'" class="cover-media">
725
+ <slot name="media">
726
+ <img v-if="image" :src="assetUrl(image)" :alt="alt">
727
+ <Placeholder v-else :label="alt || 'Cover image'" ratio="16:9" />
728
+ </slot>
729
+ </div>
730
+
731
+ <div
732
+ v-if="signs && variant !== 'mosaic' && variant !== 'poster' && !COLUMN_SIGNED.includes(variant) && !insetPair"
733
+ ref="lockupEl"
734
+ class="cover-logo"
735
+ >
736
+ <Logo variant="lockup" :size="72" />
737
+ </div>
738
+ </div>
739
+ </template>
740
+
741
+ <style scoped>
742
+ /* ★ THE BEAT MARGIN, now a token. cover/quote/statement/divider/end each spelled
743
+ `calc(var(--deck-grid) * 3)` by hand — 120px, on 37 of 314 non-bleed slides, while every token in
744
+ the system said a slide's text sat at 90. It is a real decision (a 128px display cap wants more
745
+ air than a 48px paragraph does — see scripts/deck.ts above DISPLAY_INSET_MULT), so it is named
746
+ rather than removed; what it is not is five copies of a literal that no gate could see. */
747
+ .cover {
748
+ /*
749
+ ★★ THE INSET IS ON THE CONTENT, NOT ON THE ROOT, and that is what lets a variant bleed. Every
750
+ composition here except `plain` puts something — a picture, a band, a glyph — against a slide
751
+ edge, and a padded root would have held all of them off it by 120px. The beat margin still
752
+ reaches the words, which is what it was for; it just no longer reaches the frame.
753
+ */
754
+ display: flex;
755
+ flex-direction: column;
756
+ justify-content: center;
757
+ }
758
+ .cover > .cover-content {
759
+ padding-left: var(--deck-inset-display);
760
+ padding-right: var(--deck-inset-display);
761
+ }
762
+ /* `align: 'start'` leaves the flex container at its default cross-axis (stretch) so existing
763
+ decks render pixel-identical — only the 'center' state gets an explicit rule. */
764
+ .cover.is-center {
765
+ align-items: center;
766
+ text-align: center;
767
+ }
768
+ /*
769
+ ★ CENTRING THE COLUMN DOES NOT CENTRE A CAPPED CHILD, and this rule used to say so for exactly
770
+ one of them. `align-items: center` centres `.cover-content` as a flex ITEM; its own children are
771
+ BLOCKS, which fill the column — unless a max-width caps them, and then the capped box sits flush
772
+ LEFT while `text-align: center` centres the words inside that box. The result is a line that
773
+ looks centred until you measure it.
774
+
775
+ MEASURED on the `align: center` cover: h1, the paragraph and the rule all centre on 960, and
776
+ `.cover-meta` centred on 801 — 159px off, because base.css caps it at the 720px caption measure
777
+ and the column is 1038 wide. The rule already had `margin: auto` for precisely this reason; meta
778
+ never did, and neither would the next capped thing added here.
779
+
780
+ So it applies to every child rather than being re-granted one at a time. The h1 and the paragraph
781
+ are capped too (1600 and 1200) — those caps are simply wider than the column, which is why they
782
+ were never wrong and why nobody noticed the rule was per-element.
783
+
784
+ Found by the layout overlay's own element boxes, not by eye.
785
+ */
786
+ .cover.is-center .cover-content > * {
787
+ margin-left: auto;
788
+ margin-right: auto;
789
+ }
790
+ /*
791
+ The four named grounds, the same pairs `default` takes at full field — fill and ink together, so
792
+ `dark` on the root re-points both halves.
793
+ */
794
+ /*
795
+ ⚠ THE CLASS LANDS ON EITHER ELEMENT, SO THE FILL HAS TO MATCH BOTH. On a field composition the
796
+ root carries `bg-*`; on a panel-shaped one (`inset`, `band`) the CONTENT does. These rules were
797
+ scoped to `.cover` alone, so every named-ground inset and band had the class, the ink and the
798
+ `deck-own-ground` marker — and no fill at all. Reported as `inset` on warm/accent/vivid/neutral
799
+ rendering with no panel.
800
+ */
801
+ .cover.bg-warm, .cover .cover-content.bg-warm { background: var(--deck-bg-warm); color: var(--deck-on-bg-warm); }
802
+ .cover.bg-accent, .cover .cover-content.bg-accent { background: var(--deck-bg-accent); color: var(--deck-on-bg-accent); }
803
+ .cover.bg-vivid, .cover .cover-content.bg-vivid { background: var(--deck-bg-vivid); color: var(--deck-on-bg-vivid); }
804
+ .cover.bg-neutral, .cover .cover-content.bg-neutral { background: var(--deck-bg-neutral); color: var(--deck-on-bg-neutral); }
805
+ /*
806
+ ★★ THE RULE IS THE GROUND'S MARK, NOT THE ACCENT, ON EVERY GROUND BUT PAPER. `--deck-accent` is
807
+ forest-700 — the brand's opening gesture on cream, and very nearly invisible on `vivid`, which is
808
+ a forest green itself. Each family publishes a `mark` role for exactly this: the colour that reads
809
+ AS a mark on its own ground. Same reason the list bullet re-points a few hundred lines up in
810
+ base.css, and the same table.
811
+ */
812
+ .cover.bg-warm .cover-rule { background: var(--deck-bg-warm-mark); }
813
+ .cover.bg-accent .cover-rule { background: var(--deck-bg-accent-mark); }
814
+ .cover.bg-vivid .cover-rule { background: var(--deck-bg-vivid-mark); }
815
+ .cover.bg-neutral .cover-rule { background: var(--deck-bg-neutral-mark); }
816
+ /* On the dark register with no named ground the accent is forest-300, which carries; nothing to
817
+ re-point. A named ground plus `dark` takes the pair's dark mark from the rules above. */
818
+ .cover-rule {
819
+ /* ★ 120px, not `calc(var(--deck-grid) * 3)`. The accent bar's width is a visual weight, not a
820
+ measure of the layout module — spelling it in grid units meant a change to the grid resized the
821
+ brand mark on every cover. It rode the grid because 3 units happened to be the right length. */
822
+ width: 120px;
823
+ height: 8px;
824
+ background: var(--deck-accent);
825
+ margin-bottom: var(--deck-space-4);
826
+ }
827
+ /* The cover is a beat slide — Mohr uppercase, not the layout-default working voice. */
828
+ .cover :deep(h1) {
829
+ font-family: var(--type-deck-display-family);
830
+ font-weight: var(--type-deck-display-weight);
831
+ text-transform: uppercase;
832
+ font-size: var(--type-deck-text-9xl); /* 8rem / 128px — ramp 9xl, the deck's biggest step */
833
+ line-height: 0.92;
834
+ /* Mohr uppercase averages 0.694em per character, so 18 characters ≈ 12.5em. In `ch` this
835
+ same cap reads 18ch = 14.2em = 1817px, which is WIDER than the 1680px content band — the
836
+ title would run off the slide rather than wrap. */
837
+ max-width: 12.5em;
838
+ }
839
+ /*
840
+ The subtitle under a cover title is the lead role, not body copy.
841
+
842
+ ★★ THE INK IS SCOPED OFF THE OWN-GROUND SLIDES, and it has to be. `--deck-fg-muted` and
843
+ `--deck-fg-subtle` are the SLIDE ground's roles; on a named background the ink comes from the
844
+ PAIR, and base.css already hands every `p` on a `bg-*` class back to it with `color: inherit`.
845
+ These two rules are scoped tighter than that one, so they won — measured by the audit on the new
846
+ cover run: the credit scored 2.62:1 on vivid and 1.36:1 on vivid's dark half, against the 3:1 a
847
+ graphical-weight text is held to.
848
+
849
+ ⚠ AND THE TEST IS ON BOTH ELEMENTS, because `bg-*` lands on either one. It was written as
850
+ `.cover:not(.deck-own-ground)` — the ROOT — which is right for the four field compositions and
851
+ wrong for the two panel-shaped ones, where the class rides on `.cover-content` and the root stays
852
+ bare. So `inset` and `band` on a named ground kept the muted tier over the PAIR's fill: measured
853
+ again on the same run, 2.62:1 on vivid's light half and 2.89:1 on accent's dark half, on four
854
+ slides. Both elements are now excluded, which is the same rule stated where the class actually is.
855
+
856
+ Excluding own-ground slides lets base.css do it, which is also the answer it gives for why: a
857
+ panel or a full-field slide is a PAGE, and a page's paragraphs read at its ink rather than at a
858
+ tier below it. The tiers stay where the deck's own paper is the ground.
859
+
860
+ ⚠ ONLY THE INK, THOUGH. This selector used to carry the SIZE as well, and a ground is not a type
861
+ scale: the same slide written twice — once unstated, once `dark: true` — set its lead at two
862
+ different sizes, because the first resolves to a named pair (own-ground, rule skipped, base
863
+ paragraph size) and the second to the deck's own dark register (rule applies, subtitle size).
864
+ Reported on the mosaic, where the two are a keystroke apart. A subtitle is the lead role on every
865
+ ground; what the ground decides is the colour it is drawn in, and nothing else.
866
+ */
867
+ .cover :deep(h1 + p) {
868
+ font-size: var(--type-deck-subtitle-size);
869
+ line-height: var(--type-deck-subtitle-line-height);
870
+ max-width: 20em; /* ≈40 characters of GT Haptik */
871
+ text-wrap: balance; /* a two-line subtitle splits evenly rather than orphaning a word */
872
+ }
873
+ .cover:not(.deck-own-ground) > .cover-content:not(.deck-own-ground) :deep(h1 + p) {
874
+ color: var(--deck-fg-muted);
875
+ }
876
+ /*
877
+ ★★ AND A TIER BELOW ON A NAMED GROUND TOO, MIXED FROM THE PAIR'S OWN INK. The rule above cannot
878
+ reach these slides — `--deck-fg-muted` is the SLIDE ground's role and was measured at 2.62:1 over
879
+ vivid — so for a while the subtitle read at full ink on a pair and muted on the deck's paper. Two
880
+ tiers for one role, decided by a key the author may not have written.
881
+
882
+ base.css is right that a panel or a full-field slide is a PAGE and its paragraphs read at the
883
+ page's ink; a cover subtitle is not one of those paragraphs. It is the lead under a display line,
884
+ and it is a tier below that line on every ground or it is not a role at all.
885
+
886
+ ★ 80% OF `currentColor`, MEASURED ON ALL EIGHT HALVES rather than picked: the weakest is
887
+ vivid-dark at 4.63:1, which clears the 4.5 body floor even though a 60px line is only held to 3.
888
+ (Full ink there is 6.27:1; 75% still passes at 4.27, and 85% was too close to no change at all.)
889
+
890
+ ⚠ `color-mix`, NOT `opacity`, and base.css says why twice: alpha on the element also dims any
891
+ ::before it owns and overrides an inline colour a child stated on purpose — both shipped once and
892
+ both dropped a specimen under the floor. Mixing the COLOUR touches this element's text and nothing
893
+ else. `currentColor` inside `color` resolves to the INHERITED value, which is the pair's ink.
894
+ */
895
+ .cover.deck-own-ground :deep(h1 + p),
896
+ .cover > .cover-content.deck-own-ground :deep(h1 + p) {
897
+ color: color-mix(in oklab, currentColor 80%, transparent);
898
+ }
899
+ .cover-meta {
900
+ margin-top: var(--deck-space-4);
901
+ font-size: var(--type-deck-caption-size);
902
+ /*
903
+ ★ NOT THE PROSE MEASURE. base.css caps every <p> at 20em so body copy keeps a readable line
904
+ length, and at caption size that is 720px — which wrapped a one-line credit
905
+ ("Generated specimen book · @humanforest/slidev-theme", about 50 characters) onto two, under a
906
+ title running to 1600 and a subtitle to 1200. A measure exists to stop the EYE travelling too
907
+ far between lines; a credit has no second line to travel to, so the cap only breaks it. It still
908
+ cannot exceed the content band, which is what actually keeps it on the slide.
909
+ */
910
+ max-width: none;
911
+ }
912
+ /* The credit's tier, on the deck's own paper only — see the note on the subtitle above. */
913
+ .cover:not(.deck-own-ground) > .cover-content:not(.deck-own-ground) .cover-meta {
914
+ color: var(--deck-fg-subtle);
915
+ }
916
+ /*
917
+ ⚠ EXCEPT ON THE DARK REGISTER, WHERE THE SUBTLE TIER DOES NOT CLEAR THE FLOOR. --deck-fg-subtle is
918
+ --neutral-500 in BOTH registers, and a dark cover has TWO grounds. A panel-shaped one paints its
919
+ PANEL, which with no named background takes --deck-surface — mixed from --forest-950 that is
920
+ #0d332d, where the subtle tier measures 2.89:1 against the 3:1 a graphical-weight text is held to.
921
+ Every other variant paints the slide, whose ground is --forest-950 itself and where the same tier
922
+ scrapes 3.25:1. The muted tier is --neutral-400: 5.24:1 and 5.90:1 on those two, still a step
923
+ below the ink on both.
924
+
925
+ ★★ HENCE TWO ARMS, BECAUSE `deck-dark` LANDS ON TWO DIFFERENT ELEMENTS. The panel-shaped variants
926
+ put it on .cover-content — the panel is the thing that is dark — and the rest put it on .cover.
927
+ The `deck-own-ground` pair twenty lines up already splits that way; this rule did not, so the
928
+ inset covers kept the subtle tier and the audit caught them at 2.89:1.
929
+
930
+ Scoped to this layout rather than fixed in the role, because --deck-fg-subtle has other callers
931
+ on other grounds and re-pointing it deck-wide is a bigger decision than a credit line.
932
+ */
933
+ .cover.deck-dark:not(.deck-own-ground) > .cover-content:not(.deck-own-ground) .cover-meta,
934
+ .cover:not(.deck-own-ground) > .cover-content.deck-dark:not(.deck-own-ground) .cover-meta {
935
+ color: var(--deck-fg-muted);
936
+ }
937
+ /* --deck-space-6 IS 60px, the same number this used to spell as `calc(var(--deck-grid) * 1.5)`. A
938
+ raw grid multiple used as a POSITION is the shape deckInsets.static.test.ts now refuses across
939
+ every layout, for the reason the beat margin above proves: an edge offset written as arithmetic
940
+ is invisible to the system that owns edge offsets. */
941
+ /* The interval a kicker takes — the lockup is the first thing in the block, not part of the eyebrow
942
+ under it. `align-self: start` so it is its own width rather than the column's. */
943
+ .cover-signature {
944
+ align-self: start;
945
+ margin-bottom: var(--deck-space-5);
946
+ }
947
+ .cover-logo {
948
+ position: absolute;
949
+ right: var(--deck-space-6);
950
+ bottom: var(--deck-space-6);
951
+ z-index: 10;
952
+ }
953
+
954
+ /*
955
+ ── THE SIX COMPOSITIONS ───────────────────────────────────────────────────────────────────────
956
+
957
+ ★★ EVERY PANEL AND WASH HERE IS MIXED FROM `currentColor`, NOT FROM A ROLE. The ground is a free
958
+ variable — five names, two registers, ten grounds in all — and a panel pinned to `--deck-surface`
959
+ is a page-role colour that does not re-point on any of them: it would be a cream card on green.
960
+ Mixed from the ink the ground already handed down, an inset panel, a band and the mark's own tint
961
+ are correct on all ten by construction, and on the next ground the system invents. Same reasoning
962
+ as the inline code chip, and the same alpha ladder.
963
+ */
964
+ /*
965
+ ── poster: a photograph edge to edge, the identity stacked over a scrim at the right ───────────
966
+
967
+ ★★ THE SCRIM IS A GRADIENT FROM THE SIDE THE TYPE IS ON, not an even wash. `bleed`'s own note
968
+ makes the argument and this composition is the same one: an even veil costs the photograph
969
+ everywhere, including the two thirds of it nobody is reading. Here the words are a column at the
970
+ RIGHT, so the density is heaviest there and gone by the middle — the bike, the wall and the
971
+ pavement come through untouched.
972
+
973
+ ★ BLACK, NOT --deck-bg, for the reason `.scrim-full.deck-dark` records: a scrim is a neutral
974
+ density over somebody's photograph, and --deck-bg on the dark register is a green that tints
975
+ every colour underneath it.
976
+ */
977
+ /* ⚠ THE PICTURE IS LIFTED OUT OF FLOW, and these three lines went missing once. They were the
978
+ shared `.cover-media` base, written for `figure` and `bleed`; both variants were removed and the
979
+ base went with them, leaving the poster's photograph an ordinary block that pushed the identity
980
+ stack off the top of the slide. The element is this variant's alone now, so its positioning lives
981
+ with it. */
982
+ .cover-media {
983
+ position: absolute;
984
+ inset: 0;
985
+ overflow: hidden;
986
+ z-index: 0;
987
+ }
988
+ .cover-media :deep(img) { width: 100%; height: 100%; object-fit: cover; display: block; }
989
+ .cover-media :deep(.deck-placeholder) { width: 100%; height: 100%; }
990
+ .cover.is-poster { justify-content: center; }
991
+ .cover.is-poster::after {
992
+ content: '';
993
+ position: absolute;
994
+ inset: 0;
995
+ background: linear-gradient(
996
+ to left,
997
+ rgb(0 0 0 / 76%) 0%,
998
+ rgb(0 0 0 / 58%) 30%,
999
+ transparent 68%
1000
+ );
1001
+ z-index: 1;
1002
+ }
1003
+ /*
1004
+ The stack sits in a column at the right, centred on itself. `margin-left: auto` rather than a
1005
+ width: the block is as wide as the wordmark it holds, so it cannot end up a narrow column of
1006
+ wrapped type beside a wide picture.
1007
+ */
1008
+ /*
1009
+ ⚠ `fit-content`, NOT `auto`. `.cover-content` is a block that fills the column, so `width: auto`
1010
+ left it full width and `margin-left: auto` had nothing to push — measured, the stack came out at
1011
+ x 384 on one slide and x 817 on another purely because their headings were different lengths.
1012
+ Sized to its contents, the block is as wide as the wordmark and the margin can do its job.
1013
+ */
1014
+ /*
1015
+ ⚠ AND A FLEX COLUMN, NOT A BLOCK. `align-items: center` was already here and was doing NOTHING —
1016
+ the content box is a block, so the property is inert and each child fell back to its own idea of
1017
+ the axis: the wordmark centred on the text, the pair (a block-level flex row) filled the column
1018
+ and left its two tiles hard against the left edge. Measured on this slide: the pair's box 424px
1019
+ wide with a 173px pair inside it, its marks 100px left of the wordmark's centre.
1020
+ */
1021
+ /*
1022
+ ★★ THE RIGHT THIRD, STATED AS A SHARE. Jose: "if this were split, this should be 2:1 for 142" —
1023
+ the photograph holds two parts and the identity one, which is the proportion `split` would have
1024
+ been given. A `fit-content` column could not say that: it was as wide as whatever the wordmark
1025
+ happened to be, so the axis moved with the type rather than with the composition.
1026
+
1027
+ ⚠ POSITIONED, NOT LAID OUT IN FLOW, and its own padding zeroed. Three measured reasons, in the
1028
+ order they were found:
1029
+
1030
+ · `.cover-content` pads itself by --deck-inset-display on both sides — right for a column of
1031
+ words, wrong for a share of the frame. It made the box a third PLUS 240, and put the axis the
1032
+ certification centres on 60px off the stack's.
1033
+ · `height: 100%` on a flex item measured the cover's own centred box rather than the frame: the
1034
+ column ran y 283 → 1171 on a 1080 canvas, so the stack sat low and the certification anchored
1035
+ to the foot of a box that was not the slide.
1036
+ · Pinned to the cover's padding edges, `justify-content: center` means the middle of the SLIDE
1037
+ and `bottom: 0` on the certification means the slide's own bottom line.
1038
+ */
1039
+ .cover.is-poster > .cover-content {
1040
+ display: flex;
1041
+ flex-direction: column;
1042
+ align-items: center;
1043
+ justify-content: center;
1044
+ text-align: center;
1045
+ position: absolute;
1046
+ inset-block: 0;
1047
+ /* The column is pinned to the CANVAS, so it carries the slide's own margins itself: --deck-space-6
1048
+ off the right edge (a mark sits nearer an edge than a paragraph would) and the text inset top
1049
+ and bottom, which is the line the certification lands on. */
1050
+ right: var(--deck-space-6);
1051
+ width: 33.3333%;
1052
+ padding-block: var(--deck-inset-text);
1053
+ padding-inline: 0;
1054
+ }
1055
+ .cover.is-poster .cover-lockup { margin-bottom: var(--deck-space-4); }
1056
+ .cover.is-poster .cover-pair { margin-bottom: var(--deck-space-5); }
1057
+ .cover.is-poster .cover-wordmark { margin-bottom: var(--deck-space-3); }
1058
+ /*
1059
+ The title is subordinate here — the wordmark is the display line, so a heading takes the lead
1060
+ role's size rather than the 128px step it gets on every other variant.
1061
+
1062
+ ★★ AND THE WORKING VOICE, NOT THE BEAT ONE. A cover is a beat layout and sets h1 in Mohr Black
1063
+ uppercase; on this variant the WORDMARK is already the Mohr line, so a title above it in the same
1064
+ face is the identity said twice. GT Haptik Bold, sentence case — base.css's own h1 default, which
1065
+ this file overrode for every other variant.
1066
+ */
1067
+ .cover.is-poster :deep(h1) {
1068
+ font-family: var(--type-deck-headline-family);
1069
+ font-weight: var(--type-deck-headline-weight);
1070
+ font-size: var(--type-deck-subtitle-size);
1071
+ line-height: var(--type-deck-subtitle-line-height);
1072
+ text-transform: none;
1073
+ max-width: none;
1074
+ }
1075
+ /*
1076
+ ★★ THE STACK IS VERTICALLY CENTRED AND THE CERTIFICATION IS NOT. A credential is furniture: it
1077
+ belongs at the foot of the composition, the way it does on every other cover, while the identity
1078
+ holds the middle of the frame. So it leaves the flow — but stays inside the COLUMN rather than
1079
+ going back to the slide's corner, which is what keeps it on the stack's own axis whatever the
1080
+ wordmark's size. `left: 50%` of a `fit-content` column IS that axis.
1081
+ */
1082
+ /* ⚠ Not `.cover.is-poster .cover-endorsement-stacked`: deckNotch.static reads EVERY class in a
1083
+ corner-pinned rule and demands a `v-if` on an element carrying it, and `.cover` is the layout
1084
+ root. The class is only ever emitted on the poster, so the prefix bought nothing. */
1085
+ .cover-endorsement-stacked {
1086
+ position: absolute;
1087
+ /* ⚠ THE INSET IS STATED HERE, NOT INHERITED FROM THE COLUMN'S PADDING. An absolutely positioned
1088
+ box is placed against its containing block's PADDING box, so the column's `padding-block` — the
1089
+ air its flow children get — does not move this one at all. At `bottom: 0` the mark sat flush on
1090
+ the canvas edge. The number is the text inset, the line every other piece of cover furniture
1091
+ sits on. */
1092
+ bottom: var(--deck-inset-text);
1093
+ left: 50%;
1094
+ transform: translateX(-50%);
1095
+ }
1096
+
1097
+ /* ── inset: the words in the theme's own panel, floating on the ground ───────────────────────── */
1098
+ /*
1099
+ ★★ THIS IS `split`'s PANEL, NOT AN APPROXIMATION OF ONE. It was a `currentColor` wash with a
1100
+ two-pixel edge and the code radius — a thing that looked panel-ish and was in the system nowhere.
1101
+ A panel in this theme is a named pair carried on a `bg-*` class, at `--deck-radius-card`, inset
1102
+ from the canvas: exactly what `split` draws at `frame: 'panel'`, `edge: 'inset'`. So the fill and
1103
+ the ink come from the class the root hands this element, and the geometry comes from the same two
1104
+ tokens the split panel uses. A cover's inset panel and a split's panel are now the same object.
1105
+ */
1106
+ /*
1107
+ ★★ THE PANEL IS THE SLIDE, INSET — NOT A BOX AROUND THE WORDS. It hugged its content at first,
1108
+ which put a small card adrift in a large field and read as a quote rather than an opener. The
1109
+ panel takes the whole canvas less one inset on every side, the words sit inside it at the card
1110
+ pad, and the lockup sits in the corner it cuts for itself. That is the shape the brand's own
1111
+ covers use.
1112
+ */
1113
+ /*
1114
+ ★★ THE PANEL GREW OUTWARD AND THE WORDS DID NOT MOVE. It read as tight inside: 32px of card pad
1115
+ around a 128px display title, on a panel held 96px off the canvas. Rather than simply padding it
1116
+ more — which pushes the title inward and shrinks the measure — the two insets trade. The panel now
1117
+ sits `--deck-space-4` (32) from the canvas and pads itself by `--deck-inset-text` (96): the text
1118
+ still starts 128px from the slide edge, exactly where it did, and the panel is 64px larger on every
1119
+ side with three times the air inside it.
1120
+
1121
+ Both numbers are the ladder's, not a tuning: 32 is the panel's own step off an edge, and 96 is the
1122
+ inset every slide in this deck gives its text. One variable, so the lockup below cannot drift from
1123
+ the edge it has to sit on.
1124
+ */
1125
+ .cover.is-inset {
1126
+ --cover-inset: var(--deck-space-4);
1127
+ justify-content: stretch;
1128
+ padding: var(--cover-inset);
1129
+ }
1130
+ .cover.is-inset > .cover-content {
1131
+ flex: 1;
1132
+ display: flex;
1133
+ flex-direction: column;
1134
+ justify-content: center;
1135
+ /*
1136
+ ⚠ NO MARGIN. `.slidev-layout` already pads every slide by the text inset, so a margin here
1137
+ applied it TWICE — measured 96 + 96 on each side, and the panel came out 696px tall in a 1080
1138
+ frame, nowhere near the lockup it is supposed to cut around. The root's padding IS the inset.
1139
+ */
1140
+ padding: var(--deck-inset-text);
1141
+ border-radius: var(--deck-radius-card);
1142
+ }
1143
+ /*
1144
+ ★★ THE SIGNATURE IS FLUSH WITH THE PANEL'S CORNER, and it has to be to the pixel. `occupantFor`
1145
+ decides a thing is IN a corner by touching it within 2px of slack — the same test the chrome
1146
+ passes, because chrome sits on a panel's edge. At the layout's own `--deck-space-6` the lockup
1147
+ landed outside the panel entirely; one card pad in, it was 32px clear of the edge and still not
1148
+ touching. Both returned null and the panel kept four plain radii. Flush, and `--deck-notch-gap`
1149
+ (24px) is what opens the ring around it — the gap belongs to the CUT, not to the placement.
1150
+ */
1151
+ /*
1152
+ ★★ THE PAIR IS <MarkPair>, NOT TWO ELEMENTS AND A RULE. It used to be a mark, an <img> and a block
1153
+ of CSS pinning the chrome's --deck-mark-radius / --deck-mark-ring-width — numbers computed for a
1154
+ 72px tile. The poster draws the same pair at 120 and inherited a corner and a ring 40% too small.
1155
+ The component takes a `size` and derives every other number from it; this file states the size and
1156
+ where the pair sits, which is all a layout should know about somebody else's mark.
1157
+ */
1158
+ /*
1159
+ Flush with the panel's own corner, exactly as the lockup is with the other one: `occupantFor` seats
1160
+ a thing by TOUCHING the box within 2px, so an inset here would return null and leave the picture's
1161
+ corner square under the marks. The poster's pair is part of its stack, so only the inset's is
1162
+ lifted out of flow — and it says so with a class of its own.
1163
+
1164
+ ⚠ A CLASS OF ITS OWN, `.cover-pair-seat`, rather than `.cover.is-inset .cover-pair` or
1165
+ `.cover-pair.is-seated`. deckNotch.static reads every class in a corner-pinned rule and then
1166
+ demands a `v-if` on an element carrying it — the check that stops a layout parking something in a
1167
+ chrome corner without asking whether the chrome is there. `.cover` made it ask that of the layout
1168
+ ROOT, which has no `v-if` and never could; `.cover-pair` made it ask that of the POSTER's pair,
1169
+ which is in normal flow and needs none. Only the pinned element may name the pinned rule.
1170
+ */
1171
+ .cover-pair-seat {
1172
+ position: absolute;
1173
+ top: var(--cover-inset);
1174
+ right: var(--cover-inset);
1175
+ z-index: 10;
1176
+ }
1177
+ /* Two halves of one object: the same gap between them as between either and the frame. */
1178
+ .cover.is-inset.has-media {
1179
+ flex-direction: row;
1180
+ gap: var(--cover-inset);
1181
+ }
1182
+ .cover.is-inset.has-media > .cover-content,
1183
+ .cover.is-inset .cover-inset-media {
1184
+ flex: 1;
1185
+ min-width: 0;
1186
+ }
1187
+ .cover-inset-media {
1188
+ border-radius: var(--deck-radius-card);
1189
+ overflow: hidden;
1190
+ }
1191
+ .cover-inset-media :deep(img) {
1192
+ width: 100%;
1193
+ height: 100%;
1194
+ object-fit: cover;
1195
+ display: block;
1196
+ }
1197
+ /*
1198
+ ⚠ THE TEXT TAKES A SMALLER INSET ONCE IT IS HALF A SLIDE. The single panel pads itself by
1199
+ --deck-inset-text (96) because it IS the slide and its words should start where every other
1200
+ slide's do; half that width cannot afford 96 on both sides of a 128px display line.
1201
+
1202
+ ★★ BUT IT STILL OWES THE DISPLAY INSET FROM THE CANVAS, WHICH IS WHAT SETS THE NUMBER. The panel
1203
+ floats --cover-inset (32) off the edge, so paying 120 − 32 = 88 puts the display line at 120 —
1204
+ exactly where every other beat layout's starts. --deck-space-6 (60) landed it at 92: inside the
1205
+ 120px display inset the audit grades a cover against, and inside even the 96px inset an ordinary
1206
+ paragraph keeps. Same expression the mosaic uses below, for the same complaint. The card pad (32)
1207
+ was the first answer and is a card's, not a panel's: at 32 the type sat against the edge and read
1208
+ as a crop rather than a composition.
1209
+
1210
+ ⚠ WHICH MAKES THIS THE TIGHTEST DISPLAY COLUMN IN THE THEME, and a title has to be written for it.
1211
+ The panel is (1920 − 2×32 − 32) / 2 = 912 wide, so the measure is 912 − 176 = 736. Mohr uppercase
1212
+ at the 128px step runs about 80px a character, so 736 holds NINE — and because
1213
+ `overflow-wrap: break-word` is global, a tenth does not overflow, it BREAKS MID-WORD and reads
1214
+ as a typo. Measured: "A partnership" set as "A PARTNERSH / IP".
1215
+ */
1216
+ .cover.is-inset.has-media > .cover-content {
1217
+ padding: calc(var(--deck-inset-display) - var(--cover-inset));
1218
+ }
1219
+ /*
1220
+ ★ AND THE LEAD STEPS DOWN WITH THE COLUMN. The subtitle role is 60px, set for a panel that is the
1221
+ whole slide — 1856px of measure. Half of that, less this inset, is 736, where 60px runs to three
1222
+ lines and reads as a paragraph competing with the title above it rather than as a line under it.
1223
+ 48 is the next rung (there is nothing between), and the title is still 128, so the tier survives.
1224
+
1225
+ Same reasoning as `quote`'s own step-down beside its figure, and the same mistake if left alone: a
1226
+ column that halves without the type following compounds a reduction the layout already made.
1227
+ */
1228
+ .cover.is-inset.has-media :deep(h1 + p) {
1229
+ font-size: var(--type-deck-body-size);
1230
+ }
1231
+ .cover.is-inset .cover-logo {
1232
+ right: var(--cover-inset);
1233
+ bottom: var(--cover-inset);
1234
+ }
1235
+ /* With no named background the panel still has to read as a plane, so it takes the deck's own
1236
+ lifted surface — the one role that IS the answer to "a plane on the page". */
1237
+ .cover.is-inset:not(:has(> .deck-own-ground)) > .cover-content {
1238
+ background: var(--deck-surface);
1239
+ }
1240
+ /*
1241
+ ⚠ AND THE PANEL DOES CUT FOR THE CONFIDENTIALITY MARKING — this note used to say it did not need
1242
+ to, and that stopped being true the moment the panel moved. A cover carries no running chrome (see
1243
+ `signs`), but the marking is exempt from that: slide-bottom.vue draws it from `confidential:`
1244
+ alone. It sits bottom-LEFT at --deck-inset-chrome (36px), and the panel now sits at
1245
+ --deck-space-4 (32) rather than the 96 it had when this claim was written, so the two overlap.
1246
+
1247
+ Nothing here changed to fix it — useNotchCuts already measures the marking. Its selector was
1248
+ simply the pre-split `.badge-strip-item` one and matched nothing; see the note there.
1249
+ */
1250
+
1251
+
1252
+ /*
1253
+ ── mosaic: the words on a field, beside a grid of pictures that runs off the edge ──────────────
1254
+
1255
+ ★★ THE GRID IS WIDER THAN ITS BOX AND THAT IS THE COMPOSITION. A mosaic that fits inside the slide
1256
+ reads as a contact sheet; one that is cut by the edge reads as a fragment of something larger,
1257
+ which is what the brand's own covers do. So the track is sized past the right edge and the column
1258
+ offsets stagger it, rather than the cards being placed by hand.
1259
+
1260
+ ★ THE CARDS ARE `aria-hidden`. They carry no information the title does not — the alt text on six
1261
+ decorative photographs is six interruptions before a reader reaches the one line that matters.
1262
+ */
1263
+ /* ★ 52%, and the reason is the measure rather than a proportion: the content keeps the display
1264
+ inset on both sides, so a narrower column eats the measure rather than the margin. At 46% the
1265
+ title had 555px to run in and Mohr uppercase at 128px broke MID-WORD on the eighth character —
1266
+ `overflow-wrap: break-word` is global, so it wrapped rather than overflowing and read as a
1267
+ typo. 52% leaves 758px, which is 8 characters, and the mosaic starts past it. */
1268
+ /*
1269
+ ★★ THIS VARIANT STOPS PAYING THE DISPLAY INSET TWICE. `.slidev-layout` already pads every slide by
1270
+ --deck-inset-text (96) and `.cover > .cover-content` adds --deck-inset-display (120) on top, so
1271
+ the type began 216px in and the whole block floated away from the frame. That second inset is for
1272
+ a 128px TYPESET title (see DISPLAY_INSET_MULT in scripts/deck.ts) and this variant has none — its
1273
+ display line is the wordmark, artwork with its own sidebearings built in.
1274
+
1275
+ ★ 58% OF THE CANVAS, WHICH IS A MEASURE RATHER THAN A PROPORTION. At 52% the column gave the
1276
+ display step 974px to run in and "QUARTERLY TECH REVIEW" broke to THREE lines — "TECH REVIEW" is
1277
+ 977px of Mohr uppercase at 128px, three pixels over. 58% gives it 1089 and the title sets in two,
1278
+ which also buys back the clearance above the corner mark (60px, from 1px). The wedge moved right
1279
+ by the same argument: 40% instead of 36%, taking its tiles from 307 to 288.
1280
+
1281
+ ⚠ 120 EXACTLY, NOT 96, AND THE AUDIT IS WHY. Tried the text inset first, which put the type flush
1282
+ with the wedge's own 96px bleed and read well — and scripts/deckAudit.ts grades cover, divider,
1283
+ end, quote and statement against the DISPLAY inset, so the badge came back "24px past the left
1284
+ inset" on all seven mosaic slides. The bound is right and this variant is not special enough to
1285
+ be carved out of it: the difference between 216 and 120 is the whole complaint, and 120 is a
1286
+ number the deck already owns.
1287
+ */
1288
+ .cover.is-mosaic > .cover-content {
1289
+ width: 58%;
1290
+ padding-inline-start: calc(var(--deck-inset-display) - var(--deck-inset-text));
1291
+ padding-inline-end: 0;
1292
+ }
1293
+ /*
1294
+ ★★ THREE ROWS, THE OUTER TWO EQUAL, SO THE MIDDLE ONE IS THE SLIDE'S CENTRE LINE. `1fr auto 1fr`
1295
+ is the whole mechanism: two equal fractions cannot help but put the `auto` row's midpoint at the
1296
+ container's, whatever the tail below it weighs. MEASURED before: the wordmark's box centred on 421
1297
+ against the canvas's 540 — 119px high — because the column was a centred flex block and the
1298
+ subtitle and the credential were counted into it.
1299
+
1300
+ The tail then hangs from directly under the wordmark (`align-self: start`) rather than centring in
1301
+ its own row, so the interval between the two is the wordmark's own margin and nothing else.
1302
+ */
1303
+ .cover.is-mosaic { justify-content: stretch; }
1304
+ .cover.is-mosaic > .cover-content {
1305
+ flex: 1;
1306
+ display: grid;
1307
+ grid-template-rows: 1fr auto 1fr;
1308
+ }
1309
+ .cover.is-mosaic .cover-wordmark,
1310
+ .cover.is-mosaic .cover-title { grid-row: 2; }
1311
+ .cover.is-mosaic .cover-body { grid-row: 3; align-self: start; }
1312
+ .cover-mosaic {
1313
+ position: absolute;
1314
+ /*
1315
+ ★★ THE TILES STACK INTO A WEDGE IN THE TOP-RIGHT CORNER — four across, then three, two, one —
1316
+ rather than filling a rectangle. A full grid of pictures is a contact sheet whatever its size;
1317
+ drawn and measured, three by three read as a table and four rows as a proof sheet, and both
1318
+ argued with the wordmark for the same attention. A wedge has a direction: it is heaviest where
1319
+ it meets the corner and thins as it comes down towards the type, so the eye lands on the
1320
+ identity and travels into the pictures rather than choosing between them.
1321
+
1322
+ ★ CUT EQUALLY ON THE TWO EDGES IT MEETS. Top and right both bleed by --deck-inset-text (96px),
1323
+ the same number on both axes, so the corner reads as a corner the field carries on past rather
1324
+ than as two unrelated crops. The wedge is anchored to the TOP by that measurement and hangs from
1325
+ it, so whatever the tiles cost in height runs off the bottom: four rows come to 1324px against a
1326
+ 1080px canvas, and the last tile — the single one at the point of the wedge — loses 148px. That
1327
+ is a consequence of the anchor rather than a third decision, and it falls on the tile furthest
1328
+ from the type.
1329
+ */
1330
+ inset-block-start: calc(var(--deck-inset-text) * -1);
1331
+ /*
1332
+ ★ THE WEDGE REACHES BACK PAST THE TEXT COLUMN, and only its top row is up there. The rows step
1333
+ RIGHT as they descend, so the widest one is the highest one — measured, its leftmost tile spans
1334
+ y -96 to 192 while the wordmark sits at 457 and the credit below it at 675. They share a column
1335
+ of the slide and never a row of it, which is what buys the tiles their size: held clear in x,
1336
+ each one would be 80px smaller.
1337
+
1338
+ ⚠ THE SECOND ROW IS THE ONE A TITLE CAN HIT, and 42% is where it stops being able to. That row
1339
+ DOES share y with the display line, so the only thing between them is x. Measured across the
1340
+ three titled specimens, the longest line is "FLEET REPORT" at 959px from x 120 — ending at 1079
1341
+ — and at 40% the second row began at 1088. Nine pixels, and it read as the title touching the
1342
+ picture.
1343
+
1344
+ Capping the measure instead cannot work, and the numbers say why: "FLEET REPORT" (959) and
1345
+ "TECH REVIEW" (911) are 48px apart, so every width that wraps the first also breaks "Quarterly
1346
+ Tech Review" onto a third line. The wedge is the thing with slack. At 42% the second row starts
1347
+ at 1117, which clears the longest title by 38px — more than the 32px the tiles clear each other
1348
+ by, which is the interval this composition already uses everywhere else — and costs each tile
1349
+ 10px (288 to 278).
1350
+ */
1351
+ inset-inline-start: 42%;
1352
+ inset-inline-end: calc(var(--deck-inset-text) * -1);
1353
+ display: grid;
1354
+ grid-template-columns: repeat(4, 1fr);
1355
+ gap: var(--deck-space-4);
1356
+ align-content: start;
1357
+ z-index: 0;
1358
+ /*
1359
+ ★★ THE RING DOES NOT FOLLOW THE GROUND, and that is the one place in this layout where a colour
1360
+ is pinned rather than mixed. Everything else here is `currentColor` so it re-points on all ten
1361
+ grounds — but the mark is ARTWORK, and its ring is the mark's own edge, not a reading of the
1362
+ page. Left inheriting, `<Logo>` paints that ring in the deck's ink and it goes white on the dark
1363
+ register: the tile stops being the mark and becomes a reversed drawing of it, with the eight
1364
+ photographs beside it reversing too.
1365
+
1366
+ AND THE COLOUR IS THE DESIGN SYSTEM'S, NOT THE DECK'S READING OF IT. scripts/logo.ts — the
1367
+ generator that bakes the downloadable artwork straight off the forest ramp — is explicit:
1368
+ `const INK = 950, GREEN = 700; // wordmark + ring + inner = Darkest Forest; square = Dark
1369
+ Forest`. So the ring is --forest-950 (#002A24), the same ink as the wordmark and the tree's
1370
+ inner detail; the square is --forest-700 (#046C4C).
1371
+
1372
+ ⚠ `currentColor` IS THE DECK'S REVERSING TREATMENT, NOT THE MARK'S COLOUR, and reading it back
1373
+ off the page says the wrong thing: on this deck it resolves to --deck-fg, which is
1374
+ --neutral-900 — a neutral near-black that merely looks like the answer on cream. Pinning that
1375
+ would have shipped a mark a shade off the artwork on every slide. The token is named from the
1376
+ system instead.
1377
+
1378
+ ★ ONE GROUND OVERRIDES ALL OF THAT, and `--mosaic-ink` is where it is decided rather than here —
1379
+ see `onDarkestGround` in the script. The pin is what stops the field following every ground it
1380
+ lands on; the single exception is the ground the pin would disappear into.
1381
+ */
1382
+ color: var(--mosaic-ink);
1383
+ }
1384
+ .cover-mosaic > * {
1385
+ width: 100%;
1386
+ height: auto;
1387
+ aspect-ratio: 1;
1388
+ display: block;
1389
+ }
1390
+ /* A picture in the mark's clothing: the ring as padding, so it scales with the tile the grid hands
1391
+ it, and the fill is the ground's own ink — the same colour the mark's ring takes. */
1392
+ .cover-mosaic-cell {
1393
+ background: currentColor;
1394
+ border-radius: var(--mosaic-radius);
1395
+ padding: var(--mosaic-ring);
1396
+ }
1397
+ .cover-mosaic-cell :deep(img),
1398
+ .cover-mosaic-cell :deep(.deck-placeholder) {
1399
+ width: 100%;
1400
+ height: 100%;
1401
+ object-fit: cover;
1402
+ border-radius: var(--mosaic-radius-inner);
1403
+ display: block;
1404
+ }
1405
+ /*
1406
+ The wedge, in two declarations. Auto-placement fills row 1 with cells 1-4; starting cells 5, 8
1407
+ and 10 one column further in indents each following row by one tile, so the rows step in from the
1408
+ left while their right edges stay flush against the frame.
1409
+
1410
+ ★ NO STAGGER HERE, and that is the point of the shape. A staggered rectangle was the previous
1411
+ arrangement's way of not reading as a table — a nudge to break the rows up. The wedge already has
1412
+ a direction, so an offset on top of it would only blur the diagonal it is made of.
1413
+ */
1414
+ .cover-mosaic > :nth-child(5) { grid-column-start: 2; }
1415
+ .cover-mosaic > :nth-child(8) { grid-column-start: 3; }
1416
+ .cover-mosaic > :nth-child(10) { grid-column-start: 4; }
1417
+ /*
1418
+ The mark's cell. A plate of the ground's OWN INK with the glyph knocked out of it in the ground's
1419
+ fill — so it is the same two colours as the words beside it, inverted, on all ten grounds and
1420
+ without a rule per ground.
1421
+ */
1422
+
1423
+ /*
1424
+ ⚠ AND THE MARK DOES NOT REVERSE HERE, WHICH IS AN EXCEPTION AND HAS TO OUT-SPECIFY ONE.
1425
+
1426
+ Logo.vue pins the brand ink and flips it to --neutral-0 under `.deck-dark`, which is right
1427
+ everywhere the mark signs a slide — the chrome, the cover lockup, the end card. It is wrong for
1428
+ THIS grid: the nine tiles are one object drawn in one ink, so a reversing mark put a white ring on
1429
+ the centre tile and forest-950 rings on the eight around it. Measured on the dark mosaic, which is
1430
+ the variant's own default ground.
1431
+
1432
+ Three class selectors so it beats Logo.vue's own two — a tie would be settled by whichever
1433
+ stylesheet the bundler emitted second, which is not a thing this should depend on.
1434
+ */
1435
+ .cover .cover-mosaic .cover-mosaic-mark { color: var(--mosaic-ink); }
1436
+ /*
1437
+ ⚠ THE INTERVAL BELOW THE WORDMARK BELONGS TO THE TAIL, NOT TO THE WORDMARK. As a margin it sat
1438
+ INSIDE the grid's `auto` row — the row measured 190px for a 166px wordmark — so the row centred and
1439
+ the wordmark inside it did not. Measured: 528 against the canvas's 540, twelve pixels high, which
1440
+ is exactly half the margin. Moved onto `.cover-body`, where it separates the two boxes without
1441
+ being weighed as part of either.
1442
+ */
1443
+ /* ★ ONE STEP UP THE LADDER, NOT A TUNED NUMBER. At --deck-space-3 (24) the gap was set against a
1444
+ 697px wordmark; at 813 the mark grew and the interval did not, so the badge read as attached to
1445
+ it rather than placed under it. --deck-space-4 (32) is the next rung. */
1446
+ .cover.is-mosaic .cover-body { margin-top: var(--deck-space-4); }
1447
+ /*
1448
+ ── THE CERTIFICATION SITS IN THE BOTTOM-LEFT CORNER ───────────────────────────────────────────
1449
+
1450
+ ★★ AND IT CLEARS THE ONE PIECE OF CHROME A COVER CAN STILL CARRY. Every other corner of a slide
1451
+ belongs to the running chrome, and a cover suppresses all of it (CHROMELESS_LAYOUTS) — except the
1452
+ confidentiality marking, which slide-bottom.vue deliberately exempts from the page-1 gate so a
1453
+ title slide can still be marked. That pill is bottom-LEFT, in the chrome band at
1454
+ --deck-inset-chrome.
1455
+
1456
+ Measured live on slide 221, the one slide in the book that carries a marking: the band occupies
1457
+ y 995-1044 and the pill itself y 1013-1044. At --deck-inset-text (96) from the bottom the mark
1458
+ ends on 984 — eleven pixels clear of the band, twenty-nine of the pill — which is why the vertical
1459
+ inset is that one and not --deck-space-6 (60), where the mark would end at 1020, inside both.
1460
+
1461
+ ★ THE TWO INSETS DIFFER ON PURPOSE. Vertically it is the text inset, for the clearance above;
1462
+ horizontally it is --deck-inset-display, because that is where the type column starts and the
1463
+ corner should read as the foot of that column rather than as a fifth thing on the slide.
1464
+
1465
+ ⚠ AND IT IS ABSOLUTE, SO A LONG ENOUGH TITLE BLOCK WILL REACH IT. The display row centres against
1466
+ the whole slide (see the grid above), so it grows in both directions and the corner mark does not
1467
+ move out of the way. MEASURED at the specimen's own longest slide — a two-line title, a kicker and
1468
+ a two-line lead — the block ends at 788 against a mark that begins at 848: sixty pixels, or about
1469
+ half a display line. A THIRD display line reaches it. Reserving the space inside the column would
1470
+ move the centre line the wordmark is aligned to, which is the thing this variant is built around,
1471
+ so the ceiling is recorded here rather than designed away.
1472
+ */
1473
+ /*
1474
+ ★ THE CREDENTIAL IS SIZED AGAINST THE WORDMARK ABOVE IT. At 104px it was 62.6px wide beside a
1475
+ 697px wordmark — a ratio of about 1:11, which is not "quiet", it is unresolved, and it sat 40px
1476
+ under the subtitle where the eye read it as a fourth line of the paragraph. Larger, and on the
1477
+ block's own biggest interval, so it reads as the last element of the title block rather than as
1478
+ something left over.
1479
+ */
1480
+ /*
1481
+ ★ THE PILL IS LEFT WHERE THE TYPE IS, not stretched across the column. `.badge-strip` is a flex
1482
+ ROW — correct for the strip's own job, a list of peers — and as a block-level child of a column it
1483
+ would take the column's full width, putting a single pill's box 899px wide with 300px of nothing
1484
+ after it. `width: fit-content` makes the box the pill.
1485
+ */
1486
+ .cover-badge { width: fit-content; }
1487
+ /* As a kicker it needs the interval a kicker has; as a strapline the body's own top margin already
1488
+ separates it from the wordmark, so only this position sets one. */
1489
+ .cover-title .cover-badge { margin-bottom: var(--deck-space-3); }
1490
+ /*
1491
+ ⚠ AND THE STRIP'S 20em CAP COMES OFF, because it is measuring for a job this pill is not doing.
1492
+ BadgeStrip caps a label and ellipsises it so ONE long tag cannot push a row of peers off the
1493
+ slide — the right call for a row. A cover badge has no peers and no row: it is a strapline, and
1494
+ the column it sits in is what bounds it. Left capped, "The bikes that look like trees" rendered as
1495
+ "THE BIKES THAT LOOK LIKE TR…" at exactly 600px — measured, the cap to the pixel.
1496
+
1497
+ Only the cap. The ellipsis stays armed for a strapline longer than the column, which is the case
1498
+ the cap was protecting against in the first place.
1499
+ */
1500
+ .cover-badge :deep(.badge-strip-item) { max-width: none; }
1501
+ .cover-endorsement {
1502
+ position: absolute;
1503
+ left: var(--deck-inset-display);
1504
+ bottom: var(--deck-inset-text);
1505
+ z-index: 10;
1506
+ }
1507
+ /*
1508
+ ★ THE TITLE TAKES THE COVER'S OWN DISPLAY STEP, unmodified — this variant used to DEMOTE an h1 to
1509
+ subtitle size, back when the wordmark always held the display line and a heading could only be
1510
+ something competing with it. Now a heading IS the display line, so it reads at 128px Mohr
1511
+ uppercase like every other cover's, and the column's own width is what bounds it.
1512
+ */
1513
+
1514
+ </style>