@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.
- package/LICENSE +67 -0
- package/README.md +577 -0
- package/assetUrl.js +38 -0
- package/components/Alert.vue +209 -0
- package/components/AreaBox.vue +152 -0
- package/components/AreaChart.vue +145 -0
- package/components/Avatar.vue +267 -0
- package/components/AvatarGroup.vue +94 -0
- package/components/BadgeStrip.vue +203 -0
- package/components/Bars.vue +207 -0
- package/components/BoroughShape.vue +127 -0
- package/components/BrowserFrame.vue +484 -0
- package/components/CalendarGrid.vue +480 -0
- package/components/Card.vue +70 -0
- package/components/Carousel.vue +266 -0
- package/components/ChartFrame.vue +426 -0
- package/components/ChordChart.vue +264 -0
- package/components/ComposedChart.vue +187 -0
- package/components/Contents.vue +86 -0
- package/components/DataTable.vue +244 -0
- package/components/DeckMap.vue +409 -0
- package/components/DeviceFrame.vue +331 -0
- package/components/DonutChart.vue +103 -0
- package/components/DottedMap.vue +126 -0
- package/components/Endorsement.vue +76 -0
- package/components/Grid.vue +169 -0
- package/components/GroupedBarChart.vue +141 -0
- package/components/IconTile.vue +116 -0
- package/components/Kpi.vue +188 -0
- package/components/LineChart.vue +133 -0
- package/components/Logo.vue +203 -0
- package/components/LondonMap.vue +348 -0
- package/components/MarkPair.vue +109 -0
- package/components/Marquee.vue +263 -0
- package/components/NestedDonutChart.vue +295 -0
- package/components/Placeholder.vue +88 -0
- package/components/SankeyChart.vue +223 -0
- package/components/ScatterChart.vue +142 -0
- package/components/StackedBarChart.vue +143 -0
- package/components/StatCard.vue +134 -0
- package/components/StatusTrack.vue +334 -0
- package/components/Timeline.vue +249 -0
- package/components/TimelineChart.vue +329 -0
- package/components/TreemapChart.vue +267 -0
- package/components/backgrounds.js +135 -0
- package/components/boroughGeometry.ts +59 -0
- package/components/chartData.ts +277 -0
- package/components/chartTip.ts +201 -0
- package/components/codeTheme.ts +239 -0
- package/components/deckPalette.ts +157 -0
- package/components/dottedMapGeometry.js +60 -0
- package/components/iconSlugs.js +55 -0
- package/components/iconUrl.js +51 -0
- package/components/mermaidTheme.ts +972 -0
- package/components/motionDuration.ts +41 -0
- package/components/tableRules.ts +257 -0
- package/confidential-mark.vue +151 -0
- package/custom-nav-controls.vue +79 -0
- package/endorsements/b-corp-black.svg +30 -0
- package/endorsements/b-corp-white.svg +30 -0
- package/global-top.vue +1662 -0
- package/icons/forest/bike-asterisk.svg +21 -0
- package/icons/forest/bike-down.svg +19 -0
- package/icons/forest/bike-off.svg +20 -0
- package/icons/forest/bike-up.svg +19 -0
- package/icons/forest/bike-x.svg +19 -0
- package/icons/forest/bike.svg +18 -0
- package/icons/forest/mark-fill.svg +6 -0
- package/icons/forest/mark.svg +6 -0
- package/icons/forest/parking.svg +3 -0
- package/icons/forest/star-fill.svg +4 -0
- package/icons/forest/star.svg +4 -0
- package/icons/forest/traffic-light-caution.svg +8 -0
- package/icons/forest/traffic-light-go.svg +8 -0
- package/icons/forest/traffic-light-lit.svg +8 -0
- package/icons/forest/traffic-light-stop.svg +8 -0
- package/icons/forest/traffic-light.svg +8 -0
- package/icons/lucide/battery.svg +1 -0
- package/icons/lucide/bike.svg +1 -0
- package/icons/lucide/chart-line.svg +1 -0
- package/icons/lucide/check.svg +1 -0
- package/icons/lucide/circle-check.svg +1 -0
- package/icons/lucide/clock.svg +1 -0
- package/icons/lucide/code.svg +1 -0
- package/icons/lucide/component.svg +1 -0
- package/icons/lucide/frame.svg +1 -0
- package/icons/lucide/layout-grid.svg +1 -0
- package/icons/lucide/map-pin.svg +1 -0
- package/icons/lucide/map.svg +1 -0
- package/icons/lucide/palette.svg +1 -0
- package/icons/lucide/table.svg +1 -0
- package/icons/lucide/triangle-alert.svg +1 -0
- package/icons/lucide/type.svg +1 -0
- package/icons/lucide/wrench.svg +1 -0
- package/layouts/bleed.vue +317 -0
- package/layouts/cover.vue +1514 -0
- package/layouts/default.vue +187 -0
- package/layouts/divider.vue +286 -0
- package/layouts/end.vue +384 -0
- package/layouts/quote.vue +218 -0
- package/layouts/split.vue +1875 -0
- package/layouts/stack.vue +944 -0
- package/layouts/statement.vue +67 -0
- package/manifest/chrome.ts +36 -0
- package/manifest/components.ts +49 -0
- package/manifest/index.ts +57 -0
- package/manifest/layouts.ts +37 -0
- package/manifest/manifest.json +6514 -0
- package/manifest/presets.ts +635 -0
- package/notch.js +458 -0
- package/notchPath.js +188 -0
- package/package.json +77 -0
- package/section.js +110 -0
- package/setup/mermaid-renderer.ts +2120 -0
- package/setup/mermaid.ts +30 -0
- package/setup/shiki.ts +44 -0
- package/setup/shortcuts.ts +28 -0
- package/slide-bottom.vue +305 -0
- package/slide-top.vue +268 -0
- package/styles/base.css +2611 -0
- package/styles/index.ts +71 -0
- package/styles/tokens.css +144 -0
- package/uno.config.ts +231 -0
- package/useNotchCuts.js +200 -0
package/styles/base.css
ADDED
|
@@ -0,0 +1,2611 @@
|
|
|
1
|
+
/* ───────────────────────────────────────────────────────────────────────────
|
|
2
|
+
Forest deck base — element styling for slide content.
|
|
3
|
+
|
|
4
|
+
Every value here resolves from styles/tokens.css. If a rule below wants a colour, a corner,
|
|
5
|
+
a duration or a size and there is no token for it, the token is the thing to add — not a
|
|
6
|
+
literal. That is the whole guardrail.
|
|
7
|
+
|
|
8
|
+
The type roles are the engine's, in the deck CONTEXT (--type-deck-*, generated by
|
|
9
|
+
scripts/type.ts). The mapping onto HTML is fixed here so an author writing plain markdown
|
|
10
|
+
lands on a role without knowing the role exists. Family/weight for every tier below come from
|
|
11
|
+
--type-deck-<role>-family/-weight, never a literal — that includes h1: it defaults to the
|
|
12
|
+
WORKING voice (GT Haptik Bold, sentence case — --type-deck-headline-family/-weight, text-
|
|
13
|
+
transform: none) so an ordinary content slide never has to ask for it. The four punctuation
|
|
14
|
+
layouts — divider, cover, statement, end — override their own :deep(h1) back to the BEAT voice
|
|
15
|
+
(Mohr Black uppercase — --type-deck-display-family/-weight), because that shout is a per-LAYOUT
|
|
16
|
+
choice, not a per-context one: see each layout's own <style> block. h2 is not layout-dependent —
|
|
17
|
+
it always speaks the GT Haptik shout, one tier down.
|
|
18
|
+
|
|
19
|
+
h1 display OR headline Mohr Black uppercase on the four beat layouts (divider/cover/
|
|
20
|
+
statement/end); GT Haptik bold sentence case everywhere else — the
|
|
21
|
+
slide's one shout, voice chosen by the layout
|
|
22
|
+
h2 headline GT Haptik bold, uppercase — a second shout, one tier down, always
|
|
23
|
+
h3 title GT Haptik bold, sentence case — the calm title under a shout
|
|
24
|
+
h4 heading-sm GT Haptik bold — a sub-head inside content
|
|
25
|
+
h5 label GT Haptik bold — the smallest labelled tier
|
|
26
|
+
h6 overline GT Haptik bold, uppercase, tracked — eyebrows and kickers
|
|
27
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
28
|
+
|
|
29
|
+
/* ── The slide ────────────────────────────────────────────────────────────────────────────── */
|
|
30
|
+
|
|
31
|
+
.slidev-layout {
|
|
32
|
+
font-family: var(--type-deck-body-family);
|
|
33
|
+
font-size: var(--type-deck-body-size); /* deck context: 2xl on the 1920×1080 canvas */
|
|
34
|
+
line-height: var(--type-deck-body-line-height);
|
|
35
|
+
color: var(--deck-fg);
|
|
36
|
+
background: var(--deck-bg);
|
|
37
|
+
/* ── The text inset — one value, all four edges ──────────────────────────────────────────
|
|
38
|
+
task-14-brief.md, superseding the asymmetric-then-symmetric-80/60 history this comment used
|
|
39
|
+
to record (see below for that history — it is worth keeping, because the defect it describes
|
|
40
|
+
is exactly what requirement 6's debug overlay exists to catch by eye instead of by memory).
|
|
41
|
+
|
|
42
|
+
THE PRINCIPLE: text is inset for its MEASURE, media for its FRAME. A paragraph's own ragged
|
|
43
|
+
right edge already supplies visual air a photograph's hard edge does not, so text and media
|
|
44
|
+
do not want the same margin — --deck-inset-text (90px = grid × 2.25) is text's; media reads
|
|
45
|
+
--deck-inset-media (36px) horizontally instead, in whichever layout places a figure against
|
|
46
|
+
an edge (split.vue, bleed.vue). Chrome (the mark, the page number, the section/confidentiality
|
|
47
|
+
pills) is a THIRD value, --deck-inset-chrome (36px) uniform on all four sides — furniture, not
|
|
48
|
+
content. All three derive from --deck-grid; see scripts/deck.ts's own comment above INSET for
|
|
49
|
+
the full reasoning, including why chrome and media share a number but not a meaning, and why
|
|
50
|
+
36 is deliberately off the space ladder (task-14b-brief.md revised these from the 40/40/100
|
|
51
|
+
first pass — the two-track PRINCIPLE didn't move, only the numbers).
|
|
52
|
+
|
|
53
|
+
slide-top.vue's/slide-bottom.vue's running chrome sits OUTSIDE this element (a sibling in
|
|
54
|
+
SlideWrapper.vue, not a descendant), so nothing here can see it or size around it dynamically
|
|
55
|
+
— but every layout that leaves this padding alone (all of them except bleed and split, which
|
|
56
|
+
each own their own vertical scheme for their own reasons) needs to leave room for it
|
|
57
|
+
regardless, because chrome is a per-SLIDE opt-out (`chrome: false`), not a per-LAYOUT one: any
|
|
58
|
+
layout can carry it.
|
|
59
|
+
|
|
60
|
+
⚠ RE-MEASURED, AND THE ARITHMETIC THIS COMMENT USED TO CARRY IS STALE. It read: "the chrome
|
|
61
|
+
pill band … occupying canvas y 36–85 at the top … --deck-inset-text (90px) clears that band by
|
|
62
|
+
5px at BOTH edges." That was true when the top band was a 49px pill row. It is not true now:
|
|
63
|
+
`0fd9b12` grew the running mark to 72px and nothing re-derived this. Measured live on every
|
|
64
|
+
chromed slide in all three decks:
|
|
65
|
+
|
|
66
|
+
.deck-chrome-mark 72 x 72, canvas y 36 → 108
|
|
67
|
+
.deck-chrome-pills 49 tall, centred on the mark, y 47.5 → 96.5
|
|
68
|
+
.deck-chrome-bottom 49 tall, y 995 → 1044
|
|
69
|
+
|
|
70
|
+
So 90px clears the BOTTOM band by 5px as claimed, and clears NEITHER top box: it under-clears
|
|
71
|
+
the pills by 6.5px and the mark by 18px. In practice no painted ink reaches the mark's own
|
|
72
|
+
square today, and the pill row overlaps only on the handful of slides whose layout starts a
|
|
73
|
+
heading at 90px rather than at `default`'s 130px — but the number below is no longer derived
|
|
74
|
+
from the band it is supposed to clear, and it should be treated as an OPEN geometry finding
|
|
75
|
+
rather than as a proof. It is left at 90 here deliberately: raising it moves every text box in
|
|
76
|
+
every deck, which is a re-typesetting decision, not a defect fix.
|
|
77
|
+
|
|
78
|
+
★ THE DEFECT THIS REPLACED, kept in prose because the LESSON outlives the numbers. This rule
|
|
79
|
+
used to be padding-TOP only, on the reasoning that "the confidentiality pill sits inside the
|
|
80
|
+
existing bottom margin already, not competing with it". That reasoning counted the pill's
|
|
81
|
+
OFFSET from the edge and forgot its HEIGHT — the pill is the same ~49px BadgeStrip band as the
|
|
82
|
+
top one, so it needed offset + height of room, not just offset, and the bottom margin was
|
|
83
|
+
short by 13px. scripts/deckAudit.ts could not catch it: the audit measures OVERFLOW past the
|
|
84
|
+
canvas, and an element sitting inside the content box, merely too close to another element,
|
|
85
|
+
overflows nothing. Collision is a different property from overflow, and nothing measured it —
|
|
86
|
+
which is why requirement 6's red-line overlay exists: it draws the chrome bands themselves, so
|
|
87
|
+
a collision like this one is visible by eye on the slide that has it, not provable only by
|
|
88
|
+
redoing this arithmetic by hand. */
|
|
89
|
+
padding: var(--deck-inset-text);
|
|
90
|
+
/*
|
|
91
|
+
★★ NOTHING LEAVES THE CANVAS BECAUSE IT COULD NOT FIND A BREAK.
|
|
92
|
+
|
|
93
|
+
Nothing in this theme set `overflow-wrap`, `word-break` or `hyphens` on anything — grepped, zero
|
|
94
|
+
hits — so an unbreakable run had no legal break point and simply painted out of its block. That
|
|
95
|
+
is not a hypothetical: deck-layouts slide 67's h1 paints from x=120 to x=6856.73, i.e. 4,937px
|
|
96
|
+
off the right of a 1920px canvas, with 63 of its 105 characters never reaching the screen and
|
|
97
|
+
the last visible one sliced through the middle.
|
|
98
|
+
|
|
99
|
+
It also renders as a CLEAN PASS in every geometry gate, and permanently will, because the gates
|
|
100
|
+
read `getBoundingClientRect()` on the ELEMENT: an overflowing inline run leaves the block's
|
|
101
|
+
border box at the column width, so the audit measures 120 → 1464 and reports "0 past the 90px
|
|
102
|
+
safe area" while the room sees the word run off the wall. A rule that cannot escape is worth
|
|
103
|
+
more here than a check that cannot see the escape.
|
|
104
|
+
|
|
105
|
+
`overflow-wrap` is INHERITED, so declaring it once on the layout root covers every heading,
|
|
106
|
+
paragraph, caption, table cell and chip on the slide — one rule rather than a list of selectors
|
|
107
|
+
that a new component can be added without joining.
|
|
108
|
+
|
|
109
|
+
`break-word`, not `anywhere`: both break a too-long word, but `anywhere` also feeds the break
|
|
110
|
+
opportunity into intrinsic (min-content) sizing, which would let every `auto`-sized grid track
|
|
111
|
+
and shrink-to-fit box in the theme collapse to one character. `break-word` changes only what
|
|
112
|
+
happens when a word genuinely does not fit.
|
|
113
|
+
|
|
114
|
+
It does not fight `white-space: nowrap` — a nowrap box (BadgeStrip's pills, .bars-label,
|
|
115
|
+
.bars-value) has no wrapping to do, so those keep their cap-and-ellipsis behaviour untouched.
|
|
116
|
+
*/
|
|
117
|
+
overflow-wrap: break-word;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── Headings ─────────────────────────────────────────────────────────────────────────────── */
|
|
121
|
+
|
|
122
|
+
/* h1 defaults to the WORKING voice — GT Haptik Bold, sentence case — so default/split/bleed (an
|
|
123
|
+
ordinary content slide) get the calm register with no CSS of their own. Size/line-height stay
|
|
124
|
+
on the display step deliberately (this task changes voice, not size): only family/weight/
|
|
125
|
+
transform move. The four beat layouts override this block's family/weight/transform back to
|
|
126
|
+
Mohr uppercase in their own <style> — see divider.vue/cover.vue/statement.vue/end.vue. */
|
|
127
|
+
.slidev-layout h1 {
|
|
128
|
+
font-family: var(--type-deck-headline-family);
|
|
129
|
+
font-weight: var(--type-deck-headline-weight);
|
|
130
|
+
font-size: var(--type-deck-display-size);
|
|
131
|
+
line-height: var(--type-deck-display-line-height);
|
|
132
|
+
letter-spacing: var(--type-deck-display-tracking);
|
|
133
|
+
text-transform: none;
|
|
134
|
+
color: var(--deck-fg);
|
|
135
|
+
text-wrap: balance;
|
|
136
|
+
/* The token, not a bare space step: the air above a title and the air below it are one decision,
|
|
137
|
+
and apps/deck budgets every paginated page from where content starts — so the number
|
|
138
|
+
has to be readable from deck.json rather than restated in two places. h2 keeps --deck-space-2:
|
|
139
|
+
it sits inside the content rather than starting it. */
|
|
140
|
+
margin: 0 0 var(--deck-heading-bottom-space) 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* h2 is not a per-layout choice — always the GT Haptik shout, one tier down. Its own weight/
|
|
144
|
+
tracking, not h1's: the two used to share one rule keyed to --type-deck-display-weight, which
|
|
145
|
+
was silently correct only because both tiers clamped to the same 700 under Task 11's blanket
|
|
146
|
+
override. Now that h1 can diverge (Mohr 900 on the beat layouts), sharing that rule would hand
|
|
147
|
+
h2 a weight its own family never declares. */
|
|
148
|
+
.slidev-layout h2 {
|
|
149
|
+
font-family: var(--type-deck-headline-family);
|
|
150
|
+
font-weight: var(--type-deck-headline-weight);
|
|
151
|
+
font-size: var(--type-deck-headline-size);
|
|
152
|
+
line-height: var(--type-deck-headline-line-height);
|
|
153
|
+
letter-spacing: var(--type-deck-headline-tracking);
|
|
154
|
+
text-transform: uppercase;
|
|
155
|
+
color: var(--deck-fg);
|
|
156
|
+
text-wrap: balance;
|
|
157
|
+
margin: 0 0 var(--deck-space-2) 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.slidev-layout h3 {
|
|
161
|
+
font-family: var(--type-deck-title-family);
|
|
162
|
+
font-weight: var(--type-deck-title-weight);
|
|
163
|
+
font-size: var(--type-deck-title-size);
|
|
164
|
+
line-height: var(--type-deck-title-line-height);
|
|
165
|
+
letter-spacing: var(--type-deck-title-tracking);
|
|
166
|
+
color: var(--deck-fg);
|
|
167
|
+
text-wrap: balance;
|
|
168
|
+
margin: 0 0 var(--deck-space-2) 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.slidev-layout h4 {
|
|
172
|
+
font-family: var(--type-deck-heading-sm-family);
|
|
173
|
+
font-weight: var(--type-deck-heading-sm-weight);
|
|
174
|
+
font-size: var(--type-deck-heading-sm-size);
|
|
175
|
+
line-height: var(--type-deck-heading-sm-line-height);
|
|
176
|
+
color: var(--deck-fg);
|
|
177
|
+
margin: 0 0 var(--deck-space-1) 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.slidev-layout h5 {
|
|
181
|
+
font-family: var(--type-deck-label-family);
|
|
182
|
+
font-weight: var(--type-deck-label-weight);
|
|
183
|
+
font-size: var(--type-deck-label-size);
|
|
184
|
+
line-height: var(--type-deck-label-line-height);
|
|
185
|
+
letter-spacing: var(--type-deck-label-tracking);
|
|
186
|
+
color: var(--deck-fg);
|
|
187
|
+
margin: 0 0 var(--deck-space-1) 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* The eyebrow's own vertical block — its line box plus the gap it holds to whatever follows it.
|
|
191
|
+
Published as a custom property, not left implicit in the two declarations below, because
|
|
192
|
+
split.vue has to hang an eyebrow ABOVE its anchor by exactly this much (see `.split-content >
|
|
193
|
+
.deck-eyebrow` there). A layout that needs to know how tall an eyebrow is must not restate 41px;
|
|
194
|
+
it reads the same arithmetic this rule is built from. */
|
|
195
|
+
/*
|
|
196
|
+
★★ THE OTHER HALF OF `<Grid fill>`, AND IT HAS TO LIVE OUT HERE.
|
|
197
|
+
|
|
198
|
+
A filling grid means "take the space that is left", which only has a meaning inside a container
|
|
199
|
+
that lays its children out along an axis. Grid.vue cannot say that: a child cannot make its own
|
|
200
|
+
parent a flex container, and its <style scoped> could not reach the layout root even if it could.
|
|
201
|
+
|
|
202
|
+
Keyed on `:has()` so it fires for the layouts that actually hold one and for nothing else — every
|
|
203
|
+
other slide in the deck keeps the block flow it has always had, which matters because turning a
|
|
204
|
+
block container into a flex one stops adjacent margins collapsing and would re-space every
|
|
205
|
+
paragraph on it. Direct child only (`>`): a grid nested inside some other box is that box's
|
|
206
|
+
problem to size, and silently re-flexing an ancestor two levels up would be a surprise.
|
|
207
|
+
|
|
208
|
+
The pair was measured both ways. With `height: 100%` on the grid and no rule here, a grid under an
|
|
209
|
+
h1 ran to y 1112 against a 984 safe line. With this rule and `flex: 1 1 0%`, the same slide gives
|
|
210
|
+
the heading 136 → 232 and the grid 264 → 984 exactly, and a grid with no heading still fills
|
|
211
|
+
136 → 984 as before.
|
|
212
|
+
*/
|
|
213
|
+
.slidev-layout:has(> .deck-grid.is-fill) {
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
★ A BARE FILLING CHART CLAIMS THE BODY THE SAME WAY A FILLING GRID DOES. `.deck-chart.is-fill`
|
|
220
|
+
asks for `height: 100%`, and a percentage resolves against a DEFINITE ancestor — under a heading
|
|
221
|
+
in normal flow there is none, so without the pair below the chart takes 100% of the LAYOUT and
|
|
222
|
+
overruns by exactly the heading's height. Measured: every XY chart slide ran to 1161 against a 984
|
|
223
|
+
safe line, an svg of 770 where 593 fits.
|
|
224
|
+
|
|
225
|
+
Its own rule rather than a comma on the grid's, because a static test pins that selector by exact
|
|
226
|
+
shape and a comma breaks it. Direct child only, for the reason the grid rule gives: a chart nested
|
|
227
|
+
inside some other box is that box's problem to size.
|
|
228
|
+
*/
|
|
229
|
+
.slidev-layout:has(> .deck-chart.is-fill),
|
|
230
|
+
.slidev-layout:has(> .deck-map.is-fill) {
|
|
231
|
+
display: flex;
|
|
232
|
+
flex-direction: column;
|
|
233
|
+
}
|
|
234
|
+
.slidev-layout > .deck-chart.is-fill,
|
|
235
|
+
.slidev-layout > .deck-map.is-fill {
|
|
236
|
+
flex: 1 1 0%;
|
|
237
|
+
min-height: 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/*
|
|
241
|
+
★ TWO GRIDS STACKED NEED THE AIR TWO CELLS SIDE BY SIDE GET. The dashboard shape is two grids as
|
|
242
|
+
siblings — a content-height row of KPIs, then a filling row for the chart — and they came out
|
|
243
|
+
TOUCHING: every gap inside each grid was 32, and the seam between them was 0, which reads as one
|
|
244
|
+
malformed grid rather than two.
|
|
245
|
+
|
|
246
|
+
Not `row-gap` on the flex column, which was the first thing tried: a flex gap ADDS to margins
|
|
247
|
+
rather than collapsing with them, and the h1 above already carries `margin-bottom: 32`, so the
|
|
248
|
+
heading would have been pushed to 64 while the seam went to 32. The adjacent-sibling selector
|
|
249
|
+
puts the space only where it is missing and leaves every other relationship alone.
|
|
250
|
+
|
|
251
|
+
`--deck-space-4` because that is what `gap: 'base'` resolves to — the seam between two grids is
|
|
252
|
+
the same beat as the gap inside one, which is the whole reason it looked wrong at 0.
|
|
253
|
+
*/
|
|
254
|
+
.deck-grid + .deck-grid {
|
|
255
|
+
margin-top: var(--deck-space-4);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/*
|
|
259
|
+
★★ A REGION HOLDING AN `::area-box` LAYS OUT AS A COLUMN, SO THE BOX CAN TAKE WHAT IS LEFT.
|
|
260
|
+
|
|
261
|
+
AreaBox fills its slot with `flex: 1 1 0%` and `align-self: stretch`, which covers a grid cell and
|
|
262
|
+
a flex column. It does not cover a BLOCK region that has something above the box: flex is inert
|
|
263
|
+
there, and the `height: 100%` this replaces resolved against the region's whole box rather than the
|
|
264
|
+
remainder, so the box started below its sibling and still claimed the full height.
|
|
265
|
+
|
|
266
|
+
Measured on the cover's area specimen: `.cover-content` 442-638 with `.cover-rule` plus margin
|
|
267
|
+
taking the first 40, and the box drawn 482-678 — forty pixels out of the bottom of the region it is
|
|
268
|
+
supposed to BE. The schematic ruler drew the region correctly and looked wrong for it.
|
|
269
|
+
|
|
270
|
+
Scoped with `:has(> .area-box)` so it touches only the specimen slides that draw regions as areas.
|
|
271
|
+
A real cover, quote or bleed keeps the block flow it has always had.
|
|
272
|
+
*/
|
|
273
|
+
.slidev-layout :is(.cover-content, .split-content, .stack-strip, .bleed-content, .quote-content):has(> .area-box),
|
|
274
|
+
/*
|
|
275
|
+
★ AND THE LAYOUT ROOT ITSELF, WHICH THE FIRST CUT OF THIS RULE MISSED.
|
|
276
|
+
|
|
277
|
+
`default` has no content region — its area box is a direct child of `.slidev-layout.default`,
|
|
278
|
+
which is `display: block`. So the list above (all of them content REGIONS) never reached it, flex
|
|
279
|
+
stayed inert, and with `height: 100%` gone the box fell to its own content height: measured 136-291,
|
|
280
|
+
155px inside an 888px frame, where every other area slide fills its region.
|
|
281
|
+
|
|
282
|
+
The beat layouts (divider, statement, end, quote) are already flex columns and match this harmlessly
|
|
283
|
+
— their boxes measured correct before and after. Only `default` needed it.
|
|
284
|
+
*/
|
|
285
|
+
.slidev-layout:has(> .area-box) {
|
|
286
|
+
display: flex;
|
|
287
|
+
flex-direction: column;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/*
|
|
291
|
+
★★ A BROWSER WINDOW IS TOLD HOW MUCH ROOM IT HAS, AND THAT CANNOT COME FROM THE COMPONENT.
|
|
292
|
+
|
|
293
|
+
BrowserFrame derives its screen's height from its WIDTH and its ratio, so a window with no `width`
|
|
294
|
+
on the 1728px content band is 1080 of screen plus a bar — the whole canvas and then some. It was
|
|
295
|
+
fixed caller-side, by passing a width computed against the room, which every future caller would
|
|
296
|
+
have had to repeat: deck-template slide 25 did not, and hung 367px off the bottom of the canvas
|
|
297
|
+
until scripts/deckAudit.ts's canvas pass could see it again.
|
|
298
|
+
|
|
299
|
+
The component now caps its own width against the room's HEIGHT — `100cqh` of `.deck-browser-room`,
|
|
300
|
+
minus what the bar and the border take, times the screen's ratio. Which needs the room to HAVE a
|
|
301
|
+
height, and a height is only definite where something outside supplies one. Hence this rule and
|
|
302
|
+
the three hosts below, and hence `container-type: size` living here rather than in the component:
|
|
303
|
+
a size container with no external height is size-contained down to ZERO, and the cap would then
|
|
304
|
+
make the window vanish. So it is switched on exactly where the height is known and left off
|
|
305
|
+
everywhere else, where the cap falls back to `none` and the window is width-driven as before.
|
|
306
|
+
|
|
307
|
+
Same shape and same reasoning as `<Grid fill>` and `.area-box` above — a child that means "take
|
|
308
|
+
what is left" needs its parent laid out along an axis, and a child cannot flex its own parent. And
|
|
309
|
+
same cost, stated there: the room GROWS, so a slide that puts something after the window will find
|
|
310
|
+
it pushed to the bottom of the frame. That is the established behaviour of a filling child in this
|
|
311
|
+
theme rather than a new surprise, and `>` keeps it to windows the layout holds directly — the
|
|
312
|
+
specimen's window inside a grid cell, with its caption underneath, is untouched.
|
|
313
|
+
*/
|
|
314
|
+
.slidev-layout:has(> .deck-browser-room) {
|
|
315
|
+
display: flex;
|
|
316
|
+
flex-direction: column;
|
|
317
|
+
}
|
|
318
|
+
/*
|
|
319
|
+
The three regions whose height is definite: a flex column's filling child (the layout root above,
|
|
320
|
+
split's figure cell, stack's panel band) takes it from `flex`, a block region sized by its grid row
|
|
321
|
+
(stack's other bands) from `height`. Both are declared because which one applies depends on the
|
|
322
|
+
host, and the one that does not apply is inert.
|
|
323
|
+
*/
|
|
324
|
+
.slidev-layout > .deck-browser-room,
|
|
325
|
+
.split-figure.is-figure > .deck-browser-room,
|
|
326
|
+
.stack-band > .deck-browser-room {
|
|
327
|
+
flex: 1 1 0%;
|
|
328
|
+
min-height: 0;
|
|
329
|
+
height: 100%;
|
|
330
|
+
container-type: size;
|
|
331
|
+
}
|
|
332
|
+
/*
|
|
333
|
+
★★ THE CAP IS SET ON THE CHILD, NOT ON THE ROOM, AND THAT IS A HARD REQUIREMENT RATHER THAN A
|
|
334
|
+
STYLE CHOICE. Declared on `.deck-browser-room` — as `--deck-browser-cap`, for the fit box to read
|
|
335
|
+
— the whole declaration came back EMPTY from getComputedStyle while every other declaration in the
|
|
336
|
+
same rule applied: a box asking its own size question is a cycle, and a cycle makes the value
|
|
337
|
+
invalid at computed-value time rather than an error anyone sees. Measured that way: the window
|
|
338
|
+
still rendered 1728x1142 in a room the room itself correctly reported as 679 tall.
|
|
339
|
+
|
|
340
|
+
One level down the same query is ordinary, so this is a plain `max-width` on the box it caps.
|
|
341
|
+
*/
|
|
342
|
+
.slidev-layout > .deck-browser-room > .deck-browser-fit,
|
|
343
|
+
.split-figure.is-figure > .deck-browser-room > .deck-browser-fit,
|
|
344
|
+
.stack-band > .deck-browser-room > .deck-browser-fit {
|
|
345
|
+
max-width: calc((100cqh - var(--deck-browser-frame-h)) * var(--deck-browser-screen-ratio));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/*
|
|
349
|
+
★★ THE HEADING RISES WHEN THE SECTION PILL IS NOT THERE — AND NARROWS, BECAUSE THE MARK STILL IS.
|
|
350
|
+
|
|
351
|
+
`default` holds its heading at --deck-inset-text + --deck-heading-top-space (96 + 40 = 136). With
|
|
352
|
+
a section pill in the top-left that reads correctly: the pill occupies y36-85 and the heading sits
|
|
353
|
+
51 below it. With no pill the same 136 reads as the title floating, because the left half of the
|
|
354
|
+
chrome row is empty.
|
|
355
|
+
|
|
356
|
+
★ WHAT THE 40 IS ACTUALLY FOR, which is the part that makes this more than a padding tweak. It is
|
|
357
|
+
not clearing the PILL. It is clearing the MARK, which is present either way:
|
|
358
|
+
|
|
359
|
+
mark y 36-108, x 1812-1884
|
|
360
|
+
heading band x 96-1824 → the two columns OVERLAP by 12px
|
|
361
|
+
at y136 clears the mark's bottom by 28
|
|
362
|
+
|
|
363
|
+
So dropping the space unconditionally would put a full-width heading under the running mark, on
|
|
364
|
+
every slide with no pill, and nothing would catch it — scripts/deckAudit.ts scores occlusion for
|
|
365
|
+
split/stack media surfaces only. The heading may only rise if it also stops short of the mark's
|
|
366
|
+
column, which is what the second rule does.
|
|
367
|
+
|
|
368
|
+
The reduction is derived, never written down:
|
|
369
|
+
|
|
370
|
+
max width = mark_left − gap − text_inset
|
|
371
|
+
= (canvas − chrome_inset − mark) − chrome_inset − text_inset
|
|
372
|
+
reduction = content_width − max_width
|
|
373
|
+
= mark + 2·chrome_inset − text_inset = 72 + 72 − 96 = 48px
|
|
374
|
+
|
|
375
|
+
48 of 1728, and the gap left beside the mark is one --deck-inset-chrome, the same clearance the
|
|
376
|
+
mark keeps from the canvas edge.
|
|
377
|
+
|
|
378
|
+
⚠ KEYED ON AN EMPTY PILL, NOT ON A MISSING ONE. slide-top.vue renders `.deck-chrome-pills`
|
|
379
|
+
unconditionally and puts the BadgeStrip inside it behind a `v-if`, so a slide with no section has
|
|
380
|
+
an element containing a single `<!--v-if-->` comment. `:empty` matches that (comments do not
|
|
381
|
+
defeat it, text nodes would) — verified in the browser rather than assumed, because the whole rule
|
|
382
|
+
is dead if it does not.
|
|
383
|
+
|
|
384
|
+
The second selector is the `chrome: false` case: no chrome row at all, so no mark to clear, so the
|
|
385
|
+
heading rises with NO cap. Written separately rather than folded in, because they want different
|
|
386
|
+
answers to the width question.
|
|
387
|
+
*/
|
|
388
|
+
.slidev-page:has(.deck-chrome-pills:empty) .slidev-layout.default:not(.is-center),
|
|
389
|
+
.slidev-page:not(:has(.deck-chrome-top)) .slidev-layout.default:not(.is-center) {
|
|
390
|
+
padding-top: var(--deck-inset-text);
|
|
391
|
+
}
|
|
392
|
+
/*
|
|
393
|
+
★★ `align: start` ON A SPLIT MUST CLEAR THE CHROME TOO — THE SAME PROBLEM, ONE LAYOUT OVER.
|
|
394
|
+
|
|
395
|
+
split's `align: start` collapses the anchor row so the content column starts at the grid box's own
|
|
396
|
+
top, which is the root's text inset: y 96. That is the right answer on a bare slide and the wrong
|
|
397
|
+
one under running chrome. Measured on the specimen's `align: start` rung, which carries a section
|
|
398
|
+
pill: heading at y 96, pill at y 36-85, an ELEVEN pixel gap, and the two overlap horizontally
|
|
399
|
+
(pill x 36-222, heading x 96-864). It reads as a title jammed under a label.
|
|
400
|
+
|
|
401
|
+
`default` already solves this with --deck-heading-top-space and clears its occupant by 28. This is
|
|
402
|
+
the same rule, and it is keyed on WHICH piece of chrome is actually above the column, because that
|
|
403
|
+
depends on which side the content is on:
|
|
404
|
+
|
|
405
|
+
· content on the RIGHT sits under the MARK, which is drawn whenever chrome is;
|
|
406
|
+
· content on the LEFT sits under the SECTION PILL, which is only there when a section is named —
|
|
407
|
+
so a pill-less slide keeps the tighter start rather than paying for clearance it does not need.
|
|
408
|
+
|
|
409
|
+
Same `:empty` trick as the default rule above, for the same reason: slide-top.vue always renders
|
|
410
|
+
the pills container and puts the badge inside behind a v-if.
|
|
411
|
+
*/
|
|
412
|
+
/*
|
|
413
|
+
★★ AND AN EYEBROW NEEDS THE RESERVE TO GROW, because it hangs ABOVE the thing the reserve protects.
|
|
414
|
+
|
|
415
|
+
The clearance below puts the HEADING at 136, clear of the pill. `.split-content`'s eyebrow rule
|
|
416
|
+
then pulls the eyebrow up by its own block so the heading keeps that anchor — which lands the
|
|
417
|
+
eyebrow at 95, back inside the exact 11px gap this whole rule was written to close. The heading was
|
|
418
|
+
protected and the thing above it was not.
|
|
419
|
+
|
|
420
|
+
So the reserve becomes the heading's PLUS the eyebrow's whenever the column carries one: the
|
|
421
|
+
eyebrow takes the line the heading would have had, and the heading sits its own block below,
|
|
422
|
+
anchored as before. Both keep their relationship; the pair just starts lower.
|
|
423
|
+
|
|
424
|
+
A variable rather than a second four-selector rule: the clearance below is already four selectors
|
|
425
|
+
because which chrome sits above the column depends on the side, and doubling that to eight to say
|
|
426
|
+
one thing about the eyebrow would bury the condition in the permutations.
|
|
427
|
+
*/
|
|
428
|
+
.split-grid:has(.deck-eyebrow) {
|
|
429
|
+
--split-chrome-reserve: calc(var(--deck-heading-top-space) + var(--deck-eyebrow-block));
|
|
430
|
+
}
|
|
431
|
+
.slidev-page:has(.deck-chrome-top) .slidev-layout.split.is-right.is-align-start .split-grid,
|
|
432
|
+
.slidev-page:has(.deck-chrome-top) .slidev-layout.split.is-right.is-align-body-center .split-grid,
|
|
433
|
+
.slidev-page:has(.deck-chrome-pills:not(:empty)) .slidev-layout.split.is-left.is-align-start .split-grid,
|
|
434
|
+
.slidev-page:has(.deck-chrome-pills:not(:empty)) .slidev-layout.split.is-left.is-align-body-center .split-grid {
|
|
435
|
+
grid-template-rows: var(--split-chrome-reserve, var(--deck-heading-top-space)) minmax(0, 1fr) auto;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.slidev-page:has(.deck-chrome-pills:empty) .slidev-layout.default:not(.is-center) > h1,
|
|
439
|
+
.slidev-page:has(.deck-chrome-pills:empty) .slidev-layout.default:not(.is-center) > hgroup {
|
|
440
|
+
max-width: calc(100% - (var(--deck-chrome-mark) + 2 * var(--deck-inset-chrome) - var(--deck-inset-text)));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/*
|
|
444
|
+
A CENTRED `default` CENTRES ITS CONTENT UNDER ITS EYEBROW, NOT WITH IT.
|
|
445
|
+
|
|
446
|
+
default.vue's `align: 'center'` makes the layout a flex column, and the eyebrow is a sibling of the
|
|
447
|
+
slot — so centring moved the running head along with whatever the slide held. Measured on
|
|
448
|
+
apps/deck's type-ramp pages: y 263, 203 and 251 on three consecutive pages of one
|
|
449
|
+
catalogue section. The kicker is a running head; it has to hold still.
|
|
450
|
+
|
|
451
|
+
Auto margins take the free space before `justify-content` sees any, so the slot block centres in
|
|
452
|
+
what is left below the eyebrow while the eyebrow keeps the top of the flow. Nothing is taken out
|
|
453
|
+
of flow, which matters: all four corners belong to the running chrome, and a layout that pins a box
|
|
454
|
+
in one has to ask `chromeShows()` first (deckNotch.static.test.ts). This asks nothing, because it
|
|
455
|
+
never leaves the column.
|
|
456
|
+
|
|
457
|
+
★ HERE RATHER THAN IN default.vue's OWN <style scoped>, because a scoped rule cannot see slot
|
|
458
|
+
content: Vue stamps the component's own elements with a data attribute and the slide's markdown
|
|
459
|
+
carries none, so `.deck-eyebrow + *` written there matches nothing at all. The eyebrow's own
|
|
460
|
+
margin-top stays in default.vue, where the element does belong to the component.
|
|
461
|
+
|
|
462
|
+
Guarded on the eyebrow existing — a centred slide without one is still centred by
|
|
463
|
+
`justify-content` alone, exactly as it was.
|
|
464
|
+
*/
|
|
465
|
+
.slidev-layout.default.is-center > .deck-eyebrow + * {
|
|
466
|
+
margin-top: auto;
|
|
467
|
+
}
|
|
468
|
+
.slidev-layout.default.is-center:has(> .deck-eyebrow) > :last-child {
|
|
469
|
+
margin-bottom: auto;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/*
|
|
473
|
+
AND `body-center` CENTRES UNDER THE HEADING, WHICH IS THE SAME RULE ONE ELEMENT FURTHER IN.
|
|
474
|
+
|
|
475
|
+
The case it exists for: a wide mermaid diagram is a few hundred units tall on a 1080 canvas, so
|
|
476
|
+
`align: start` leaves the bottom half of the slide empty under it, and `align: center` fixes the
|
|
477
|
+
emptiness by dragging the heading into the middle — which costs the running rhythm every other
|
|
478
|
+
content slide keeps. Neither is right for a body that is short because it is WIDE.
|
|
479
|
+
|
|
480
|
+
So the heading takes the top exactly as it does on a start-anchored slide, and the free space is
|
|
481
|
+
split above and below whatever follows it. Auto margins again, for the same reason as the eyebrow
|
|
482
|
+
above: they absorb the free space before `justify-content` sees any, and nothing leaves the flow,
|
|
483
|
+
so no corner is claimed and `chromeShows()` never has to be asked.
|
|
484
|
+
|
|
485
|
+
`hgroup` is named beside `h1` because default.vue's own eyebrow prop and a hand-written kicker
|
|
486
|
+
produce different markup for the same slide — the eyebrow is a sibling `<p>`, a written one is an
|
|
487
|
+
`<hgroup>` wrapping the heading. Both are "the heading block", and the body centres under either.
|
|
488
|
+
|
|
489
|
+
★ A SLIDE WITH NO HEADING FALLS BACK TO `start`, by construction rather than by a guard: the
|
|
490
|
+
sibling selector matches nothing and `:has()` is false, so no auto margin is handed out and the
|
|
491
|
+
column stays top-anchored. That is the right answer — there is no heading to hold the top, so
|
|
492
|
+
there is nothing to centre underneath.
|
|
493
|
+
*/
|
|
494
|
+
/*
|
|
495
|
+
★★ `.slidev-page` IS A SPECIFICITY COUNTERWEIGHT, NOT A SCOPE — it is here to win one exact tie.
|
|
496
|
+
|
|
497
|
+
A chart slide leads with a spec label, and that paragraph is styled by
|
|
498
|
+
`.slidev-layout p:has(> .deck-spec-label:only-child)`, which sets `margin: 0 0 … 0`. Its
|
|
499
|
+
specificity is (0,3,1): `:has()` contributes its own argument's, and `.deck-spec-label:only-child`
|
|
500
|
+
is a class plus a pseudo-class. Written without the ancestor, the rule below is (0,3,1) too — an
|
|
501
|
+
exact tie, decided by source order, and the spec-label rule is further down this file. So the
|
|
502
|
+
paragraph kept `margin-top: 0`, the auto never applied, and the free space went entirely below the
|
|
503
|
+
body: the slide still hugged the top while reporting `is-body-center` correctly.
|
|
504
|
+
|
|
505
|
+
MEASURED on the specimen's Status track slide: two autos live instead of three, 103.5px each below
|
|
506
|
+
the chart and nothing above the label. Setting the same margin inline made all three resolve to 69
|
|
507
|
+
and the block moved — which is how the tie was found rather than the selector blamed.
|
|
508
|
+
|
|
509
|
+
The mermaid slides never showed it: their first body element is a plain wrapper with no rule of
|
|
510
|
+
its own to tie against.
|
|
511
|
+
*/
|
|
512
|
+
.slidev-page .slidev-layout.default.is-body-center > :is(h1, hgroup) + * {
|
|
513
|
+
margin-top: auto;
|
|
514
|
+
}
|
|
515
|
+
.slidev-layout.default.is-body-center:has(> :is(h1, hgroup)) > :last-child {
|
|
516
|
+
margin-bottom: auto;
|
|
517
|
+
}
|
|
518
|
+
/*
|
|
519
|
+
★★ AND IT SITS ABOVE THE MIDDLE, BECAUSE THE MIDDLE LOOKS LOW.
|
|
520
|
+
|
|
521
|
+
Split evenly, the body is centred in a box the viewer cannot see — and the eye judges centring
|
|
522
|
+
against the FRAME. Measured on the specimen's Flowchart slide before this rule: the diagram's
|
|
523
|
+
centre landed 84px below the slide's own centre, because the heading occupies the top and the
|
|
524
|
+
split never knew. The two gaps a viewer actually perceives were 210 above and 274 below, off by
|
|
525
|
+
exactly the 64 that separates the heading's own 32px margin (which falls on the top side of the
|
|
526
|
+
split) from the container's 96px bottom inset (which falls on the bottom, and paints nothing).
|
|
527
|
+
|
|
528
|
+
⚠ THE WEIGHTING CANNOT BE A NUMBER, because the free space is not one — it is whatever the body
|
|
529
|
+
leaves, and it changes with every diagram. Flexbox divides free space EQUALLY between auto
|
|
530
|
+
margins, so the ratio comes from HOW MANY there are rather than from a length: two autos give 1:1,
|
|
531
|
+
and a third below gives 1:2. The body then takes a third of the space above it and two thirds
|
|
532
|
+
below, i.e. it rides one sixth of the free space above centre.
|
|
533
|
+
|
|
534
|
+
That is why the correction is a pseudo-element and not a `calc()`. It also makes the rule
|
|
535
|
+
self-limiting for free: as a body grows the free space shrinks, the lift shrinks with it, and a
|
|
536
|
+
body tall enough to fill the slide converges on the plain centre. Nothing to clamp.
|
|
537
|
+
|
|
538
|
+
★ GATED ON THE HEADING like the two rules above it, so a slide with no heading stays top-anchored
|
|
539
|
+
rather than gaining a lone auto margin with nothing to balance against.
|
|
540
|
+
*/
|
|
541
|
+
.slidev-layout.default.is-body-center:has(> :is(h1, hgroup))::after {
|
|
542
|
+
content: '';
|
|
543
|
+
margin-top: auto;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/*
|
|
547
|
+
★★ AND A THIRD TIME, ON stack's `frame: content` BAND — THE SAME 96 UNDER THE SAME CHROME.
|
|
548
|
+
|
|
549
|
+
Every other stack band is a SURFACE: `panel`, `photo` and `figure` all notch, so the band rises to
|
|
550
|
+
the 36 chrome inset and the measured cut in useNotchCuts.js takes the chrome out of its corners.
|
|
551
|
+
`frame: content` is the one that paints no ground, so it never notches, and nothing reserved
|
|
552
|
+
anything — it simply kept the plain text inset and started at 96.
|
|
553
|
+
|
|
554
|
+
Measured on the specimen's only such slide (45), and the numbers are the split rule's numbers
|
|
555
|
+
again almost exactly:
|
|
556
|
+
|
|
557
|
+
section pill y 36-85, x 36-222
|
|
558
|
+
band y 96, x 96-1824 → an ELEVEN pixel gap, overlapping horizontally
|
|
559
|
+
first text run y 94 → the painted line sits ABOVE its own box top
|
|
560
|
+
|
|
561
|
+
★ AND THE 40 IS CLEARING THE MARK, NOT THE PILL — same as `default`, and it matters here more,
|
|
562
|
+
because a band is FULL WIDTH:
|
|
563
|
+
|
|
564
|
+
mark y 36-108, x 1812-1884
|
|
565
|
+
band x 96-1824 → the two columns overlap by 12px
|
|
566
|
+
at 136 → clears the mark's bottom by 28
|
|
567
|
+
|
|
568
|
+
⚠ KEYED ON THE CHROME ROW, NOT ON A NON-EMPTY PILL, and that is the one place this rule departs
|
|
569
|
+
from `default`'s. `default` lets its heading rise to 96 on a pill-less slide and NARROWS it by 48
|
|
570
|
+
so it stops short of the mark's column. A band cannot take that trade: its full width IS the
|
|
571
|
+
layout's statement — it is the thing distinguishing a band from a paragraph — so narrowing it to
|
|
572
|
+
dodge the mark would change what the frame means. The mark is present whenever the chrome row is,
|
|
573
|
+
so the clearance is owed whenever the chrome row is, pill or no pill. A `chrome: false` slide has
|
|
574
|
+
no mark to clear and keeps the tighter 96 from stack.vue's own rule.
|
|
575
|
+
*/
|
|
576
|
+
.slidev-page:has(.deck-chrome-top) .slidev-layout.stack .stack-band.is-content {
|
|
577
|
+
margin-top: calc(var(--deck-inset-text) + var(--deck-heading-top-space));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/*
|
|
581
|
+
★★ A BLEEDING SURFACE HAS NO NOTCH, SO ITS CONTENT MUST CLEAR THE MARK ON ITS OWN.
|
|
582
|
+
|
|
583
|
+
Everywhere else the ring of ground around a figure is where the running chrome sits, and the notch
|
|
584
|
+
cuts it a seat. `bleed` gives up that ring on purpose, so the chrome is drawn ON the surface — which
|
|
585
|
+
is the documented trade for the SURFACE, and not a licence for the CONTENT to collide with it.
|
|
586
|
+
|
|
587
|
+
Measured on the specimen's bleeding panel band before this rule: the content's padding box began at
|
|
588
|
+
y 32, ABOVE the mark's own top at 36 and 76px above its bottom. The unbled band puts the same
|
|
589
|
+
content at 132, and the arithmetic behind that number is not the card padding — it is the notch:
|
|
590
|
+
|
|
591
|
+
chrome inset 36 where the mark's box starts
|
|
592
|
+
mark 72 its height
|
|
593
|
+
notch gap 24 the ring the cut leaves around it
|
|
594
|
+
---
|
|
595
|
+
132 which is exactly where a notched panel starts its content
|
|
596
|
+
|
|
597
|
+
So the bleeding surface reproduces it from the same three tokens rather than restating 132, and the
|
|
598
|
+
two move together if any of them is ever retuned.
|
|
599
|
+
|
|
600
|
+
⚠ KEYED ON THE CHROME ROW, like the two rules above it. With `chrome: false` there is no mark to
|
|
601
|
+
clear and the surface keeps the tighter top the layouts set — a bleeding slide with no chrome would
|
|
602
|
+
otherwise carry 132px of empty ground for a collision that cannot happen.
|
|
603
|
+
|
|
604
|
+
`photo` is excluded on purpose: an edge-to-edge photograph is the point of bleeding one, and its
|
|
605
|
+
content IS the image. That is the one case where the chrome really does sit on the picture, and the
|
|
606
|
+
answer there is `chrome: false` or the `bleed` layout's scrim — not padding.
|
|
607
|
+
|
|
608
|
+
★★ AND `spill` IS EXCLUDED, WHICH IS A DEFECT THIS RULE WAS CAUSING RATHER THAN A NEW OPTION.
|
|
609
|
+
stack.vue's own spill rule already says `padding-top: 0`, with a note recording exactly what this
|
|
610
|
+
reservation does to a spilling band: "measured, it started the window 132px below the band's own
|
|
611
|
+
top edge and the slide then cut a window that had never reached the top." That rule was losing.
|
|
612
|
+
`:has()` contributes its ARGUMENT's specificity, so this selector is (0,8,0) against the scoped
|
|
613
|
+
rule's (0,5,0) — the same trap this repo has hit before, and no amount of care inside stack.vue
|
|
614
|
+
fixes it because the two rules are not in the same file.
|
|
615
|
+
|
|
616
|
+
Excluded here rather than out-specified there, because the exclusion is the true statement: a
|
|
617
|
+
spilling band's content is meant to leave the frame, so it is not a surface the chrome can collide
|
|
618
|
+
with in the way this reservation assumes. Measured on the live-page spill slide — the band's own
|
|
619
|
+
top is y=300 and the chrome mark's bottom is y=108, so there was nothing to clear in the first
|
|
620
|
+
place.
|
|
621
|
+
*/
|
|
622
|
+
.slidev-page:has(.deck-chrome-top) .slidev-layout.split .split-figure.is-bleed:not(.is-content):not(.is-photo):not(.is-spill),
|
|
623
|
+
.slidev-page:has(.deck-chrome-top) .slidev-layout.stack .stack-band.is-bleed:not(.is-content):not(.is-photo):not(.is-spill) {
|
|
624
|
+
padding-top: calc(var(--deck-inset-chrome) + var(--deck-chrome-mark) + var(--deck-notch-gap));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.slidev-layout {
|
|
628
|
+
--deck-eyebrow-block: calc(
|
|
629
|
+
var(--type-deck-overline-size) * var(--type-deck-overline-line-height) + var(--deck-space-1)
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* h6 IS the overline role — the eyebrow tier. */
|
|
634
|
+
.slidev-layout h6,
|
|
635
|
+
.slidev-layout .deck-eyebrow {
|
|
636
|
+
font-family: var(--type-deck-overline-family);
|
|
637
|
+
font-weight: var(--type-deck-overline-weight);
|
|
638
|
+
font-size: var(--type-deck-overline-size);
|
|
639
|
+
line-height: var(--type-deck-overline-line-height);
|
|
640
|
+
letter-spacing: var(--type-deck-overline-tracking);
|
|
641
|
+
text-transform: uppercase;
|
|
642
|
+
color: var(--deck-accent);
|
|
643
|
+
margin: 0 0 var(--deck-space-1) 0;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/* ── The editorial hgroup — a heading with its kicker ──────────────────────────────────────
|
|
647
|
+
Same pattern the docs site runs (apps/docs/src/assets/docs.css): an <hgroup> is a heading
|
|
648
|
+
plus the line that introduces it, and a bare <p> inside one IS that kicker — so it takes the
|
|
649
|
+
overline role automatically. An author writes two lines of HTML and gets the system's
|
|
650
|
+
eyebrow rhythm; nothing has to be classed. */
|
|
651
|
+
.slidev-layout hgroup {
|
|
652
|
+
display: flex;
|
|
653
|
+
flex-direction: column;
|
|
654
|
+
gap: var(--deck-space-1);
|
|
655
|
+
margin-bottom: var(--deck-space-3);
|
|
656
|
+
}
|
|
657
|
+
.slidev-layout hgroup > * {
|
|
658
|
+
margin-bottom: 0;
|
|
659
|
+
}
|
|
660
|
+
.slidev-layout hgroup > p {
|
|
661
|
+
font-family: var(--type-deck-overline-family);
|
|
662
|
+
font-weight: var(--type-deck-overline-weight);
|
|
663
|
+
font-size: var(--type-deck-overline-size);
|
|
664
|
+
line-height: var(--type-deck-overline-line-height);
|
|
665
|
+
letter-spacing: var(--type-deck-overline-tracking);
|
|
666
|
+
text-transform: uppercase;
|
|
667
|
+
color: var(--deck-accent);
|
|
668
|
+
}
|
|
669
|
+
/* A kicker over the big display line wants more air than one over a section title. */
|
|
670
|
+
.slidev-layout hgroup:has(h1) {
|
|
671
|
+
gap: var(--deck-space-2);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/* ── Measure ──────────────────────────────────────────────────────────────────────────────
|
|
675
|
+
★★ RUNNING PROSE IS NOT CAPPED. A paragraph, a list item and a caption run the width of
|
|
676
|
+
whatever holds them — the slide's own text inset on a full-width layout, the column inside a
|
|
677
|
+
split or a stack band.
|
|
678
|
+
|
|
679
|
+
It was capped: body at 20em (≈39 characters), lists at 22em, captions at 23em. The case
|
|
680
|
+
against those numbers is not that they were wrong for reading — 39 characters is comfortably
|
|
681
|
+
inside the conventional 45–75 — it is that a SLIDE is not a page. The 45–75 finding is about
|
|
682
|
+
sustained reading, many lines and many return sweeps; a slide's paragraph is one to three
|
|
683
|
+
lines and is scanned. MEASURED on a full-width `default` carrying a heading, a paragraph, a
|
|
684
|
+
nested list, an ordered list and a blockquote:
|
|
685
|
+
|
|
686
|
+
capped at 39 chars paragraph 3 lines, blockquote 2 content ran to y 1079 — 95px PAST
|
|
687
|
+
the 984 inset
|
|
688
|
+
uncapped (70 chars) paragraph 2 lines, blockquote 1 the same content fits
|
|
689
|
+
|
|
690
|
+
That is the real cost, and it is the opposite of the one the cap was defending: capping trades
|
|
691
|
+
the dimension a slide has (width) for the dimension it runs out of (height). The right-hand
|
|
692
|
+
third it left empty was not composition either — nothing was placed there, it was leftover.
|
|
693
|
+
|
|
694
|
+
⚠ THE BEAT LAYOUTS KEEP THEIR CAPS, and they are a different instrument: cover's 12.5em title,
|
|
695
|
+
quote's 17em, statement's 14em are display type being SHAPED — a deliberate two- or three-line
|
|
696
|
+
block — not running text being made readable. They are unaffected by any of this.
|
|
697
|
+
|
|
698
|
+
★ AND THOSE CAPS ARE STILL IN `em`, NEVER `ch`, which is a brand-font fact rather than a
|
|
699
|
+
preference. MEASURED on the shipped faces:
|
|
700
|
+
|
|
701
|
+
GT Haptik 1ch = 0.778em average lowercase advance = 0.511em
|
|
702
|
+
MohrAlt 900 1ch = 0.789em average uppercase advance = 0.694em
|
|
703
|
+
|
|
704
|
+
The `ch` unit is the advance of "0", and in both faces that glyph is far wider than the average
|
|
705
|
+
letter — so a `max-width` in `ch` permits about 1.5× the characters it appears to promise.
|
|
706
|
+
`38ch` of GT Haptik is a 58-character line. Any cap added here takes <target characters> ×
|
|
707
|
+
<measured average advance>, using the uppercase figure for Mohr.
|
|
708
|
+
───────────────────────────────────────────────────────────────────────────────────────── */
|
|
709
|
+
|
|
710
|
+
/* ── Body copy ────────────────────────────────────────────────────────────────────────────── */
|
|
711
|
+
|
|
712
|
+
.slidev-layout p {
|
|
713
|
+
margin: 0 0 var(--deck-space-2) 0;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* The lead paragraph: the sans bridge directly under a Mohr shout. */
|
|
717
|
+
.slidev-layout .deck-lead {
|
|
718
|
+
font-size: var(--type-deck-subtitle-size);
|
|
719
|
+
line-height: var(--type-deck-subtitle-line-height);
|
|
720
|
+
color: var(--deck-fg-muted);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* Caption / footnote tier — the quiet line under a figure or at the foot of a slide. */
|
|
724
|
+
.slidev-layout figure {
|
|
725
|
+
margin: 0;
|
|
726
|
+
}
|
|
727
|
+
.slidev-layout figcaption,
|
|
728
|
+
.slidev-layout .deck-caption {
|
|
729
|
+
font-size: var(--type-deck-caption-size);
|
|
730
|
+
line-height: var(--type-deck-caption-line-height);
|
|
731
|
+
color: var(--deck-fg-muted);
|
|
732
|
+
margin-top: var(--deck-space-2);
|
|
733
|
+
}
|
|
734
|
+
.slidev-layout .deck-note {
|
|
735
|
+
font-size: var(--type-deck-body-sm-size);
|
|
736
|
+
color: var(--deck-fg-subtle);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/*
|
|
740
|
+
── A LABELLED SPECIMEN: a label, then the thing it names ────────────────────────────────────
|
|
741
|
+
`.deck-spec` marks a wrapper whose FIRST paragraph is the label for whatever follows it;
|
|
742
|
+
`.deck-spec-under` puts the label last instead, for swatches and bands where the colour leads.
|
|
743
|
+
|
|
744
|
+
The register is the OVERLINE role in muted ink — the same quiet furniture tier the running
|
|
745
|
+
chrome's own pills use. A label must never compete with what it labels, and on a colour
|
|
746
|
+
specimen it must not introduce a colour of its own. That is why this is not `.deck-caption`
|
|
747
|
+
(caption size, sentence case) and not `.deck-eyebrow` (overline, but the ACCENT): the gap
|
|
748
|
+
between those two is exactly this class, and it had no name until a deck needed one.
|
|
749
|
+
|
|
750
|
+
★ TWO WAYS IN, and which one a deck uses depends on whether there is anything to GROUP.
|
|
751
|
+
|
|
752
|
+
`.deck-spec-label` is the label carrying its own class, written `[text]{.deck-spec-label}`. That
|
|
753
|
+
needs comark, which Slidev 52 ships and a deck opts into with `comark: true` in its headmatter —
|
|
754
|
+
apps/deck does. On 0.50 it was not available at all: markdown could not attach a class,
|
|
755
|
+
and the alternative was a 250-character inline `style` on every label, 279 of them in one
|
|
756
|
+
generated deck, which is what made that file unreadable.
|
|
757
|
+
|
|
758
|
+
`.deck-spec` / `.deck-spec-under` keep the class on a WRAPPER, and they are not a legacy fallback:
|
|
759
|
+
they group a label AND the specimen it names, and the rule below is what positions one against the
|
|
760
|
+
other. A wrapper is the right shape when there are two things in it. It was the wrong shape when
|
|
761
|
+
there was one, which is the case the inline class now covers.
|
|
762
|
+
|
|
763
|
+
The coupling is a contract either way, which is why the rule names `.deck-spec` and never a bare
|
|
764
|
+
`div > p`.
|
|
765
|
+
|
|
766
|
+
★★ `.deck-spec-wide` IS THE OPT-IN, AND IT EXISTS BECAUSE A PANEL CENTRES ITS CHILDREN.
|
|
767
|
+
|
|
768
|
+
`panelAlign: title-start|title-center` lays the panel out as a column with `align-items: center`,
|
|
769
|
+
so every direct child SHRINK-WRAPS and is centred on the panel's midline. On the panelAlign ladder
|
|
770
|
+
that is exactly right — the rungs must shrink-wrap so the ladder moves one variable — but on a
|
|
771
|
+
register PAIR it is wrong twice over: the specimen in the panel is a different width AND a
|
|
772
|
+
different alignment from its partner in the content column, so a reader comparing two registers is
|
|
773
|
+
also comparing two layouts. Measured on the unfilled-badges slide: the column half ran 768px wide
|
|
774
|
+
from x 96, the panel half 275px centred on 1440.
|
|
775
|
+
|
|
776
|
+
A half that needs its full width asks for it, per split.vue's own note, rather than the panel
|
|
777
|
+
handing it out to everyone.
|
|
778
|
+
*/
|
|
779
|
+
.deck-spec-wide {
|
|
780
|
+
align-self: stretch;
|
|
781
|
+
}
|
|
782
|
+
/*
|
|
783
|
+
★ THE PARAGRAPH AROUND AN INLINE LABEL HAS TO TURN WITH IT. `[text]{.deck-spec-label}` on its own
|
|
784
|
+
line is a SPAN inside a <p>, and a line box is at least as tall as its parent's strut — so the
|
|
785
|
+
paragraph's body line-height won, the label's block came out taller than the overline it is set
|
|
786
|
+
in, and every chart budget derived from CAPTION_BLOCK (49px = a 33px overline line box plus
|
|
787
|
+
--deck-space-2) was short. MEASURED: charts to bottom 1098 on a 1080 canvas, 18px over, across 20
|
|
788
|
+
slides. The wrapper forms never had this — there the label IS the paragraph.
|
|
789
|
+
*/
|
|
790
|
+
.slidev-layout p:has(> .deck-spec-label:only-child) {
|
|
791
|
+
/* ★ FONT-SIZE AS WELL AS LINE-HEIGHT, because the overline's line-height is UNITLESS (1.1) and a
|
|
792
|
+
unitless value multiplies the element's OWN font-size. Setting only line-height on the wrapping
|
|
793
|
+
paragraph computed 1.1 against the paragraph's 48px body size — 52.8px, a 69px block against
|
|
794
|
+
the 49px CAPTION_BLOCK every chart budget is derived from. With both, the paragraph's strut is
|
|
795
|
+
the label's own line box: 30 x 1.1 = 33, plus --deck-space-2, exactly 49. */
|
|
796
|
+
font-size: var(--type-deck-overline-size);
|
|
797
|
+
line-height: var(--type-deck-overline-line-height);
|
|
798
|
+
margin: 0 0 var(--deck-space-2) 0;
|
|
799
|
+
max-width: none;
|
|
800
|
+
}
|
|
801
|
+
.slidev-layout .deck-spec > p:first-child,
|
|
802
|
+
.slidev-layout .deck-spec-under > p:last-child,
|
|
803
|
+
.slidev-layout .deck-spec-label {
|
|
804
|
+
font-family: var(--type-deck-overline-family);
|
|
805
|
+
font-size: var(--type-deck-overline-size);
|
|
806
|
+
font-weight: var(--type-deck-overline-weight);
|
|
807
|
+
line-height: var(--type-deck-overline-line-height);
|
|
808
|
+
letter-spacing: var(--type-deck-overline-tracking);
|
|
809
|
+
text-transform: uppercase;
|
|
810
|
+
color: var(--deck-fg-muted);
|
|
811
|
+
max-width: none;
|
|
812
|
+
}
|
|
813
|
+
.slidev-layout .deck-spec > p:first-child { margin: 0 0 var(--deck-space-2) 0; }
|
|
814
|
+
/*
|
|
815
|
+
★★ AN IDENTIFIER INSIDE A LABEL KEEPS ITS OWN CASE, and on a custom property that is correctness
|
|
816
|
+
rather than taste. The label register is an uppercase overline, which is right for the prose half
|
|
817
|
+
of a caption ("light register", "three cards, two clicks") and WRONG for a token: CSS custom
|
|
818
|
+
property names are case-sensitive, so `--DECK-SHADE-1` is a different property from the one the
|
|
819
|
+
swatch beside it is actually painting. A specimen book that misspells the thing it specimens is
|
|
820
|
+
worse than one with no label.
|
|
821
|
+
|
|
822
|
+
So a label says which half is which by writing the identifier as inline code, and this turns the
|
|
823
|
+
overline treatment off for that span only — case, tracking and family all come back. Sized in `em`
|
|
824
|
+
so it tracks whatever the label is set at rather than needing its own number, and left on the
|
|
825
|
+
label's own muted ink rather than the code chip's ground: a chip fill inside a 33px line box would
|
|
826
|
+
make the label a row of boxes.
|
|
827
|
+
|
|
828
|
+
★★ AND IT MUST TURN OFF ALL OF THE CHIP, NOT JUST THE FILL. This zeroed `background` and `padding`
|
|
829
|
+
and stopped there, which was complete until the chip grew an EDGE and a RADIUS. After that a token
|
|
830
|
+
name in a label drew as a hollow lozenge: no fill, a 2px currentColor rule hugging the text at
|
|
831
|
+
`padding: 0`, and 16px of radius on a box the zeroed padding had shrunk to 40 canvas px — 65% of
|
|
832
|
+
its own height, where the chip rule budgets that same 16px as two fifths of a 65px chip.
|
|
833
|
+
|
|
834
|
+
The lesson is the shape rather than the value: a context that neutralises the chip has to
|
|
835
|
+
neutralise every property the chip rule sets, and the two are 250 lines apart. A gate in
|
|
836
|
+
packages/slidev-theme/test/ now checks that they agree.
|
|
837
|
+
*/
|
|
838
|
+
.slidev-layout .deck-spec > p:first-child code,
|
|
839
|
+
.slidev-layout .deck-spec-under > p:last-child code,
|
|
840
|
+
.slidev-layout .deck-spec-label code {
|
|
841
|
+
font-family: var(--font-mono);
|
|
842
|
+
font-size: 0.92em;
|
|
843
|
+
font-weight: 400;
|
|
844
|
+
text-transform: none;
|
|
845
|
+
letter-spacing: normal;
|
|
846
|
+
background: none;
|
|
847
|
+
border: none;
|
|
848
|
+
border-radius: 0;
|
|
849
|
+
padding: 0;
|
|
850
|
+
color: inherit;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/*
|
|
854
|
+
★★ A CELL HANDS A FILLING CHART ITS LEFTOVER, NOT ITS WHOLE HEIGHT. `.deck-spec` is a label
|
|
855
|
+
paragraph followed by the specimen, and a filling frame asks for `height: 100%` — of the box the
|
|
856
|
+
label is also in. Measured on Legend positions with the grid filling and the rows stretching: the
|
|
857
|
+
grid's own box ended at the 984 safe line while the second row's FRAMES ran to 1066, 82px past it,
|
|
858
|
+
because each cell was paying for its label twice. The ink pass saw none of it; the text inside
|
|
859
|
+
those frames stopped at 747, so only the boxes crossed.
|
|
860
|
+
|
|
861
|
+
A flex column fixes it at the cell: the label takes its line, the chart takes the rest. `flex-basis`
|
|
862
|
+
of 0 is what makes the chart's own `height: 100%` stop mattering — a flex item sizes from its
|
|
863
|
+
basis, so the two no longer fight.
|
|
864
|
+
|
|
865
|
+
Scoped with `:has()` so a cell only becomes a flex column when it actually holds a filling chart.
|
|
866
|
+
Every other cell in the deck — swatches, bands, code, tables — is untouched.
|
|
867
|
+
*/
|
|
868
|
+
.slidev-layout .deck-spec:has(> .deck-chart.is-fill) {
|
|
869
|
+
display: flex;
|
|
870
|
+
flex-direction: column;
|
|
871
|
+
height: 100%;
|
|
872
|
+
}
|
|
873
|
+
.slidev-layout .deck-spec > .deck-chart.is-fill {
|
|
874
|
+
flex: 1 1 0%;
|
|
875
|
+
min-height: 0;
|
|
876
|
+
}
|
|
877
|
+
.slidev-layout .deck-spec-under > p:last-child { margin: var(--deck-space-2) 0 0 0; }
|
|
878
|
+
|
|
879
|
+
/* Emphasis is the brand green. */
|
|
880
|
+
.slidev-layout strong,
|
|
881
|
+
.slidev-layout b {
|
|
882
|
+
color: var(--deck-accent);
|
|
883
|
+
font-weight: 700;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/*
|
|
887
|
+
── Emphasis is TWO channels, and naming the second one is the point of this rule ──────────────
|
|
888
|
+
`strong` spends the ACCENT. `em` spends the OBLIQUE. On a deck where the accent is the loudest
|
|
889
|
+
thing on the page that distinction is worth having: an author needs a way to emphasise a word
|
|
890
|
+
without reaching for the one colour the whole slide is built around.
|
|
891
|
+
|
|
892
|
+
Colour and weight are INHERITED deliberately. An `em` that also changed colour would just be a
|
|
893
|
+
second `strong`, and the deck would have two ways to say one thing.
|
|
894
|
+
|
|
895
|
+
This rule is close to a no-op today — the browser default already resolves to the right face —
|
|
896
|
+
and that is exactly why it is written down. Italic worked by accident: nothing in this theme said
|
|
897
|
+
it was sanctioned, no other deck had ever used it, and the first one that did found the face
|
|
898
|
+
arriving late (see styles/index.ts). A treatment the system relies on should be a decision.
|
|
899
|
+
|
|
900
|
+
`font-synthesis-style: none` makes the face a guarantee rather than a coincidence. The brand
|
|
901
|
+
licenses and ships real obliques (GTHaptik-RegularOblique / -BoldOblique, declared
|
|
902
|
+
`font-style: italic`), so nothing should ever be slanting the roman by matrix. If the oblique
|
|
903
|
+
ever stops shipping, this renders upright — visibly wrong — instead of silently faking a face
|
|
904
|
+
that is not the brand's.
|
|
905
|
+
*/
|
|
906
|
+
.slidev-layout em,
|
|
907
|
+
.slidev-layout i {
|
|
908
|
+
font-style: italic;
|
|
909
|
+
font-synthesis-style: none;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/*
|
|
913
|
+
`.hl` WAS HERE — a highlighter mark, accent ink on accent-soft, with a negative margin so the tint
|
|
914
|
+
bled past the text and `box-decoration-break: clone` so a wrapped phrase got rounded ends on each
|
|
915
|
+
fragment. It worked; nothing ever used it. Zero occurrences in either deck, in the generators, in
|
|
916
|
+
any component or test, for the deck's whole life, and it carried no note explaining the decision —
|
|
917
|
+
the signature of a treatment added speculatively rather than because a slide asked for it.
|
|
918
|
+
|
|
919
|
+
Two things worth keeping if it ever comes back. `strong` already spends the accent (see the note
|
|
920
|
+
above `em`), so a highlight is a THIRD emphasis tier and needs a reason to exist beside the two
|
|
921
|
+
that do. And it must not be accent-on-accent-soft: that is the inline `code` chip's pairing, and
|
|
922
|
+
two meanings on one visual is a genuine ambiguity at projector distance.
|
|
923
|
+
*/
|
|
924
|
+
|
|
925
|
+
/* ── Lists — the accent bullet ────────────────────────────────────────────────────────────── */
|
|
926
|
+
|
|
927
|
+
/*
|
|
928
|
+
★ THE GAP BETWEEN ITEMS IS 0.3em, NOT 0.6, BECAUSE THE LINE BOX ALREADY CARRIES HALF OF IT.
|
|
929
|
+
Body sets `line-height: 1.6`, so a 48px item has a 77px line box — 29px of leading, half above the
|
|
930
|
+
glyph and half below. A 0.6em margin adds another 29px on top of that, and two consecutive items
|
|
931
|
+
ended up 58px apart against a 48px cap height: the list read as separate paragraphs rather than as
|
|
932
|
+
one list. Measured on apps/deck's prose slide, which is where it was reported. 0.3em puts
|
|
933
|
+
the total separation at about 43px — still clearly a break between items, no longer a stanza.
|
|
934
|
+
*/
|
|
935
|
+
.slidev-layout ul {
|
|
936
|
+
list-style: none;
|
|
937
|
+
padding-left: 0;
|
|
938
|
+
}
|
|
939
|
+
.slidev-layout ul > li {
|
|
940
|
+
position: relative;
|
|
941
|
+
padding-left: 1.6em;
|
|
942
|
+
margin-bottom: 0.3em;
|
|
943
|
+
}
|
|
944
|
+
.slidev-layout ul > li::before {
|
|
945
|
+
content: '';
|
|
946
|
+
position: absolute;
|
|
947
|
+
/*
|
|
948
|
+
★★ CENTRED ON THE LINE BOX, NOT ON A CONSTANT. This was `top: 0.62em`, which put the dot's
|
|
949
|
+
centre 0.87em below the item's top — measured on the specimen's own list, 41.8px against a
|
|
950
|
+
line-box centre of 34.8px, so it sat 7px low. The comment two rules up reasons from a body
|
|
951
|
+
line-height of 1.6 and the deck computes 1.45, which is how a hard-coded offset drifts: it is
|
|
952
|
+
independent of the thing it is supposed to centre against. `0.5lh - 0.25em` is half the line box
|
|
953
|
+
less half the dot, so it lands on the centre at any size and any tier and cannot drift again.
|
|
954
|
+
|
|
955
|
+
(For the record, 0.87em was almost exactly the X-HEIGHT centre, 40.8px — a real typographic
|
|
956
|
+
target, and the right one for lowercase-heavy text. Deck copy is sentence case and full of caps
|
|
957
|
+
and ascenders, so the eye reads the cap band instead and the dot looked low against it. Cap
|
|
958
|
+
centre is 36px, within 1.2px of the line box, so centring on the line box satisfies both.)
|
|
959
|
+
|
|
960
|
+
★ AND IT SITS 0.55em IN RATHER THAN ON THE MARGIN. At `left: 0` the dot's right edge was 1.1em
|
|
961
|
+
from the text — more than twice the dot's own width — so the pair read as two objects rather
|
|
962
|
+
than one marker. Moving the dot rather than the text closes it to 0.55em and leaves the text
|
|
963
|
+
column where it is, which matters because a list usually sits under a paragraph and the two
|
|
964
|
+
share a left edge. What it gives up is the hanging bullet, where the dot aligns with the
|
|
965
|
+
paragraph margin and the text is indented past it.
|
|
966
|
+
*/
|
|
967
|
+
left: 0.55em;
|
|
968
|
+
top: calc(0.5lh - 0.25em);
|
|
969
|
+
width: 0.5em;
|
|
970
|
+
height: 0.5em;
|
|
971
|
+
border-radius: var(--deck-radius-full);
|
|
972
|
+
background: var(--deck-accent);
|
|
973
|
+
}
|
|
974
|
+
/* `list-style-type: decimal` is not the browser default here — Slidev's own base styles (via
|
|
975
|
+
UnoCSS's Tailwind-style preflight) reset it to `none` on every list, the same normalisation
|
|
976
|
+
`ul` above deliberately keeps (it draws its own disc via `::before` instead of a native
|
|
977
|
+
marker). `ol` never got the equivalent restoration: found live, adding the first `<ol>` this
|
|
978
|
+
theme's own demo deck had ever rendered — `.slidev-layout ol > li::marker` two rules down has
|
|
979
|
+
existed since this file did, and had never had anything to colour, because `list-style-type:
|
|
980
|
+
none` means `::marker` generates no box at all regardless of what colours it. */
|
|
981
|
+
.slidev-layout ol {
|
|
982
|
+
list-style-type: decimal;
|
|
983
|
+
padding-left: 1.4em;
|
|
984
|
+
}
|
|
985
|
+
.slidev-layout ol > li {
|
|
986
|
+
margin-bottom: 0.3em;
|
|
987
|
+
}
|
|
988
|
+
.slidev-layout ol > li::marker {
|
|
989
|
+
color: var(--deck-accent);
|
|
990
|
+
font-weight: 700;
|
|
991
|
+
}
|
|
992
|
+
/*
|
|
993
|
+
── THE CONTENTS PAGE ──────────────────────────────────────────────────────────────────────────
|
|
994
|
+
|
|
995
|
+
Every section and subsection the deck declares, in five columns. It is an INDEX — scanned for one
|
|
996
|
+
word, not read — so the whole treatment is about making a section findable and its subsections
|
|
997
|
+
legible as belonging to it.
|
|
998
|
+
|
|
999
|
+
★★ THE TWO TIERS ARE RAMP RUNGS, AND DIFFER BY SIZE, WEIGHT, INDENT AND INK. This deck has two
|
|
1000
|
+
weights, so a hierarchy that needs a third of anything does not exist here: a section is
|
|
1001
|
+
`heading-sm` (48px, 700) and a subsection is `body-sm` (36px, 400) — two rungs the ramp already
|
|
1002
|
+
has, one bold and one not. Nothing is invented and nothing is a fraction of something else.
|
|
1003
|
+
|
|
1004
|
+
★★ GREEN IS THE SUBJECT, WARM IS THE CONNECTIVE TISSUE. The sections carry `--deck-accent`, which
|
|
1005
|
+
is what the deck already means by "this is a thing you can go to". The warm family is spent on the
|
|
1006
|
+
parts that are not text — the rule a section's children hang from, and the ground a row takes when
|
|
1007
|
+
it is under the pointer — so the page reads as one green list on paper rather than two colours
|
|
1008
|
+
competing down a column. `--deck-ink-warm` appears only on hover, where it is the only warm INK on
|
|
1009
|
+
the page and therefore unmistakably the thing being pointed at.
|
|
1010
|
+
|
|
1011
|
+
★ THE PROSE ROW MARGIN COMES OFF, AND IT IS WHAT MAKES THE PAGE FIT. `ol > li` above carries
|
|
1012
|
+
`margin-bottom: 0.3em` because consecutive prose items are separate thoughts; thirty-four of those
|
|
1013
|
+
is 250px. Measured against the 96px bottom inset the audit enforces: with the margin the page
|
|
1014
|
+
misses that line, without it it clears.
|
|
1015
|
+
*/
|
|
1016
|
+
.slidev-layout .deck-contents {
|
|
1017
|
+
column-count: 5;
|
|
1018
|
+
column-gap: var(--deck-space-4);
|
|
1019
|
+
}
|
|
1020
|
+
.slidev-layout .deck-contents ol {
|
|
1021
|
+
list-style: none;
|
|
1022
|
+
padding-left: 0;
|
|
1023
|
+
}
|
|
1024
|
+
/*
|
|
1025
|
+
⚠ NO BLANKET `margin-bottom: 0` HERE ANY MORE. One existed to cancel the prose row margin, and at
|
|
1026
|
+
(0,2,1) it also beat the per-tier gaps below at (0,2,0) — so the two rules that give this page its
|
|
1027
|
+
spacing silently did nothing and a wrapped row still read as two rows. Both tiers state their own
|
|
1028
|
+
gap instead, and each is (0,2,0) against the prose rule's (0,1,2), which they win on class count.
|
|
1029
|
+
*/
|
|
1030
|
+
/* ★★ THE ROW IS THE UNBREAKABLE UNIT, NOT THE GROUP — AND THE PAGE ONLY FITS BECAUSE OF IT. Holding
|
|
1031
|
+
a whole section together was the first answer (measured at three columns, a nested list broke away
|
|
1032
|
+
from its own section and opened the next column alone, reading as a top-level entry), but a group
|
|
1033
|
+
the balancer may not split sets a floor no column count can get under: held whole, the longest
|
|
1034
|
+
section is 670px of a 720px column, so four columns could pack at most 827 against that budget and
|
|
1035
|
+
the page overflowed by construction as the deck grew. A wrapped ROW still may not split. What
|
|
1036
|
+
keeps a continuation reading as subordinate is the warm rule and the indent below, which travel
|
|
1037
|
+
with the row rather than with the column. */
|
|
1038
|
+
.slidev-layout .deck-contents-section {
|
|
1039
|
+
margin-bottom: var(--deck-space-5);
|
|
1040
|
+
}
|
|
1041
|
+
/*
|
|
1042
|
+
★★ THREE GAPS, AND THEY HAVE TO BE IN THIS ORDER OR A WRAPPED ROW READS AS TWO ROWS. A subsection
|
|
1043
|
+
title long enough to wrap is the case that decides this page's spacing: with the ramp's own body-sm
|
|
1044
|
+
leading (1.5) and no gap between items, the space INSIDE a wrapped item was exactly the space
|
|
1045
|
+
BETWEEN two items, so "Device and browser / frames" read as two entries. Reported on sight.
|
|
1046
|
+
|
|
1047
|
+
So the three are separated deliberately, tightest first:
|
|
1048
|
+
|
|
1049
|
+
inside a wrapped row 1.2 line-height ~7px of leading
|
|
1050
|
+
between rows --deck-space-3 24px
|
|
1051
|
+
between sections --deck-space-5 40px
|
|
1052
|
+
|
|
1053
|
+
★★ AND THE TWO OUTER GAPS ARE SET BY THE SPACE LEFT OVER, not by the ladder's next rung. Five
|
|
1054
|
+
columns of rows that may wrap took the page from 54px past the canvas to 121px short of the text
|
|
1055
|
+
inset, and a list that stops two-thirds down a slide reads as a page that ran out rather than one
|
|
1056
|
+
that was composed. MEASURED on the longer of the two contents slides, list bottom against the 984
|
|
1057
|
+
inset: 16/24 ends at 863, 24/40 at 927, 32/40 at 959, 32/48 at 975. 24/40 spends the slack and
|
|
1058
|
+
keeps 57px in hand for a face that sets wider; 32/48 is inside the noise.
|
|
1059
|
+
|
|
1060
|
+
★ THE 1.2 IS NOT INVENTED — it is `--type-deck-label-line-height`, the ramp's own tight line box at
|
|
1061
|
+
this exact size (2.25rem). The ramp pairs that size with two leadings, 1.5 for running prose and
|
|
1062
|
+
1.2 for a label, and an index is the second thing rather than the first.
|
|
1063
|
+
|
|
1064
|
+
★ AND THE ROW IS WHERE `break-inside: avoid` LIVES, for the reason above: a row that wrapped across
|
|
1065
|
+
a column boundary would be the one thing on this page that genuinely cannot be read.
|
|
1066
|
+
*/
|
|
1067
|
+
.slidev-layout .deck-contents-subsection {
|
|
1068
|
+
break-inside: avoid;
|
|
1069
|
+
margin-bottom: var(--deck-space-3);
|
|
1070
|
+
}
|
|
1071
|
+
.slidev-layout .deck-contents-subsection:last-child {
|
|
1072
|
+
margin-bottom: 0;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.slidev-layout .deck-contents-link {
|
|
1076
|
+
/* The underline the theme gives every `a` is wrong on an index: thirty-four of them is a page of
|
|
1077
|
+
rules rather than a list of names. */
|
|
1078
|
+
text-decoration: none;
|
|
1079
|
+
transition: color var(--motion-fast) var(--motion-ease-out);
|
|
1080
|
+
}
|
|
1081
|
+
/*
|
|
1082
|
+
★★ THE SIZE AND THE LINE BOX ARE SET ON THE ROW, NOT ON THE LINK. The link is inline, and an
|
|
1083
|
+
inline element does not own its line box — the row's own strut does, so a `line-height` on the
|
|
1084
|
+
`<a>` was simply outvoted by the `li`'s inherited 1.45 and every row stood a third taller than it
|
|
1085
|
+
was measured to. The page overflowed its bottom inset by 41px on exactly that.
|
|
1086
|
+
*/
|
|
1087
|
+
.slidev-layout .deck-contents-section > .deck-contents-link.is-section,
|
|
1088
|
+
.slidev-layout li.deck-contents-section {
|
|
1089
|
+
font-size: var(--type-deck-heading-sm-size);
|
|
1090
|
+
line-height: var(--type-deck-heading-sm-line-height);
|
|
1091
|
+
}
|
|
1092
|
+
.slidev-layout .deck-contents-link.is-section {
|
|
1093
|
+
font-weight: 700;
|
|
1094
|
+
color: var(--deck-accent);
|
|
1095
|
+
}
|
|
1096
|
+
.slidev-layout li.deck-contents-subsection,
|
|
1097
|
+
.slidev-layout .deck-contents-subsection > .deck-contents-link {
|
|
1098
|
+
font-size: var(--type-deck-body-sm-size);
|
|
1099
|
+
line-height: var(--type-deck-label-line-height);
|
|
1100
|
+
}
|
|
1101
|
+
.slidev-layout .deck-contents-subsection > .deck-contents-link {
|
|
1102
|
+
font-weight: 400;
|
|
1103
|
+
color: var(--deck-fg-muted);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/*
|
|
1107
|
+
★ THE SUBSECTIONS HANG OFF A WARM RULE, which is the only thing on the page that says "these
|
|
1108
|
+
belong to the name above them". A rule rather than an indent alone: at four columns the longest
|
|
1109
|
+
title wraps, and a wrapped line under a bare indent reads as a new entry — against a rule it
|
|
1110
|
+
reads as a continuation. The inset is the rule's own offset plus its gap, so a retune moves both.
|
|
1111
|
+
*/
|
|
1112
|
+
.slidev-layout .deck-contents ol.deck-contents-subsections {
|
|
1113
|
+
position: relative;
|
|
1114
|
+
margin-top: var(--deck-space-1);
|
|
1115
|
+
/*
|
|
1116
|
+
★ THE WHOLE GROUP IS INSET, BAR INCLUDED. The bar is positioned against this box, so indenting
|
|
1117
|
+
the box moves the rule and its rows together — the alternative, nudging only the `::before`,
|
|
1118
|
+
would leave the rule floating inside the text block rather than hanging beside it. Flush at the
|
|
1119
|
+
column edge it read as a border on the column rather than a mark belonging to the section.
|
|
1120
|
+
*/
|
|
1121
|
+
margin-inline-start: var(--deck-space-2);
|
|
1122
|
+
/*
|
|
1123
|
+
★ THE SELECTOR CARRIES `ol` ON PURPOSE. `.deck-contents ol { padding-left: 0 }` above is
|
|
1124
|
+
(0,2,1) and strips the browser's list indent; a class-only rule here is (0,2,0) and loses to
|
|
1125
|
+
it, so the indent silently stayed at zero and the subsections hung under their section with
|
|
1126
|
+
nothing but weight telling them apart. Matched at (0,3,1) instead.
|
|
1127
|
+
*/
|
|
1128
|
+
padding-left: var(--deck-space-2);
|
|
1129
|
+
}
|
|
1130
|
+
/*
|
|
1131
|
+
★★ THE BAR IS A PSEUDO-ELEMENT, BECAUSE A `border-left` HAS NO ENDS. It was one, and a border is
|
|
1132
|
+
drawn to the edges of its box with square corners — there is no property that rounds only the two
|
|
1133
|
+
ends of one side. Absolutely positioned instead, so it can carry a radius and a width of its own
|
|
1134
|
+
without either touching the text's indent.
|
|
1135
|
+
|
|
1136
|
+
★★ `--deck-bg-warm-mark`, NOT `--deck-bg-warm`. The fill is a GROUND — measured 1.30:1 against the
|
|
1137
|
+
page, right for a panel behind type and invisible as a rule. The family's `mark` role is the one
|
|
1138
|
+
meant to be seen on its own ground: 4.99:1 here. A rule that cannot be seen is worse than none,
|
|
1139
|
+
because the indent then reads as an accident.
|
|
1140
|
+
*/
|
|
1141
|
+
.slidev-layout .deck-contents-subsections::before {
|
|
1142
|
+
content: '';
|
|
1143
|
+
position: absolute;
|
|
1144
|
+
inset-block: 0;
|
|
1145
|
+
inset-inline-start: 0;
|
|
1146
|
+
width: calc(var(--deck-space-1) / 2);
|
|
1147
|
+
border-radius: var(--deck-radius-full);
|
|
1148
|
+
background: var(--deck-bg-warm-mark);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/*
|
|
1152
|
+
★ HOVER AND FOCUS TOGETHER, ALWAYS. The pointer is not the only way into this page — a presenter
|
|
1153
|
+
tabs it, and an index whose affordance is pointer-only is an index half the room cannot use.
|
|
1154
|
+
*/
|
|
1155
|
+
.slidev-layout .deck-contents-link:hover,
|
|
1156
|
+
.slidev-layout .deck-contents-link:focus-visible {
|
|
1157
|
+
color: var(--deck-ink-warm);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/*
|
|
1161
|
+
Nested lists step down a tier rather than repeating the shout.
|
|
1162
|
+
|
|
1163
|
+
★★ A TIER IS A SIZE, AND FOR A LONG TIME ONLY THE MARKER STEPPED. The rule below quietened the
|
|
1164
|
+
disc — subtle instead of accent, 0.4em instead of 0.5 — and the TEXT beside it stayed at the
|
|
1165
|
+
parent's size, so a nested item was a full-size line wearing a smaller bullet. Measured on
|
|
1166
|
+
apps/deck's prose slide: both tiers at 48px.
|
|
1167
|
+
|
|
1168
|
+
The step is the ramp's own next rung, `body-sm`, taken from the token rather than expressed as a
|
|
1169
|
+
fraction: the nested tier is not "a bit smaller", it is the role the type system already has for
|
|
1170
|
+
subordinate running text, and a `0.75em` would be that same number restated where a retuned ramp
|
|
1171
|
+
could not reach it. Its line-height comes with it, or the tighter type would sit in the taller
|
|
1172
|
+
tier's leading.
|
|
1173
|
+
|
|
1174
|
+
Everything the marker is sized in is `em`, so it follows the text down on its own: 0.4em of the
|
|
1175
|
+
nested 36px is a 14px disc where it was 19px at 48. The one number that does NOT follow is
|
|
1176
|
+
`top`, which was tuned against the parent tier's 1.6 line box and is re-tuned here for body-sm's
|
|
1177
|
+
1.5.
|
|
1178
|
+
*/
|
|
1179
|
+
.slidev-layout li ul,
|
|
1180
|
+
.slidev-layout li ol {
|
|
1181
|
+
font-size: var(--type-deck-body-sm-size);
|
|
1182
|
+
line-height: var(--type-deck-body-sm-line-height);
|
|
1183
|
+
margin-top: 0.3em;
|
|
1184
|
+
}
|
|
1185
|
+
/* The nested tier carries the same two corrections, against its own smaller dot: half the line box
|
|
1186
|
+
less half of 0.4em, and the same 0.55em inset. It runs at the dense type tier, whose line-height
|
|
1187
|
+
differs again — which is the whole reason neither number is written as a constant. */
|
|
1188
|
+
.slidev-layout li ul > li::before {
|
|
1189
|
+
background: var(--deck-fg-subtle);
|
|
1190
|
+
width: 0.4em;
|
|
1191
|
+
height: 0.4em;
|
|
1192
|
+
left: 0.55em;
|
|
1193
|
+
top: calc(0.5lh - 0.2em);
|
|
1194
|
+
border-radius: var(--deck-radius-full);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/* ── Rules, links, code, quotes ───────────────────────────────────────────────────────────── */
|
|
1198
|
+
|
|
1199
|
+
.slidev-layout hr {
|
|
1200
|
+
border: none;
|
|
1201
|
+
height: 4px;
|
|
1202
|
+
width: var(--deck-space-7);
|
|
1203
|
+
background: var(--deck-accent);
|
|
1204
|
+
margin: var(--deck-space-3) 0;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.slidev-layout a {
|
|
1208
|
+
color: var(--deck-accent);
|
|
1209
|
+
text-decoration: underline;
|
|
1210
|
+
text-underline-offset: 0.15em;
|
|
1211
|
+
text-decoration-thickness: 0.06em;
|
|
1212
|
+
transition: color var(--motion-fast) var(--motion-ease-out);
|
|
1213
|
+
}
|
|
1214
|
+
.slidev-layout a:hover {
|
|
1215
|
+
color: var(--deck-accent-strong);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/* ⚠ NO `background` HERE, and the one that was is why this note exists. This set
|
|
1219
|
+
`var(--deck-accent-soft)` at (0,1,1), and the rule below re-points inline code to --deck-surface
|
|
1220
|
+
at (0,3,1) — so the declaration could never apply. It was left behind when the code block moved to
|
|
1221
|
+
Catppuccin and the chip went back to the deck surface; measured live, the chip's computed fill is
|
|
1222
|
+
the surface mix, identical to the block's. A dead declaration is worse than none: it reads as the
|
|
1223
|
+
answer to "what colour is an inline code chip" and is not. */
|
|
1224
|
+
.slidev-layout code {
|
|
1225
|
+
font-family: var(--font-mono);
|
|
1226
|
+
font-size: 0.85em;
|
|
1227
|
+
color: var(--deck-accent);
|
|
1228
|
+
padding: 0.05em 0.3em;
|
|
1229
|
+
/* ⚠ NO `border-radius` HERE, AND THE ONE THAT WAS NEVER APPLIED. This selector is (0,1,1) and
|
|
1230
|
+
@slidev/client's `.slidev-layout :not(pre) > code` is (0,2,1), so its `var(--slidev-code-radius)`
|
|
1231
|
+
won every time: the chip has been drawing at Slidev's 24px, not at the --deck-radius-item this
|
|
1232
|
+
rule asked for, since the theme shipped. Measured live before moving it. The radius now sits on
|
|
1233
|
+
the (0,3,1) rule below, beside the fill and the edge that had to go there for the same reason —
|
|
1234
|
+
the same trap the note above this rule records for `background`. */
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/*
|
|
1238
|
+
★★ INLINE CODE KEEPS THE DECK SURFACE; ONLY THE BLOCK TAKES CATPPUCCIN'S GROUND.
|
|
1239
|
+
|
|
1240
|
+
Slidev paints both from --slidev-code-background (its own code.css), and that token is Catppuccin's
|
|
1241
|
+
base now — right for a block, wrong for a `code` chip sitting inside a sentence of Forest prose,
|
|
1242
|
+
which is deck furniture and should read as one. So the chip is re-pointed back at the surface it
|
|
1243
|
+
always had. The block below is the only thing that changes register-source.
|
|
1244
|
+
*/
|
|
1245
|
+
/*
|
|
1246
|
+
⚠ `:not(.shiki)` IS A SPECIFICITY COUNTERWEIGHT, NOT A FILTER, and it is here because the note in
|
|
1247
|
+
styles/tokens.css already measured this exact collision. @slidev/client's own rule is
|
|
1248
|
+
`.slidev-layout :not(pre) > code` at (0,2,1); repeating that selector verbatim TIES, and a tie is
|
|
1249
|
+
decided by whichever stylesheet the bundler happens to emit last — which is not a thing to leave a
|
|
1250
|
+
register-correct colour resting on. One more class-shaped pseudo makes it (0,3,1) and the tie is
|
|
1251
|
+
gone. An inline `<code>` is never `.shiki` (Shiki writes that class on the `<pre>`), so the
|
|
1252
|
+
selector matches exactly what it did before.
|
|
1253
|
+
*/
|
|
1254
|
+
/*
|
|
1255
|
+
★★ THE CHIP IS A WASH OF ITS OWN INK, NOT A FIXED SURFACE.
|
|
1256
|
+
|
|
1257
|
+
It was --deck-surface, which is the SLIDE's neutral surface mix and tracks the page rather than
|
|
1258
|
+
whatever the chip is actually sitting on. Inside a warm panel that put a grey chip with green type
|
|
1259
|
+
on a peach ground — three families in one word, and the thing that reads as wrong is the grey.
|
|
1260
|
+
|
|
1261
|
+
`currentColor` makes the fill and the edge derive from the chip's own colour, so the chip lands on
|
|
1262
|
+
every ground the deck has without a rule per ground: on the page it is a wash of the accent, on a
|
|
1263
|
+
named background it is a wash of that family's ink (below), and on the dark register it inverts
|
|
1264
|
+
with the ink it is mixed from. A ground this theme has not invented yet is handled too.
|
|
1265
|
+
|
|
1266
|
+
⚠ MIXED WITH `transparent`, NOT WITH THE GROUND. The chip must not know what is behind it — it sits
|
|
1267
|
+
on panels, on cards and on the page, and a mix against a named ground would be wrong on two of the
|
|
1268
|
+
three. An alpha wash is right on all of them.
|
|
1269
|
+
*/
|
|
1270
|
+
.slidev-note :not(pre) > code:not(.shiki),
|
|
1271
|
+
.slidev-layout :not(pre) > code:not(.shiki) {
|
|
1272
|
+
background: color-mix(in oklab, currentColor 8%, transparent);
|
|
1273
|
+
/*
|
|
1274
|
+
THE STRONG EDGE ALPHA, AT THE CHIP'S OWN WIDTH. This was 18%, BELOW the 20% the card's
|
|
1275
|
+
`hairline` edge takes — so the chip carried the faintest edge in the deck while being the
|
|
1276
|
+
smallest thing that has one, and read as a hairline at room scale. 40% is the card's `strong`
|
|
1277
|
+
alpha; the card spends 4px on it because it is a frame, and a chip inside a sentence spends 2.
|
|
1278
|
+
Measured over the chip's own 8% fill: 1.30:1 at 18%, 1.84:1 here.
|
|
1279
|
+
*/
|
|
1280
|
+
border: 2px solid color-mix(in oklab, currentColor 40%, transparent);
|
|
1281
|
+
/*
|
|
1282
|
+
A SOFT CORNER, NOT A PILL. The chip measures 65 canvas px tall, so Slidev's --slidev-code-radius
|
|
1283
|
+
(24px, 39 canvas px) is 60% of its height and draws a lozenge. Twice the base rung is 26 canvas
|
|
1284
|
+
px, two fifths of the height — a rounded box, still a box. There is no NAMED step here:
|
|
1285
|
+
--deck-radius-nested is the same 16px today but is derived from the card radius and follows it,
|
|
1286
|
+
which is not a dependency a chip in a sentence should have.
|
|
1287
|
+
*/
|
|
1288
|
+
border-radius: calc(var(--deck-radius) * 2);
|
|
1289
|
+
}
|
|
1290
|
+
.slidev-layout pre {
|
|
1291
|
+
font-family: var(--font-mono);
|
|
1292
|
+
background: var(--slidev-code-background);
|
|
1293
|
+
/* ★ THE EDGE FOLLOWS THE GROUND. This was --deck-border — deck-bg mixed 14% toward the ink — which
|
|
1294
|
+
tracked the SLIDE, not the block. With the ground moved to Catppuccin's base the two came apart:
|
|
1295
|
+
measured on the dark register, a #24443e border (hue 181, teal-green) around a #1e1e2e block
|
|
1296
|
+
(hue 282, violet), a hundred degrees of hue between a box and its own edge. */
|
|
1297
|
+
border: 1px solid var(--deck-code-border);
|
|
1298
|
+
/* ★★ ONE RADIUS FOR EVERY CODE BLOCK-SHAPED THING, and it is Slidev's own `--slidev-code-radius`
|
|
1299
|
+
rather than the nested step. `.slidev-code` already carries that value with an `!important` of
|
|
1300
|
+
Slidev's making, so a bare `<pre>`, a code GROUP and a magic-move well rounding at the nested
|
|
1301
|
+
16px meant four objects that sit in the same place on a slide drew four different corners —
|
|
1302
|
+
visible the moment two of them appear on consecutive slides. The nested step stays where it
|
|
1303
|
+
means something: a `pre` INSIDE a card, which is genuinely one box in another. */
|
|
1304
|
+
border-radius: var(--slidev-code-radius);
|
|
1305
|
+
padding: var(--deck-space-3) var(--deck-space-4);
|
|
1306
|
+
}
|
|
1307
|
+
/* A code chip inside a block is already on the block's ground — drop the double fill. */
|
|
1308
|
+
/*
|
|
1309
|
+
★★ THE FONT SIZE IS PART OF THE RESET. The rule above is for an inline code CHIP and shrinks it to
|
|
1310
|
+
0.85em to sit in a line of prose; `code` inside a `pre` matches it too, so without this a block
|
|
1311
|
+
renders at 30.6px against the 36px its own token asks for.
|
|
1312
|
+
|
|
1313
|
+
It also caused the seams between lit lines: 1.5 x 30.6 is 45.9px, a fractional line box, so each
|
|
1314
|
+
row rounded to a device pixel on its own and a hairline of ground showed between backgrounds that
|
|
1315
|
+
the layout says are flush (measured gap: exactly 0). At 36px the box is 54px and they tile.
|
|
1316
|
+
*/
|
|
1317
|
+
.slidev-layout pre code {
|
|
1318
|
+
background: none;
|
|
1319
|
+
padding: 0;
|
|
1320
|
+
color: inherit;
|
|
1321
|
+
font-size: inherit;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/* ── The code register switch — a MEASURED fix, not a preference ──────────────────────────────
|
|
1325
|
+
Shiki in dual-theme mode writes both palettes onto every token span as `--shiki-light` and
|
|
1326
|
+
`--shiki-dark`, and @slidev/client/styles/code.css picks between them with:
|
|
1327
|
+
|
|
1328
|
+
html.dark .shiki span { color: var(--shiki-dark) }
|
|
1329
|
+
html:not(.dark) .shiki span { color: var(--shiki-light) }
|
|
1330
|
+
|
|
1331
|
+
★★ `html.dark` IS NOT THIS DECK'S DARK. The deck runs LIGHT and marks individual slides
|
|
1332
|
+
`class: deck-dark` — dark is punctuation here, not the ground (deck.css says so at length) — so
|
|
1333
|
+
`html` stays `.light` all the way through a dark slide and Slidev's second rule keeps handing
|
|
1334
|
+
out the LIGHT palette over a dark code block. Measured on a live render, Go on a deck-dark
|
|
1335
|
+
slide, against the block's own #0c332d ground:
|
|
1336
|
+
|
|
1337
|
+
func #1e754f 2.43:1 <- the commonest token in the sample
|
|
1338
|
+
0 #2f798a 2.77:1
|
|
1339
|
+
* #ab5959 2.82:1
|
|
1340
|
+
Fleet #2e8f82 3.51:1
|
|
1341
|
+
( #999999 4.83:1 <- punctuation, the only scope that passed
|
|
1342
|
+
|
|
1343
|
+
A screenshot did not show it. At thumbnail size the eye reconstructs the glyphs; the DOM is what
|
|
1344
|
+
said the contrast was not there, which is why this was probed rather than looked at.
|
|
1345
|
+
|
|
1346
|
+
SPECIFICITY IS THE WHOLE MECHANISM, so it is spelled out. Slidev's rule scores (0,2,2) —
|
|
1347
|
+
`html` + `:not(.dark)` + `.shiki` + `span`. A plain `.deck-dark .shiki span` is (0,2,1) and
|
|
1348
|
+
LOSES, silently, leaving this section looking correct and changing nothing. Qualifying with the
|
|
1349
|
+
layout class makes it (0,3,1): three classes beats two whatever the element count, and it does
|
|
1350
|
+
not depend on which stylesheet the bundler happens to emit last.
|
|
1351
|
+
|
|
1352
|
+
★★ AND THE CLASS CAN LAND IN THREE PLACES, WHICH IS WHY THERE ARE THREE SELECTORS RATHER THAN
|
|
1353
|
+
TWO. This listed the layout root (`class: deck-dark` in frontmatter) and an ancestor of it, and
|
|
1354
|
+
missed the one placement that is now a PROP: `panelDark` puts the class on a panel INSIDE the
|
|
1355
|
+
layout (split.vue, stack.vue), and styles/tokens.css has always said the class can land on "a
|
|
1356
|
+
wrapper inside a slide (a dark card band, a chart well)". Every role follows it there, because
|
|
1357
|
+
custom properties inherit; this rule did not, because it names its ancestor.
|
|
1358
|
+
|
|
1359
|
+
The failure was silent in the way this whole section is about: the panel's ground turned over,
|
|
1360
|
+
the code block's ground turned over with it (--slidev-code-background is --deck-surface, a role),
|
|
1361
|
+
and the TOKENS stayed on the light palette — the same 2.43:1 the table above measures, on a
|
|
1362
|
+
surface that now looks deliberate. All three placements score (0,3,1); the descendant one is not
|
|
1363
|
+
weaker for being a descendant. */
|
|
1364
|
+
.slidev-layout.deck-dark .shiki,
|
|
1365
|
+
.deck-dark .slidev-layout .shiki,
|
|
1366
|
+
.slidev-layout .deck-dark .shiki {
|
|
1367
|
+
color: var(--shiki-dark, inherit);
|
|
1368
|
+
}
|
|
1369
|
+
.slidev-layout.deck-dark .shiki span,
|
|
1370
|
+
.deck-dark .slidev-layout .shiki span,
|
|
1371
|
+
.slidev-layout .deck-dark .shiki span {
|
|
1372
|
+
color: var(--shiki-dark);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/* ★★ RETIRED — a code block inside a Card no longer needs lifting off it.
|
|
1376
|
+
This raised the block to --deck-surface-strong because both it and the Card painted --deck-surface,
|
|
1377
|
+
so a block inside a card vanished into its container. The block's ground is Catppuccin's own base
|
|
1378
|
+
now (see tokens.css), which is a different colour from the Card on both registers — 1.04:1 apart on
|
|
1379
|
+
light and 1.19:1 on dark is not much, but the two are no longer the SAME value, which is what the
|
|
1380
|
+
rule existed for. Keeping it would have pushed the block a rung away from a ground it no longer
|
|
1381
|
+
shares, for no reason a reader could see. */
|
|
1382
|
+
|
|
1383
|
+
/* ── Code groups ──────────────────────────────────────────────────────────────────────────────
|
|
1384
|
+
The tab strip and the block under it are ONE surface, and Slidev draws them as two: the strip
|
|
1385
|
+
carries the code background and the block re-declares it, so at the seam a 1px inset shadow is
|
|
1386
|
+
all that joins them. On a projector that reads as a floating label. Border and corner come from
|
|
1387
|
+
the deck contract so the group matches every other container on the slide. */
|
|
1388
|
+
.slidev-layout .slidev-code-group {
|
|
1389
|
+
border: 1px solid var(--deck-border);
|
|
1390
|
+
/* The same radius a lone block takes — a group IS a block, with tabs. See the note on `pre`. */
|
|
1391
|
+
border-radius: var(--slidev-code-radius);
|
|
1392
|
+
overflow: hidden;
|
|
1393
|
+
}
|
|
1394
|
+
.slidev-layout .slidev-code-group .slidev-code {
|
|
1395
|
+
border: none;
|
|
1396
|
+
border-radius: 0 !important;
|
|
1397
|
+
}
|
|
1398
|
+
/* ★ A TAB IS A FILENAME, SO IT KEEPS ITS OWN CASE. Every other piece of furniture in this deck —
|
|
1399
|
+
the section pill, the eyebrow, the specimen label — is set in the overline role, which uppercases.
|
|
1400
|
+
A filename is not furniture, it is an IDENTIFIER: `Panel.vue` and `panel.vue` are two different
|
|
1401
|
+
files on a case-sensitive disk, and `PANEL.VUE` is neither. So the tab strip takes the overline's
|
|
1402
|
+
size and tracking (it is still small, quiet, room-scale furniture) and NOT its transform. */
|
|
1403
|
+
/*
|
|
1404
|
+
── THE NUMBERED GUTTER ────────────────────────────────────────────────────────────────────────
|
|
1405
|
+
|
|
1406
|
+
Slidev draws it as a `::before` on every `.line` and styles it
|
|
1407
|
+
`--uno: w-4 mr-6 text-gray-400 dark-text-gray-600`. Three problems on this deck, all from the same
|
|
1408
|
+
cause — those utilities were written against Uno's default scale, and uno.config.ts REPLACES
|
|
1409
|
+
`theme.spacing` wholesale with the deck's own named ladder:
|
|
1410
|
+
|
|
1411
|
+
· `w-4` and `mr-6` are numeric steps that do not exist here, so they generate nothing and the
|
|
1412
|
+
gutter's width and gap fall back to whatever the preset happens to give.
|
|
1413
|
+
· `text-gray-400` is a grey belonging to no register in this deck, and it does not move between
|
|
1414
|
+
light and dark the way every other quiet ink does.
|
|
1415
|
+
|
|
1416
|
+
A line number is the one thing on a code slide nobody reads in sequence — it is an index you jump
|
|
1417
|
+
to when someone says "line four" — so it takes the muted role at the annotation rung, and its
|
|
1418
|
+
width is stated in `ch` because it is counting DIGITS, not spanning a box.
|
|
1419
|
+
*/
|
|
1420
|
+
.slidev-layout .slidev-code-line-numbers .slidev-code code .line::before {
|
|
1421
|
+
width: 2ch;
|
|
1422
|
+
margin-right: var(--deck-space-4);
|
|
1423
|
+
color: var(--deck-fg-subtle);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/* ★★ AND THE SAME STRIP SERVES A SINGLE BLOCK'S TITLE. Slidev draws ```go [fleet.go] with
|
|
1427
|
+
`.slidev-code-block-title` — the same header bar as a code group's tab row, with one label
|
|
1428
|
+
instead of a row of them — and this rule named only the group. A deck could put a filename on a
|
|
1429
|
+
lone block and get mermaid's… no: get SLIDEV's stock strip, sans-serif and square against a
|
|
1430
|
+
themed one three lines further down the same slide. Named together because they ARE the same
|
|
1431
|
+
piece of furniture. */
|
|
1432
|
+
/*
|
|
1433
|
+
★★ THE TAB ROW IS SQUARED AND THE LONE TITLE IS LEFT ALONE, which is the opposite of where this
|
|
1434
|
+
started. Stock already rounds BOTH strips — `client/styles/code.css:54`, `border-radius: R R 0 0`
|
|
1435
|
+
— and this theme used to flatten both with a `border-radius: 0` that read as "keep Slidev's
|
|
1436
|
+
corners out". That was the bug behind the square-topped block on `A numbered block, named`: the
|
|
1437
|
+
`pre` rounds `0 0 R R` precisely because a strip sits above it, so suppressing the strip's corners
|
|
1438
|
+
left the object round along the bottom and cut square along the top. Deleting the override is the
|
|
1439
|
+
whole fix there — stock is right, and no rule for the title belongs here.
|
|
1440
|
+
|
|
1441
|
+
A GROUP is the other way round. `.slidev-code-group` is itself a rounded, clipping box, so stock's
|
|
1442
|
+
radius on its tab row is a second curve inside the first; with the two at different values it drew
|
|
1443
|
+
a visible seam — the group clipping on one arc, the row's grey ending on another, the group's 1px
|
|
1444
|
+
border tracing a third. Reported on `Code group`. The wrapper clips; the row stays square.
|
|
1445
|
+
*/
|
|
1446
|
+
.slidev-layout .slidev-code-group-tabs {
|
|
1447
|
+
border-radius: 0;
|
|
1448
|
+
}
|
|
1449
|
+
/*
|
|
1450
|
+
★★ NO HAIRLINE UNDER EITHER STRIP. Stock draws one as `box-shadow: inset 0 -1px` — a 1px rule
|
|
1451
|
+
between the strip and the code. It is redundant here: the strip already sits on
|
|
1452
|
+
`--deck-surface-strong` and the code on `--slidev-code-background`, two different grounds, so the
|
|
1453
|
+
seam is a change of fill and does not also need a line drawn on it. Removing it is what lets the
|
|
1454
|
+
filename centre (below), and it costs no height either way.
|
|
1455
|
+
|
|
1456
|
+
★★ AND A LONE TITLE SPENDS THE TAB'S UNDERLINE AS PADDING RATHER THAN RESERVING IT. `border-bottom-width`
|
|
1457
|
+
above is a TAB's active marker; a strip with one label has nothing to mark. It cannot simply be
|
|
1458
|
+
dropped — a tab row is padding + label + that 8px, so a title without it stands 8px shorter than
|
|
1459
|
+
the row on the previous slide, and this book puts the two on consecutive slides. Nor can it stay:
|
|
1460
|
+
reserved at the BOTTOM it pushed the label up, measured 16px above and 24px below.
|
|
1461
|
+
|
|
1462
|
+
So the rung is taken off the border and added to the block padding, half at each end: same total
|
|
1463
|
+
height as a tab row, label centred in it. (It was briefly a transparent border, which held the
|
|
1464
|
+
height and kept the label 8px high — this is the same arithmetic with the space where it belongs.)
|
|
1465
|
+
*/
|
|
1466
|
+
.slidev-layout .slidev-code-block-title,
|
|
1467
|
+
.slidev-layout .slidev-code-group-tabs {
|
|
1468
|
+
box-shadow: none;
|
|
1469
|
+
}
|
|
1470
|
+
.slidev-layout .slidev-code-block-title,
|
|
1471
|
+
.slidev-layout .slidev-code-group-tabs {
|
|
1472
|
+
font-family: var(--font-mono);
|
|
1473
|
+
/* ★ AND NOT THE OVERLINE'S TRACKING EITHER. This took the overline's letter-spacing along with
|
|
1474
|
+
its size, on the reasoning that the strip is small, quiet furniture. Tracking is opened up to
|
|
1475
|
+
make UPPERCASE furniture legible; a filename is neither uppercase (see above) nor furniture,
|
|
1476
|
+
and letter-spaced mono reads as a label of a file rather than as the file's name. */
|
|
1477
|
+
letter-spacing: normal;
|
|
1478
|
+
/* The strip is a header ON the block, not a row floating above it: its own raised ground and one
|
|
1479
|
+
divider, so the tabs and the code read as one object the way the design system's own code group
|
|
1480
|
+
does.
|
|
1481
|
+
|
|
1482
|
+
★ THE DIVIDER IS STOCK'S, NOT A BORDER OF OURS. `client/styles/code.css:55` already draws it as
|
|
1483
|
+
`box-shadow: inset 0 -1px var(--slidev-code-tab-divider)`, and tokens.css points that variable
|
|
1484
|
+
at `--deck-border` — the same line, the same colour. Adding a real border drew it twice and,
|
|
1485
|
+
because a border occupies layout where an inset shadow does not, left the tab row exactly 1px
|
|
1486
|
+
taller than a lone title strip. That was the last of a 14px difference between two strips this
|
|
1487
|
+
book puts on consecutive slides. */
|
|
1488
|
+
background: var(--deck-surface-strong);
|
|
1489
|
+
}
|
|
1490
|
+
/*
|
|
1491
|
+
★★ AND THE TWO STRIPS STAND THE SAME HEIGHT, which they did not: 70px against a tab row's 84.
|
|
1492
|
+
Slidev wraps a lone filename in its own `.leading-1em` div, so the label sat in a 1em line box
|
|
1493
|
+
while a tab sat in the inherited 1.45 one — same padding, same border, 14px apart, and the two
|
|
1494
|
+
are read on consecutive slides in this book. The utility is neutralised rather than the padding
|
|
1495
|
+
retuned, so both strips keep taking their height from the same three terms.
|
|
1496
|
+
*/
|
|
1497
|
+
.slidev-layout .slidev-code-block-title > .leading-1em {
|
|
1498
|
+
line-height: inherit;
|
|
1499
|
+
}
|
|
1500
|
+
.slidev-layout .slidev-code-block-title,
|
|
1501
|
+
.slidev-layout .slidev-code-tab {
|
|
1502
|
+
padding: var(--deck-space-2) var(--deck-space-4);
|
|
1503
|
+
/* Stock draws a 2px underline. At 1920 across a room that is a hairline — the one thing on the
|
|
1504
|
+
strip that says which file you are looking at, drawn at the width of a border. */
|
|
1505
|
+
border-bottom-width: var(--deck-space-1);
|
|
1506
|
+
border-bottom-style: solid;
|
|
1507
|
+
/* Stock hard-codes `transition: color 0.25s`, which is off the ladder in both duration and
|
|
1508
|
+
easing, and is the same class of miss as the magic-move timing below. */
|
|
1509
|
+
transition:
|
|
1510
|
+
color var(--motion-base) var(--motion-ease-out),
|
|
1511
|
+
border-color var(--motion-base) var(--motion-ease-out);
|
|
1512
|
+
}
|
|
1513
|
+
/*
|
|
1514
|
+
★ AFTER the rule above, deliberately: both selectors are (0,2,0), so this only wins on order. It
|
|
1515
|
+
was written before it once and silently lost — the strip kept the tab's 8px rung and its label
|
|
1516
|
+
stayed 8px high.
|
|
1517
|
+
*/
|
|
1518
|
+
.slidev-layout .slidev-code-block-title {
|
|
1519
|
+
border-bottom-width: 0;
|
|
1520
|
+
padding-block: calc(var(--deck-space-2) + var(--deck-space-1) / 2);
|
|
1521
|
+
}
|
|
1522
|
+
/* The file-type mark. Slidev's TitleIcon renders it with `w-3.5 h-3.5`, and those utilities do not
|
|
1523
|
+
exist in this deck — uno.config.ts replaces `theme.spacing` wholesale with the deck's own named
|
|
1524
|
+
ladder, so a numeric `w-3.5` generates nothing at all and the icon fell back to whatever box the
|
|
1525
|
+
preset gave it. Sized here against the tab's own text instead: an editor draws this mark at
|
|
1526
|
+
roughly the cap height of the label beside it, and that proportion is what makes it read as a
|
|
1527
|
+
file mark rather than as a picture. */
|
|
1528
|
+
.slidev-layout .slidev-code-block-title > [class*='i-vscode-icons'],
|
|
1529
|
+
.slidev-layout .slidev-code-tab > [class*='i-vscode-icons'] {
|
|
1530
|
+
width: 1.1em;
|
|
1531
|
+
height: 1.1em;
|
|
1532
|
+
flex: none;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/*
|
|
1536
|
+
★ THE COPY BUTTON IS SIZED FOR A WEB PAGE, AND THIS IS A 1920 CANVAS.
|
|
1537
|
+
|
|
1538
|
+
Slidev renders it as `<svg class="slidev-icon p-2 w-8 h-8">` — a 32px icon, which is right at a
|
|
1539
|
+
reader's arm's length and is 1.7% of the width of a slide. Beside 36px mono it reads as a speck.
|
|
1540
|
+
MEASURED on apps/deck 9: a 32px icon in a button box 32 wide and SEVENTY tall, because
|
|
1541
|
+
the button inherits the slide's body line-height (48px x 1.45 = 70) and never sets its own — so
|
|
1542
|
+
most of what it occupies is leading around an icon that is not there.
|
|
1543
|
+
|
|
1544
|
+
Sized against the CODE rather than against the slide: the button takes the block's own font size,
|
|
1545
|
+
the icon takes 1.1em of that, and `line-height: 1` makes the box the icon again. Same reasoning
|
|
1546
|
+
and the same number as the file-type mark above — an editor draws its furniture at roughly the cap
|
|
1547
|
+
height of the text beside it, and that proportion is what makes it read as a control rather than
|
|
1548
|
+
as a picture.
|
|
1549
|
+
|
|
1550
|
+
It also sat flush in the block's corner, over the padding rather than inside it, so it crowded the
|
|
1551
|
+
first line's last token. Inset by the block's own padding variable, which is the same edge every
|
|
1552
|
+
other thing in the block is measured from.
|
|
1553
|
+
|
|
1554
|
+
Left at Slidev's own visibility — invisible until the block is hovered — deliberately: on a
|
|
1555
|
+
projector nothing is ever hovered, so this costs the room nothing, and the specimen is also read
|
|
1556
|
+
on a screen where copying a frontmatter sample is the point.
|
|
1557
|
+
*/
|
|
1558
|
+
.slidev-layout .slidev-code-wrapper .slidev-code-copy {
|
|
1559
|
+
top: var(--slidev-code-padding);
|
|
1560
|
+
right: var(--slidev-code-padding);
|
|
1561
|
+
font-size: var(--slidev-code-font-size);
|
|
1562
|
+
line-height: 1;
|
|
1563
|
+
}
|
|
1564
|
+
/*
|
|
1565
|
+
★★ AND IT MOVES INTO THE STRIP WHEN THERE IS ONE, because `top` above is measured from the
|
|
1566
|
+
WRAPPER and the wrapper starts at the title bar, not at the code. On a bare block one code padding
|
|
1567
|
+
down is the first line's last token, which is what that rule is for; put a 42px strip above the
|
|
1568
|
+
same block and the button lands ASTRIDE the divider — measured on the specimen's `A numbered
|
|
1569
|
+
block, named`: button 293.7→317.8, strip ending 316.7, so it hung 1px into the code below and cut
|
|
1570
|
+
the one line that separates the two halves of the object.
|
|
1571
|
+
|
|
1572
|
+
Centred on the strip rather than nudged: the strip is its own vertical padding plus a 1em label
|
|
1573
|
+
(`.leading-1em`), and the button is 1.1em, so half the difference is the whole correction and it
|
|
1574
|
+
follows any retune of either variable.
|
|
1575
|
+
|
|
1576
|
+
★ A CODE GROUP NEEDS NOTHING HERE, and the obvious companion selector for it is DEAD — checked
|
|
1577
|
+
rather than assumed. A group's tab row hangs off `.slidev-code-group`, not `.slidev-code-wrapper`:
|
|
1578
|
+
the tabs sit beside `.slidev-code-group-blocks`, and each block inside keeps its OWN wrapper. So
|
|
1579
|
+
the copy button's wrapper never contains the tab row, `:has(.slidev-code-group-tabs)` matches
|
|
1580
|
+
nothing, and the button correctly stays one code padding inside the code it copies.
|
|
1581
|
+
*/
|
|
1582
|
+
.slidev-layout .slidev-code-wrapper:has(.slidev-code-block-title) .slidev-code-copy {
|
|
1583
|
+
/*
|
|
1584
|
+
★★ GIVEN THE LABEL'S OWN LINE BOX AND CENTRED IN IT, rather than offset by a number. This was
|
|
1585
|
+
`calc(var(--deck-space-2) - 0.05em)`, solved against the strip as it was then — 16px padding and
|
|
1586
|
+
a 1em label — and it silently stopped being centred the moment the strip took the tab's underline
|
|
1587
|
+
rung as padding: measured 14.2px above the button and 29.7px below.
|
|
1588
|
+
|
|
1589
|
+
Two terms, both the strip's own: `top` is exactly the strip's padding-block, so the button starts
|
|
1590
|
+
where the filename starts; `height` is the line box that filename sits in, so centring inside it
|
|
1591
|
+
centres the two against each other. Nothing here is a measured constant, so a retune of the
|
|
1592
|
+
padding rung or the tab font size moves both together.
|
|
1593
|
+
*/
|
|
1594
|
+
top: calc(var(--deck-space-2) + var(--deck-space-1) / 2);
|
|
1595
|
+
height: calc(var(--type-deck-body-line-height) * var(--slidev-code-tab-font-size));
|
|
1596
|
+
display: flex;
|
|
1597
|
+
align-items: center;
|
|
1598
|
+
}
|
|
1599
|
+
.slidev-layout .slidev-code-wrapper .slidev-code-copy .slidev-icon {
|
|
1600
|
+
width: 1.1em;
|
|
1601
|
+
height: 1.1em;
|
|
1602
|
+
padding: 0;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/* ── The marquee, frozen for export ───────────────────────────────────────────────────────────
|
|
1606
|
+
`slidev export` navigates to the /print route, which renders every slide inside
|
|
1607
|
+
`#print-container`, and calls `page.pdf()`. It also does `emulateMedia({ media: 'screen' })`
|
|
1608
|
+
first — so an `@media print` rule is dead here, and this ancestor class is the only hook there is.
|
|
1609
|
+
|
|
1610
|
+
Without it the PDF catches the band wherever the compositor had it: measured, the first item read
|
|
1611
|
+
"…thwark" where "Southwark" should be. The band is designed to be legible standing still, and
|
|
1612
|
+
translate(0) is the frame it must stand still ON — the same frame the engine's reduced-motion
|
|
1613
|
+
floor lands it on.
|
|
1614
|
+
|
|
1615
|
+
★★ IT IS HERE RATHER THAN IN Marquee.vue BECAUSE A SCOPED BLOCK DESTROYS IT. Vue compiles
|
|
1616
|
+
`:global(#print-container) .deck-marquee-track { … }` down to `#print-container { … }` — the
|
|
1617
|
+
descendant is dropped and the declaration lands on the container itself, doing nothing. The
|
|
1618
|
+
component's own comment records that; this is where the rule actually lives. */
|
|
1619
|
+
#print-container .deck-marquee-track {
|
|
1620
|
+
animation: none;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/* ── Magic move ───────────────────────────────────────────────────────────────────────────────
|
|
1624
|
+
shiki-magic-move animates each token to its new position, and its stylesheet times that on its
|
|
1625
|
+
own hard-coded durations. The deck has a motion ladder and a reduced-motion floor that every
|
|
1626
|
+
other transition on a slide obeys; a code animation is not the one exception. `move` is the long
|
|
1627
|
+
travel (a token crossing the block), `enter`/`leave` the short one. */
|
|
1628
|
+
/* ★ IT PAINTS THE CODE GROUND, NOT THE DECK SURFACE, and names the same two tokens `pre` does. The
|
|
1629
|
+
two roles are not the same colour: a copy of `pre`'s fill here drifts the moment the block's
|
|
1630
|
+
ground moves, and a magic-move slide then carries a different well from the slide before it. */
|
|
1631
|
+
.slidev-layout .shiki-magic-move-container {
|
|
1632
|
+
background: var(--slidev-code-background);
|
|
1633
|
+
border: 1px solid var(--deck-code-border);
|
|
1634
|
+
/* The same radius a lone block takes — the well stands where a block would. */
|
|
1635
|
+
border-radius: var(--slidev-code-radius);
|
|
1636
|
+
padding: var(--deck-space-3) var(--deck-space-4);
|
|
1637
|
+
}
|
|
1638
|
+
.slidev-layout .shiki-magic-move-container pre.shiki {
|
|
1639
|
+
background: none;
|
|
1640
|
+
border: none;
|
|
1641
|
+
padding: 0;
|
|
1642
|
+
}
|
|
1643
|
+
/* Magic move keeps shiki-magic-move's own timing. The --motion-* ladder is scaled for a control
|
|
1644
|
+
answering a click, where the ceiling is 300ms; this is choreography an audience follows, and the
|
|
1645
|
+
library's own 500ms default is scaled for that.
|
|
1646
|
+
|
|
1647
|
+
⚠ An override here needs `!important`. The renderer sets the timing as an inline style on the
|
|
1648
|
+
container (`container.style.setProperty('--smm-duration', …)`), which outranks a plain declaration.
|
|
1649
|
+
|
|
1650
|
+
Reduced motion is handled by the global floor in @humanforest/tokens/motion: it allow-lists
|
|
1651
|
+
`transition-property`, so the travel and the container resize snap and the colour fade survives.
|
|
1652
|
+
Nothing is needed here for it. */
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
/* ── Maths (KaTeX) ────────────────────────────────────────────────────────────────────────────
|
|
1656
|
+
The one built-in that arrived nearly right: KaTeX renders into ordinary DOM (no shadow root, no
|
|
1657
|
+
canvas), inherits `color` from the slide, and needs no palette of its own. What it does need is
|
|
1658
|
+
to be told the deck's SIZES, because its own are relative to a 16px web page.
|
|
1659
|
+
|
|
1660
|
+
★ KATEX SETS 1.21em ON EVERY `.katex`, INLINE ONES INCLUDED. That multiplier is the TeX
|
|
1661
|
+
convention for DISPLAY maths, and KaTeX applies it to the class rather than to display mode — so
|
|
1662
|
+
measured on this deck, `$\frac{r}{v \cdot h}$` in a 48px paragraph rendered at 58.08px: maths
|
|
1663
|
+
21% taller than the sentence carrying it, pushing the line box open around it. Inline maths
|
|
1664
|
+
belongs ON the line.
|
|
1665
|
+
|
|
1666
|
+
The 1.05em that replaces it is an OPTICAL correction, not a reversal to 1. KaTeX_Main is a
|
|
1667
|
+
Computer Modern cut with a noticeably smaller x-height than GT Haptik, so matched px-for-px it
|
|
1668
|
+
reads smaller than the words either side of it. Nothing here re-points the family: a maths face
|
|
1669
|
+
is a maths face, the brand does not have one, and setting GT Haptik on an equation would break
|
|
1670
|
+
the glyph metrics KaTeX positions everything against.
|
|
1671
|
+
|
|
1672
|
+
Display maths takes a real rung instead of a multiple of whatever it happens to sit in — an
|
|
1673
|
+
equation on its own line is the subject of the slide, so it is set one tier ABOVE body. */
|
|
1674
|
+
.slidev-layout .katex {
|
|
1675
|
+
font-size: 1.05em;
|
|
1676
|
+
}
|
|
1677
|
+
.slidev-layout .katex-display > .katex {
|
|
1678
|
+
font-size: var(--type-deck-title-size);
|
|
1679
|
+
}
|
|
1680
|
+
.slidev-layout .katex-display {
|
|
1681
|
+
margin: var(--deck-space-4) 0;
|
|
1682
|
+
/* A long equation cannot be re-broken — TeX has already set it — so the only honest options are
|
|
1683
|
+
scroll it or let it paint off the canvas. `visible` is what KaTeX ships, and an equation that
|
|
1684
|
+
leaves the slide is invisible to scripts/deckAudit.ts, which measures the ELEMENT's box. */
|
|
1685
|
+
overflow-x: auto;
|
|
1686
|
+
/* ★ AND THE SCROLL BOX CLIPS VERTICALLY TOO, which is a CSS rule rather than a choice: with
|
|
1687
|
+
`overflow-x: auto`, a `visible` overflow-y computes to `auto`, so the box clips on both axes
|
|
1688
|
+
whatever is written here. Tall constructs reach past their own line box — a summation's limits,
|
|
1689
|
+
an integral, a stacked fraction — and the audit caught exactly that: the `n` above a \sum was
|
|
1690
|
+
31% hidden, 12px cut off the top, with no ellipsis to say so. The padding is the room those
|
|
1691
|
+
constructs need inside the box that now clips them. */
|
|
1692
|
+
overflow-y: hidden;
|
|
1693
|
+
padding-block: var(--deck-space-2);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/* ── DIMMING: MATHS ONLY. CODE IS STOCK. ──────────────────────────────────────────────────────
|
|
1697
|
+
Slidev dims what is NOT under discussion to `opacity: 0.3` in both kinds of highlighting — a
|
|
1698
|
+
`$$ {1|3|all}` maths block (`.mord.dishonored`) and a ```go {4-6} code block
|
|
1699
|
+
(`.slidev-code-dishonored`). This theme used to override BOTH, on one measurement:
|
|
1700
|
+
|
|
1701
|
+
#a9b2ac on #f1ede6 1.86:1 (30 elements, the whole un-lit body of a code block)
|
|
1702
|
+
#aac6b8 on #f1ede6 1.57:1
|
|
1703
|
+
#d3bab4 on #f1ede6 1.57:1
|
|
1704
|
+
|
|
1705
|
+
The point of dimming a row is that the audience can still READ it and see it is not the one under
|
|
1706
|
+
discussion; at 1.6:1 across a room it is not dimmed, it is gone.
|
|
1707
|
+
|
|
1708
|
+
★ MATHS STILL TAKES THE OVERRIDE, and takes it as a COLOUR rather than an opacity. An equation
|
|
1709
|
+
has one ink, so there is no hue to preserve, and a ground-relative mix is strictly better than an
|
|
1710
|
+
opacity: it is contrast-gated, and the muted role is the deck's own answer to "present but not
|
|
1711
|
+
the subject". The percentage table below is the tuning.
|
|
1712
|
+
|
|
1713
|
+
★★ CODE NO LONGER DOES — 2026-09-06, deliberately, with the number above in view. See the block
|
|
1714
|
+
further down for what came out with it and why. This file's rules stop at `.mord.dishonored`.
|
|
1715
|
+
*/
|
|
1716
|
+
/*
|
|
1717
|
+
⚠ THE CODE BLOCK'S OWN GROUND RE-POINT CAME OUT WITH THE MIX. `.slidev-code` used to set
|
|
1718
|
+
`--deck-mermaid-label-bg: var(--slidev-code-background)` so the dimming mix was taken against the
|
|
1719
|
+
surface the code actually sits on rather than against the page — correct while code was dimmed by
|
|
1720
|
+
colour, and dead now that it is dimmed by opacity, which composites against whatever is behind it
|
|
1721
|
+
without being told. The maths branch keeps the variable and its page default.
|
|
1722
|
+
*/
|
|
1723
|
+
.slidev-layout .slidev-katex-wrapper .mord.dishonored {
|
|
1724
|
+
opacity: 1;
|
|
1725
|
+
/*
|
|
1726
|
+
★★ A MIX TOWARD THE GROUND, NOT A ROLE, AND THAT IS WHAT LETS IT BE TUNED AT ALL.
|
|
1727
|
+
`--deck-fg-subtle` is one colour for every ground, so its contrast is whatever each ground
|
|
1728
|
+
happens to give it — MEASURED across the five, dimmed against ground:
|
|
1729
|
+
|
|
1730
|
+
page 4.55 neutral 4.17 accent 4.02 warm 3.50 vivid 2.62
|
|
1731
|
+
|
|
1732
|
+
A flat role also has nowhere to go: the next step down the ramp is neutral-400, at 2.51 on the
|
|
1733
|
+
page and 1.44 on vivid. So "dim it further" was not available while the value was a role.
|
|
1734
|
+
|
|
1735
|
+
Ground-relative, the whole set moves together. MEASURED at each step, page → vivid:
|
|
1736
|
+
|
|
1737
|
+
55% 4.55 4.42 4.33 4.11 3.63 ≈ where the flat role sat
|
|
1738
|
+
50% 3.85 3.77 3.72 3.56 3.21 the last step clearing 3:1 everywhere
|
|
1739
|
+
45% 3.31 3.23 3.21 3.08 2.82
|
|
1740
|
+
40% 2.83 2.77 2.76 2.67 2.49 ← shipped
|
|
1741
|
+
35% 2.45 2.41 2.39 2.33 2.18
|
|
1742
|
+
|
|
1743
|
+
⚠ 40% IS BELOW THE 3:1 LARGE-TEXT FLOOR AND THAT IS DELIBERATE. This is the one place in the
|
|
1744
|
+
deck where the contrast that matters is not ink-against-ground but LIT-against-DIMMED: the whole
|
|
1745
|
+
mechanism exists to make one row stand out, and a dimmed row held at the reading floor does not
|
|
1746
|
+
dim enough to do that. The lit row is the subject and stays at full ink; these rows are context
|
|
1747
|
+
a viewer glances at, not text they read from the back of the room. Held to the floor, `1|2|3` is
|
|
1748
|
+
an equation with a slightly darker line in it.
|
|
1749
|
+
|
|
1750
|
+
The number is a judgement and the table is here so the next one is too — not a derivation.
|
|
1751
|
+
|
|
1752
|
+
The INK is the ground's own, not --deck-fg: on a toned panel those differ, and this is the same
|
|
1753
|
+
pair the mermaid edge label mutes against. The variable's name is mermaid's by history; what it
|
|
1754
|
+
carries is "the ground here and the ink that goes on it".
|
|
1755
|
+
*/
|
|
1756
|
+
color: color-mix(
|
|
1757
|
+
in oklab,
|
|
1758
|
+
var(--deck-mermaid-label-ink, var(--deck-fg)) 40%,
|
|
1759
|
+
var(--deck-mermaid-label-bg, var(--deck-bg))
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
/*
|
|
1763
|
+
★★ CODE HIGHLIGHTING IS STOCK SLIDEV, DELIBERATELY, AND THAT IS A REVERSAL WORTH READING.
|
|
1764
|
+
|
|
1765
|
+
This theme used to own the whole mechanism: a ground-relative mix that flattened the syntax hues
|
|
1766
|
+
on dimmed rows, a neutral band on the lit row, and an accent bar in its gutter — each with a
|
|
1767
|
+
measured justification, all of them removed on 2026-09-06 in favour of `client/styles/code.css`:
|
|
1768
|
+
|
|
1769
|
+
.slidev-code .slidev-code-highlighted { } <- empty, in stock
|
|
1770
|
+
.slidev-code .slidev-code-dishonored { opacity: .3 }
|
|
1771
|
+
|
|
1772
|
+
So a lit row now gets NOTHING and the entire signal is that the others recede, which is stock's
|
|
1773
|
+
design. Two things recommend it. It is the same treatment inside a magic move, where the theme's
|
|
1774
|
+
`.line`-keyed rules never reached (magic-move stamps its class on every TOKEN, not on rows) — so
|
|
1775
|
+
the two forms of highlighting stopped disagreeing. And it takes the `.line` machinery with it: the
|
|
1776
|
+
inline-block, the negative-margin gutter reservation, and the `<code> { display: block }` that
|
|
1777
|
+
only existed to stop the inline-blocks accumulating whitespace into a scrollbar.
|
|
1778
|
+
|
|
1779
|
+
⚠ THE COST IS ON THE RECORD. 0.3 over this deck's warm paper measures 1.47–1.51:1, and the note
|
|
1780
|
+
this block replaced put it plainly: at that ratio, across a room, a dimmed row is not dimmed, it
|
|
1781
|
+
is gone. The deck is sized for a room. Chosen anyway, with the numbers in view — if it reads badly
|
|
1782
|
+
from the back, the fix is a value on the rule below, not a return to the band and the bar.
|
|
1783
|
+
*/
|
|
1784
|
+
.slidev-layout .slidev-katex-wrapper .mord.highlighted {
|
|
1785
|
+
color: var(--deck-fg);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/* ── …and the lit EQUATION row does NOT get one, which is a finding rather than an omission ───
|
|
1789
|
+
The lit and dimmed rows here separate by 3.25:1 — the same ink-only gap a code block has, and
|
|
1790
|
+
for the same reason it is not quite enough on its own. A code block gets a bar in the gutter.
|
|
1791
|
+
Maths cannot have one, and it is worth saying why so nobody spends the afternoon again:
|
|
1792
|
+
|
|
1793
|
+
· Slidev classes the `.mord` CELL in every column of the aligned environment, so there are two
|
|
1794
|
+
per lit row and neither is the row.
|
|
1795
|
+
· The first column is `col-align-r`, so its cells' left edges move row by row with the width of
|
|
1796
|
+
the word in them — a bar hung there zigzags.
|
|
1797
|
+
· The two columns are ADJACENT. There is no gutter between them: a bar hung off either cell
|
|
1798
|
+
lands through the middle of the equation. Tried, and it drew straight through the word.
|
|
1799
|
+
· The `.vlist > span` row wrapper is a constant width at a constant x, which looks ideal, and is
|
|
1800
|
+
`height: 0` — so a bar there needs an invented height, and the top row's anchor sits above
|
|
1801
|
+
the block where `.katex-display`'s own `overflow-y: hidden` clips it. Rendered 8x66, accent
|
|
1802
|
+
coloured, and invisible.
|
|
1803
|
+
· Anchoring to `.katex-display` fails too: KaTeX marks several of its own spans `position:
|
|
1804
|
+
relative`, so an absolutely positioned marker resolves against one of those, not the block.
|
|
1805
|
+
|
|
1806
|
+
The ink gap is therefore what maths gets, and it is at the floor of what the token ladder allows:
|
|
1807
|
+
--deck-fg-subtle is 4.08:1 on paper and nothing lighter clears 3:1. The specimen slide states
|
|
1808
|
+
this rather than leaving a reader to wonder why the two treatments differ. */
|
|
1809
|
+
/* ── …AND THE LIT LINES NEED A SHAPE, NOT JUST A HUE ──────────────────────────────────────────
|
|
1810
|
+
Recolouring the dimmed lines fixed their legibility and broke the thing highlighting is FOR.
|
|
1811
|
+
Measured on this deck's own highlighted-code slide, after that change:
|
|
1812
|
+
|
|
1813
|
+
lit ink #002a24 dimmed ink #545252 between them: 1.99:1
|
|
1814
|
+
|
|
1815
|
+
Two inks that close are one lightness with two hues, and hue is the first thing to go from the
|
|
1816
|
+
back of a room — so every line looked the same and nothing read as lit.
|
|
1817
|
+
|
|
1818
|
+
★★ A TINTED BAND CANNOT FIX IT, and that is a property of this token system rather than a
|
|
1819
|
+
missing value. Measured against the code ground (#f1ede6): --deck-accent-soft scores **1.02:1**,
|
|
1820
|
+
--deck-bg 1.06, --deck-surface-strong 1.09. The derived surface ladder is deliberately quiet —
|
|
1821
|
+
every step sits within about a tenth of the others — so there is no band here that a room can
|
|
1822
|
+
see. The same "paper on paper" finding the Callout tail and the Alert fill both produced.
|
|
1823
|
+
|
|
1824
|
+
So the marker is a BAR IN THE GUTTER: a shape, in the accent, at the start of every lit line.
|
|
1825
|
+
The same answer Timeline's present marker and Carousel's current indicator arrived at — size and
|
|
1826
|
+
shape survive distance, a ten-percent luminance step does not.
|
|
1827
|
+
|
|
1828
|
+
★ EVERY LINE RESERVES THE GUTTER, lit or not (a transparent border on all of them), so the code
|
|
1829
|
+
does not shift sideways as the highlight moves between clicks. And the lines have to become
|
|
1830
|
+
`inline-block` to carry a border at all — shiki emits them inline, where a border wraps the text
|
|
1831
|
+
rather than the row. `width: 100%` with border-box keeps the row the width of the block. */
|
|
1832
|
+
.slidev-layout blockquote {
|
|
1833
|
+
border-left: 4px solid var(--deck-accent);
|
|
1834
|
+
color: var(--deck-fg-muted);
|
|
1835
|
+
padding-left: var(--deck-space-3);
|
|
1836
|
+
margin: var(--deck-space-3) 0;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/* ── Tables ───────────────────────────────────────────────────────────────────────────────── */
|
|
1840
|
+
|
|
1841
|
+
.slidev-layout table {
|
|
1842
|
+
width: 100%;
|
|
1843
|
+
border-collapse: collapse;
|
|
1844
|
+
font-size: var(--type-deck-body-sm-size);
|
|
1845
|
+
}
|
|
1846
|
+
.slidev-layout th,
|
|
1847
|
+
.slidev-layout td {
|
|
1848
|
+
text-align: left;
|
|
1849
|
+
padding: var(--deck-space-1) var(--deck-space-2);
|
|
1850
|
+
border-bottom: 1px solid var(--deck-border);
|
|
1851
|
+
}
|
|
1852
|
+
.slidev-layout th {
|
|
1853
|
+
font-family: var(--font-sans);
|
|
1854
|
+
font-weight: 700;
|
|
1855
|
+
color: var(--deck-fg);
|
|
1856
|
+
border-bottom: 2px solid var(--deck-accent);
|
|
1857
|
+
}
|
|
1858
|
+
.slidev-layout td {
|
|
1859
|
+
color: var(--deck-fg-muted);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/* ── Card surface ─────────────────────────────────────────────────────────────────────────
|
|
1863
|
+
The deck's one container, and the deck's copy of the web's .forest-card contract: a card is
|
|
1864
|
+
a quiet fill one step AWAY from the slide ground — lighter on light, lighter again on dark —
|
|
1865
|
+
never a shadow. Anything nested inside it picks up the concentric corner automatically.
|
|
1866
|
+
|
|
1867
|
+
── THE EDGE IS THE CARD'S OWN INK, AND IT USED TO BE --deck-border ──────────────────────
|
|
1868
|
+
That token is derived from the SLIDE's bg/fg, so a card sitting on anything other than the slide
|
|
1869
|
+
ground drew an edge computed for a ground it is not on: measured on the warm panel, 1.07:1, which
|
|
1870
|
+
is no edge. `currentColor` follows whatever ink the card inherited and holds 1.42–1.91 on every
|
|
1871
|
+
fill a card can take. It also gave the four named backgrounds an edge they did not have — they
|
|
1872
|
+
set `border-color: transparent` on the grounds that the fill was the separation, which is true of
|
|
1873
|
+
vivid (1.74:1 off the slide ground) and false of accent (1.13) and neutral (1.09), and flatly
|
|
1874
|
+
wrong for `accent` on a dark slide, where the fill IS --deck-bg and the card disappeared.
|
|
1875
|
+
|
|
1876
|
+
`border` picks the weight, and `none` is the DEFAULT: a card is a fill, and the edge is a thing
|
|
1877
|
+
an author asks for. `hairline` is 1px at 20% (measured 1.42–1.91 against the fills a card can
|
|
1878
|
+
take), `strong` is 4px at 40% (2.12–2.50 light, 2.27–3.59 dark). At 4px it is a frame rather than
|
|
1879
|
+
an outline, which is the point — the two weights are different instruments, not two settings of
|
|
1880
|
+
one. The 1px transparent border is declared on every card so switching to `hairline` costs no
|
|
1881
|
+
reflow; only `strong` changes the box, by 3px a side.
|
|
1882
|
+
|
|
1883
|
+
⚠ TWO BACKGROUNDS HAVE NOTHING ELSE. `transparent` at `border: none` is a box with no fill and
|
|
1884
|
+
no edge — legitimate (a padded, aligned cell in a grid) but it takes both props to say, so it is
|
|
1885
|
+
never an accident. `surface` sits 1.12:1 off the slide ground, a separation you can see on a
|
|
1886
|
+
screen and can lose in a room, so a surface card that has to read as a tile takes the hairline. */
|
|
1887
|
+
.slidev-layout .deck-card {
|
|
1888
|
+
background: var(--deck-surface);
|
|
1889
|
+
border: 1px solid transparent;
|
|
1890
|
+
border-radius: var(--deck-radius-card);
|
|
1891
|
+
padding: var(--deck-card-pad);
|
|
1892
|
+
}
|
|
1893
|
+
.slidev-layout .deck-card.is-border-hairline {
|
|
1894
|
+
border-color: color-mix(in oklab, currentColor 20%, transparent);
|
|
1895
|
+
}
|
|
1896
|
+
.slidev-layout .deck-card.is-border-strong {
|
|
1897
|
+
border-width: 4px;
|
|
1898
|
+
border-color: color-mix(in oklab, currentColor 40%, transparent);
|
|
1899
|
+
}
|
|
1900
|
+
.slidev-layout .deck-card > :last-child {
|
|
1901
|
+
margin-bottom: 0;
|
|
1902
|
+
}
|
|
1903
|
+
.slidev-layout .deck-card :where(.deck-card, pre, img, figure > img) {
|
|
1904
|
+
border-radius: var(--deck-radius-nested);
|
|
1905
|
+
}
|
|
1906
|
+
.slidev-layout .deck-card .deck-card :where(.deck-card, pre, img) {
|
|
1907
|
+
border-radius: var(--deck-radius-nested-2);
|
|
1908
|
+
}
|
|
1909
|
+
/* The transparent card — the container that claims no ground at all. It borrows whatever it lands
|
|
1910
|
+
on, and takes only the fill away: the edge above is already the card's own ink, which is the same
|
|
1911
|
+
reasoning BadgeStrip's `outline` reads --deck-ink-<name> rather than a pinned half. It is the one
|
|
1912
|
+
background where `border: none` leaves nothing at all, which is a legitimate thing to ask for
|
|
1913
|
+
(a padded, aligned cell in a grid) and never an accident, because it takes two props to say. */
|
|
1914
|
+
.slidev-layout .deck-card.is-transparent {
|
|
1915
|
+
background: none;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/* A card on a named background — the one that carries a claim rather than a container. The
|
|
1919
|
+
fills are the FOLLOWING tokens, so the register picks the half; `dark` on the card adds
|
|
1920
|
+
`.deck-dark`, which re-declares them to the dark half (deck.css). The ink is set once on the
|
|
1921
|
+
card and every text role inside inherits it: a token pair is fine on its own and still fails
|
|
1922
|
+
when a heading keeps reading the page's --deck-fg on a fill that is not the page. */
|
|
1923
|
+
.slidev-layout .deck-card.bg-warm { background: var(--deck-bg-warm); color: var(--deck-on-bg-warm); }
|
|
1924
|
+
.slidev-layout .deck-card.bg-accent { background: var(--deck-bg-accent); color: var(--deck-on-bg-accent); }
|
|
1925
|
+
.slidev-layout .deck-card.bg-vivid { background: var(--deck-bg-vivid); color: var(--deck-on-bg-vivid); }
|
|
1926
|
+
.slidev-layout .deck-card.bg-neutral { background: var(--deck-bg-neutral); color: var(--deck-on-bg-neutral); }
|
|
1927
|
+
/*
|
|
1928
|
+
★★ EVERY TEXT TIER ON A NAMED BACKGROUND READS THE PAIR'S INK, WHATEVER SURFACE IT IS. This was
|
|
1929
|
+
scoped to `.deck-card.bg-*` and so reached exactly one of the four surfaces that wear these
|
|
1930
|
+
classes. The others were wrong and only LOOKED right: a heading carries a hard `color:
|
|
1931
|
+
var(--deck-fg)` at (0,1,1) from the type rules above, which beats inheriting the pair's ink, so
|
|
1932
|
+
the h1 on a warm panel painted neutral-900 near-black while its own paragraphs painted warm-950
|
|
1933
|
+
brown. Measured on the specimen's full-field warm band — h1 rgb(0.096 0.088 0.090) against a band
|
|
1934
|
+
ink of rgb(0.249 0.144 0.117). It hid on the DARK half of every pair, where --deck-fg is
|
|
1935
|
+
neutral-0 and the pair's ink is neutral-0 too: right by coincidence, not by rule.
|
|
1936
|
+
|
|
1937
|
+
Keyed on the background classes rather than on any component, so a card, a split panel, a stack
|
|
1938
|
+
band, a full-field band and a `default` slide on a colour all behave the same, and the next
|
|
1939
|
+
surface to take a named background inherits the behaviour instead of having to remember it.
|
|
1940
|
+
*/
|
|
1941
|
+
:is(.slidev-layout, .slidev-layout *):is(.bg-warm, .bg-accent, .bg-vivid, .bg-neutral) :where(h1, h2, h3, h4, h5, h6, strong, b, a),
|
|
1942
|
+
:is(.slidev-layout, .slidev-layout *):is(.bg-warm, .bg-accent, .bg-vivid, .bg-neutral) > .deck-eyebrow {
|
|
1943
|
+
color: inherit;
|
|
1944
|
+
}
|
|
1945
|
+
/* ⚠ `li` IS NOT IN THIS SET, and it was for one commit. `opacity` on the item applies to its
|
|
1946
|
+
::before as well — a child cannot opt out of an ancestor's alpha — so dimming list text to 0.85
|
|
1947
|
+
dimmed the bullet with it and dropped the vivid pairing from 3.55 to 2.86, under the 3:1 floor.
|
|
1948
|
+
The audit caught it on the specimen's own vivid slide. A list item reads at full ink instead,
|
|
1949
|
+
which is right anyway: a list here is a set of short statements, not running prose. */
|
|
1950
|
+
:is(.slidev-layout, .slidev-layout *):is(.bg-warm, .bg-accent, .bg-vivid, .bg-neutral) :where(p, .deck-caption) {
|
|
1951
|
+
color: inherit;
|
|
1952
|
+
}
|
|
1953
|
+
/*
|
|
1954
|
+
⚠ THE 0.85 IS A CARD IDIOM AND STAYS ON THE CARD. Generalising it to every named background — one
|
|
1955
|
+
commit — dimmed a paragraph that had stated its own colour on purpose: the Ink specimen draws each
|
|
1956
|
+
role as a `p` with an inline `color`, and an inline colour survives `color: inherit` but nothing
|
|
1957
|
+
survives an ancestor's alpha. --deck-fg-subtle on the accent panel's dark half fell from its own
|
|
1958
|
+
value to #636767 and scored 2.70:1, under the 3:1 floor. The audit named the slide.
|
|
1959
|
+
|
|
1960
|
+
A claim card sits its body copy back from its heading because the card is one short statement. A
|
|
1961
|
+
panel, a band or a full-field slide is a page, and its paragraphs are the page's body — they read
|
|
1962
|
+
at full ink like every other slide's.
|
|
1963
|
+
*/
|
|
1964
|
+
.slidev-layout :is(.deck-card.bg-warm, .deck-card.bg-accent, .deck-card.bg-vivid, .deck-card.bg-neutral) :where(p, .deck-caption) {
|
|
1965
|
+
opacity: 0.85;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/* ── Own-ground components — a Card on a named background ──────────────────────────────────────────────
|
|
1969
|
+
An own-ground component establishes its OWN fill/ink pair rather than reading the page's, and a
|
|
1970
|
+
Card on a named background is the last one: it paints --deck-bg-<name> with --deck-on-bg-<name>.
|
|
1971
|
+
The ink is set once on the component's root, so any descendant that plainly INHERITS colour reads
|
|
1972
|
+
correctly for free. The
|
|
1973
|
+
element rules above this comment do not inherit: `strong`/`b`/`a`/headings/`.deck-caption`/
|
|
1974
|
+
`.deck-note` all carry HARD colours at (0,2,0), which beat inheritance outright regardless of what
|
|
1975
|
+
ground they sit on or how deep they are nested.
|
|
1976
|
+
|
|
1977
|
+
⚠ THE TWO COMPONENTS THIS WAS WRITTEN FOR ARE BOTH GONE — Chip, whose `tone` picked a saturated
|
|
1978
|
+
fill, and Callout, which painted its own paper. The measurements that produced the rule are kept
|
|
1979
|
+
because they are the evidence for it and it still applies: `<Chip>` at its default tone containing
|
|
1980
|
+
`**12%**` rendered the `<strong>` at 1.00:1 (--deck-accent text on a fill of the exact same
|
|
1981
|
+
value), and `<Card variant="accent"><Callout>…</Callout></Card>` rendered white body copy on
|
|
1982
|
+
Callout's own near-white paper at ~1.02:1. The second one is why the Card rules above exist.
|
|
1983
|
+
|
|
1984
|
+
Fixed HERE, in base.css, rather than in each component's own scoped `<style>`, for two reasons:
|
|
1985
|
+
|
|
1986
|
+
1. A Vue SFC's scoped CSS and this global stylesheet land in the SAME (0,2,0) specificity band
|
|
1987
|
+
for a single-class selector, and which one wins a tie then depends on stylesheet injection
|
|
1988
|
+
order — unpredictable across dev and a production build. Landing the override in the same
|
|
1989
|
+
file as the rules it overrides turns that into a plain textual "declared later wins".
|
|
1990
|
+
2. It also has to beat the NAMED-BACKGROUND CARD rule directly above (`:where(p, .deck-caption)`), which
|
|
1991
|
+
is a plain descendant selector with NO exemption for a nested component that establishes its
|
|
1992
|
+
own ground — so it wins regardless of how deep `.deck-chip`/`.deck-callout` is nested unless
|
|
1993
|
+
something here specifically out-ranks or out-orders it. Both rules below tie that rule's
|
|
1994
|
+
(0,2,0) specificity and are declared AFTER it, so the tie-break favours the component's own
|
|
1995
|
+
ground no matter how deeply it's nested inside a named-background card.
|
|
1996
|
+
|
|
1997
|
+
`currentColor` rather than a specific token, deliberately: it resolves to whatever ink the
|
|
1998
|
+
component actually set — Chip's per-tone ink, Callout's fixed `--deck-fg` — with ONE rule
|
|
1999
|
+
instead of one per tone, and it keeps working if a tone is ever added or renamed. `opacity: 1`
|
|
2000
|
+
is explicit rather than left to inherit the named-background card rule's 0.85 above: both components'
|
|
2001
|
+
own pairs are already independently measured against the body-text floor (see
|
|
2002
|
+
test/frontmatter.static.test.ts's "chip pairs clear the contrast floor" suite), and diluting
|
|
2003
|
+
that with an unrelated opacity would spend back margin the fix just put in.
|
|
2004
|
+
|
|
2005
|
+
The next component that establishes its own ground (rather than reading the page's) needs the
|
|
2006
|
+
same shape of rule, with its own root class beside `.deck-callout`. (`.deck-chip` was the other one until Chip
|
|
2007
|
+
was retired — its tones live on as BadgeStrip's and IconTile's.) */
|
|
2008
|
+
/* List markers need the SAME treatment but were missed the first time round: `ul > li::before`
|
|
2009
|
+
(the bullet disc) and `li ul > li::before` (its nested tier) paint through `background`, not
|
|
2010
|
+
`color`, so adding them to the `:where(...)` list above would do nothing — `color: currentColor`
|
|
2011
|
+
cannot reach a `background` declaration. Measured: `<Chip><ul><li>…</li></ul></Chip>` at the
|
|
2012
|
+
default tone (`forest`) painted its bullet in `--deck-accent` on a `--deck-bg-vivid-dark` fill of
|
|
2013
|
+
the identical value, 1.00:1 — the original Critical, unchanged, for markup a demo slide never
|
|
2014
|
+
exercised. `.deck-callout` gets the same fix for the same reason a component with its own
|
|
2015
|
+
ground always does (see the comment above), even though Callout's own ground/ink pair was never
|
|
2016
|
+
independently reachable-broken for a marker the way Chip's tone-fill was — no configuration
|
|
2017
|
+
pairs `--deck-accent` against a Callout ground dark enough to fail 3:1.
|
|
2018
|
+
|
|
2019
|
+
The nested tier stays QUIETER on purpose — `.slidev-layout li ul > li::before` above steps down
|
|
2020
|
+
to `--deck-fg-subtle` rather than repeating the accent, and that hierarchy is worth keeping
|
|
2021
|
+
rather than flattening both tiers to the same full-strength `currentColor`. `opacity: 0.6` is a
|
|
2022
|
+
measured choice, not a guess: blending each own-ground pair's ink over its own fill at candidate
|
|
2023
|
+
opacities and re-running contrast() against WCAG's 3:1 floor for a non-text/graphical object
|
|
2024
|
+
(not the 4.5:1 body-text floor the rules above target — a bullet is UI chrome, not a sentence)
|
|
2025
|
+
put the worst of the six pairs measured at the time at 3.37:1 at 0.6, clear of the floor — and
|
|
2026
|
+
that worst pair was the forest-700 fill, the SAME one Critical 1's text repro used, not the bark
|
|
2027
|
+
step (a first, uncomputed guess would have picked wrong: bark measures a comfortable 3.63:1 at
|
|
2028
|
+
0.6). 0.5 was tried first and rejected — it drops forest-700 to 2.82:1 and the peach step to
|
|
2029
|
+
2.76:1, both under the floor. (Those were the six poetic CHIP names, which no longer exist; the
|
|
2030
|
+
numbers are kept because they are the measurement this 0.6 rests on, and the fills they name are
|
|
2031
|
+
still in the table as `vivid-dark`, `warm-dark` and `warm-light`.)
|
|
2032
|
+
|
|
2033
|
+
★ fix round 1, CRITICAL 1 re-measured this once the accent Card joined `.deck-own-ground`
|
|
2034
|
+
(below). That card painted `--deck-accent` with `--deck-on-accent`: light forest-700 / #ffffff,
|
|
2035
|
+
3.37:1 at 0.6, and dark forest-300 / forest-950, 3.32:1.
|
|
2036
|
+
|
|
2037
|
+
★★ RE-MEASURED AGAIN, ALL EIGHT, when that card became `background` × `dark` and started reading
|
|
2038
|
+
the named-backgrounds table instead. Neither of the two numbers above survives the move, and
|
|
2039
|
+
assuming they did was wrong twice over: the fills are now the table's, so five of the eight
|
|
2040
|
+
halves had never been measured here at all, and the ink is `--deck-on-bg-<name>` — neutral-0
|
|
2041
|
+
(#FEFBFC), not the #ffffff `--deck-on-accent` was. Blending each half's ink over its own fill,
|
|
2042
|
+
marker at 0.6 and body at 0.85:
|
|
2043
|
+
|
|
2044
|
+
warm-light warm-200 / warm-950 3.51 · 6.91 accent-light forest-100 / forest-950 4.01 · 8.61
|
|
2045
|
+
warm-dark warm-700 / neutral-0 3.57 · 5.44 accent-dark forest-950 / neutral-0 6.20 · 11.20
|
|
2046
|
+
vivid-light forest-300/ forest-950 3.32 · 6.11 neutral-light neutral-100/ neutral-900 4.49 · 10.33
|
|
2047
|
+
vivid-dark forest-700/ neutral-0 3.31 · 5.02 neutral-dark neutral-800/ neutral-0 6.21 · 10.94
|
|
2048
|
+
|
|
2049
|
+
Every half clears both floors, and the true worst case is vivid-dark at 3.31:1 — not the 3.32
|
|
2050
|
+
the old note named, and not the 3.37 it called the worst, which only reproduces with the retired
|
|
2051
|
+
pure-white ink. 0.85 bottoms out on the same half at 5.02:1 against the 4.5 body floor.
|
|
2052
|
+
|
|
2053
|
+
The top-tier bullet/marker rules below need no equivalent `opacity: 1` guard: `opacity` doesn't
|
|
2054
|
+
inherit, and nothing else in this file sets it on `li::before`/`li::marker`, so browser-default
|
|
2055
|
+
full strength is what they get without asking.
|
|
2056
|
+
|
|
2057
|
+
Specificity verified rather than assumed, because `li ul > li::before` carries one more element
|
|
2058
|
+
than `ul > li::before` does: `.slidev-layout .deck-own-ground ul > li::before` is (0,2,3) against
|
|
2059
|
+
the base rule's (0,1,3); `.slidev-layout .deck-own-ground li ul > li::before` is (0,2,4) against
|
|
2060
|
+
(0,1,4). Both win outright on specificity, unlike the text rules above — no reliance on source
|
|
2061
|
+
order.
|
|
2062
|
+
|
|
2063
|
+
★★ fix round 1, CRITICAL 1 — this used to be `.deck-chip ul > li::before, .deck-callout ul >
|
|
2064
|
+
li::before` (and the two rules below it, same shape): an enumerated pair, one selector per
|
|
2065
|
+
component, exactly the thing "the next component that establishes its own ground needs the same
|
|
2066
|
+
rule" (this file's own words, two paragraphs up) warned would need REMEMBERING. It did not get
|
|
2067
|
+
remembered: the accent Card (today `.deck-card.bg-*`, "A card on a named background" above)
|
|
2068
|
+
established its own ground exactly like Chip and Callout did — `background: var(--deck-accent)`
|
|
2069
|
+
— and was never added here. A plain
|
|
2070
|
+
markdown bullet list written directly inside the accent Card (a documented API, used
|
|
2071
|
+
three times in apps/deck-template) painted its bullet in the GLOBAL `ul > li::before` rule's
|
|
2072
|
+
`background: var(--deck-accent)` — the exact same value as its own ground. 1.00:1, invisible, in
|
|
2073
|
+
both registers, and scripts/deckAudit.ts's rendered-contrast sweep could not see it either: its
|
|
2074
|
+
collector only ever scored elements with a direct text-node child, and a `::before`/`::marker`
|
|
2075
|
+
pseudo-element is neither an element nor a text node (fixed separately, see deckAudit.ts's own
|
|
2076
|
+
comment on `PSEUDO_SELECTORS`).
|
|
2077
|
+
|
|
2078
|
+
Fixed HERE, once, by construction rather than by memory: `.deck-own-ground` is a plain marker
|
|
2079
|
+
class with no styling of its own, added to the ROOT of every component that paints its own
|
|
2080
|
+
fill/ink pair (then Chip.vue and Callout.vue; today Card.vue on a named background) instead of
|
|
2081
|
+
reading the page's.
|
|
2082
|
+
The three rules below key off that ONE class, so the next own-ground component only has to add
|
|
2083
|
+
the class to its root — nothing here has to change, and nothing here can be forgotten to
|
|
2084
|
+
change. */
|
|
2085
|
+
/* ⚠ `:is(.slidev-layout.deck-own-ground, .slidev-layout .deck-own-ground)`, NOT the descendant
|
|
2086
|
+
alone. Every own-ground component so far has been a box INSIDE a slide, so a plain descendant
|
|
2087
|
+
selector reached all of them and nobody noticed it could not reach a LAYOUT that establishes its
|
|
2088
|
+
own ground — default.vue on a named `background` puts the class on `.slidev-layout` itself, which
|
|
2089
|
+
is the element, not a descendant of it. Measured before this line was widened: a bullet on a
|
|
2090
|
+
`background: vivid` slide painted --deck-accent, forest-300 on a forest-700 field. It passed the
|
|
2091
|
+
audit at 3.55:1 and was still the wrong colour — the one green mark on a slide whose ink is
|
|
2092
|
+
white — which is why the specimen draws a list on every one of those five slides. */
|
|
2093
|
+
:is(.slidev-layout.deck-own-ground, .slidev-layout .deck-own-ground) ul > li::before {
|
|
2094
|
+
background: currentColor;
|
|
2095
|
+
}
|
|
2096
|
+
/*
|
|
2097
|
+
★★ EXCEPT ON A NAMED BACKGROUND, WHERE THE MARK TAKES THAT NAME'S OWN FAMILY. `currentColor` is
|
|
2098
|
+
the safe answer for an own-ground surface whose fill is arbitrary — a solid Alert paints a status
|
|
2099
|
+
colour, and a dot in some other hue on it would say the wrong thing. A NAMED background is not
|
|
2100
|
+
arbitrary: the engine publishes a mark for each one, `--deck-bg-<name>-mark`, which is the
|
|
2101
|
+
liveliest step of that family still clearing the 3:1 a graphical object is held to against the
|
|
2102
|
+
fill it lands on. Orange on peach and on bark, the vivid green on both accent fields, mint on the
|
|
2103
|
+
dark vivid one. See PANEL in scripts/deck.ts for how each was chosen and what it measures.
|
|
2104
|
+
|
|
2105
|
+
One rule per name and no register branch: the token is re-declared under `.deck-dark`, so a panel
|
|
2106
|
+
carrying that class picks up its own half without this file knowing which register it is in.
|
|
2107
|
+
|
|
2108
|
+
★ WHY NOT --deck-accent EVERYWHERE, which was the first attempt. It measures fine on all eight
|
|
2109
|
+
halves, 3.55 worst. It also puts a forest dot on a peach panel and on a grey one, which is a
|
|
2110
|
+
second hue on a slide that had already chosen one. The deck's own ground keeps the accent, because
|
|
2111
|
+
there the accent IS the slide's colour.
|
|
2112
|
+
*/
|
|
2113
|
+
/* A mermaid edge label knocks its own line out, so its backing takes whatever ground it is over —
|
|
2114
|
+
the page by default (--deck-bg, already per-register), or the named fill when it is on one. Read
|
|
2115
|
+
inside the SVG by setup/mermaid-renderer.ts; a custom property is what carries across that
|
|
2116
|
+
boundary, since the diagram cannot see the class its container wears. */
|
|
2117
|
+
:is(.slidev-layout, .slidev-layout *).bg-warm { --deck-mermaid-label-bg: var(--deck-bg-warm); --deck-mermaid-label-ink: var(--deck-ink-warm); }
|
|
2118
|
+
:is(.slidev-layout, .slidev-layout *).bg-accent { --deck-mermaid-label-bg: var(--deck-bg-accent); --deck-mermaid-label-ink: var(--deck-ink-accent); }
|
|
2119
|
+
:is(.slidev-layout, .slidev-layout *).bg-vivid { --deck-mermaid-label-bg: var(--deck-bg-vivid); --deck-mermaid-label-ink: var(--deck-ink-vivid); }
|
|
2120
|
+
:is(.slidev-layout, .slidev-layout *).bg-neutral { --deck-mermaid-label-bg: var(--deck-bg-neutral); --deck-mermaid-label-ink: var(--deck-ink-neutral); }
|
|
2121
|
+
|
|
2122
|
+
:is(.slidev-layout, .slidev-layout *).bg-warm ul > li::before { background: var(--deck-bg-warm-mark); }
|
|
2123
|
+
:is(.slidev-layout, .slidev-layout *).bg-accent ul > li::before { background: var(--deck-bg-accent-mark); }
|
|
2124
|
+
:is(.slidev-layout, .slidev-layout *).bg-vivid ul > li::before { background: var(--deck-bg-vivid-mark); }
|
|
2125
|
+
:is(.slidev-layout, .slidev-layout *).bg-neutral ul > li::before { background: var(--deck-bg-neutral-mark); }
|
|
2126
|
+
|
|
2127
|
+
/*
|
|
2128
|
+
★★ AND THE CHIP'S INK FOLLOWS THE SAME GROUND, which is what makes the wash above land in the
|
|
2129
|
+
family rather than beside it. --deck-accent is the deck's green and is right on the page; on a
|
|
2130
|
+
named background it is a third family in a word that already has two.
|
|
2131
|
+
|
|
2132
|
+
IT IS THE FAMILY'S INK, NOT ITS MARK, AND THAT IS MEASURED. The mark tone carries the bullets one
|
|
2133
|
+
rule up, and bullets are a GRAPHICAL mark held to 3:1. Code is text and owes 4.5:1, which the mark
|
|
2134
|
+
misses on two of the four:
|
|
2135
|
+
|
|
2136
|
+
family ground mark mark:ground ink ink:ground
|
|
2137
|
+
warm #FFD6A6 #A75633 3.83 ✗ #44231C 10.26 ✓
|
|
2138
|
+
accent #CEF5DD #046C4C 5.45 ✓ #002A24 13.07 ✓
|
|
2139
|
+
vivid #3BD996 #075640 4.78 ✓ #002A24 8.51 ✓
|
|
2140
|
+
neutral #F3EFF0 #757273 4.18 ✗ #191617 15.76 ✓
|
|
2141
|
+
|
|
2142
|
+
So the chip takes --deck-ink-<name>, the same ink the prose around it takes. It stops being a
|
|
2143
|
+
different COLOUR from its sentence and stays a different THING: mono, boxed, edged. The fill and
|
|
2144
|
+
the border derive from this colour, so naming the ink moves all three.
|
|
2145
|
+
*/
|
|
2146
|
+
:is(.slidev-layout, .slidev-layout *).bg-warm :not(pre) > code:not(.shiki) { color: var(--deck-ink-warm); }
|
|
2147
|
+
:is(.slidev-layout, .slidev-layout *).bg-accent :not(pre) > code:not(.shiki) { color: var(--deck-ink-accent); }
|
|
2148
|
+
:is(.slidev-layout, .slidev-layout *).bg-vivid :not(pre) > code:not(.shiki) { color: var(--deck-ink-vivid); }
|
|
2149
|
+
:is(.slidev-layout, .slidev-layout *).bg-neutral :not(pre) > code:not(.shiki) { color: var(--deck-ink-neutral); }
|
|
2150
|
+
:is(.slidev-layout.deck-own-ground, .slidev-layout .deck-own-ground) ol > li::marker {
|
|
2151
|
+
color: currentColor;
|
|
2152
|
+
}
|
|
2153
|
+
:is(.slidev-layout.deck-own-ground, .slidev-layout .deck-own-ground) li ul > li::before {
|
|
2154
|
+
background: currentColor;
|
|
2155
|
+
opacity: 0.6;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/* ── The notch — ONE cut, worn by every figure that seats the running mark ────────────────
|
|
2159
|
+
task-15-brief.md built this shape; task-18-brief.md moved it here. It used to live inside
|
|
2160
|
+
split.vue's scoped <style>, which was correct while split was the only layout with a panel to
|
|
2161
|
+
bite out of. stack.vue's band is the second, cutting the same corner around the same mark, and a
|
|
2162
|
+
five-layer composited mask is not a thing this theme can afford two copies of — the geometry is
|
|
2163
|
+
the DESIGN, not one layout's implementation detail.
|
|
2164
|
+
|
|
2165
|
+
Each layout still owns what is genuinely its own: whether there is a cut at all (../notch.js's
|
|
2166
|
+
hasNotch(), which both call), which corner is the outer one (`data-notch-corner`, bound in the
|
|
2167
|
+
template because it flips with split's `side`), the margin that raises the figure to meet the
|
|
2168
|
+
mark, and the band a text-holding panel reserves from its own content.
|
|
2169
|
+
|
|
2170
|
+
★★ NOT A STRETCHED MASK — the requirement that decides the implementation. Jose: "i mean, not
|
|
2171
|
+
just stretch a fixed mask". The wrong build authors one shape and scales it to whatever size the
|
|
2172
|
+
figure happens to be, so a 48px corner becomes an ellipse on a narrow panel and flattens on a
|
|
2173
|
+
wide one; across split's six ratios that is six different corner treatments where the design says
|
|
2174
|
+
there is one. So the mask is COMPOSED from gradients in ABSOLUTE units and composited: gradient
|
|
2175
|
+
geometry sized in px does not stretch with its box, and only the figure's own width and height
|
|
2176
|
+
differ between ratios. Nothing here is authored at an aspect ratio, so nothing here has one to
|
|
2177
|
+
distort — there is no viewBox, no `mask-size: 100% 100%` on a shape, and no reused data URI.
|
|
2178
|
+
Measured rather than asserted, at both extremes of the ratio ladder — see task-15-report.md, and
|
|
2179
|
+
task-18-report.md for the same measurement repeated after this move.
|
|
2180
|
+
|
|
2181
|
+
FIVE LAYERS, and what each one is. Written top-first (CSS layer order), composited bottom-up:
|
|
2182
|
+
|
|
2183
|
+
FULL the whole figure subtract (everything below)
|
|
2184
|
+
RETURN-A the convex fillet where the cut meets the figure's TOP edge add
|
|
2185
|
+
RETURN-B the convex fillet where the cut meets the figure's OUTER edge add
|
|
2186
|
+
RECT the cut itself: occupant + one gap, anchored in the corner subtract (the sliver below)
|
|
2187
|
+
SLIVER the concave corner's own curve, kept OUT of the cut add
|
|
2188
|
+
|
|
2189
|
+
So the removed region is (RECT − SLIVER) ∪ RETURN-A ∪ RETURN-B, and the figure is everything
|
|
2190
|
+
else. The three curved layers are the SAME shape — a square minus a disc of radius --notch-r
|
|
2191
|
+
pinned to one of its corners — placed three times. That single shape is what produces the
|
|
2192
|
+
S-curve: rounding the figure's two convex corners takes material away, rounding the cut's concave
|
|
2193
|
+
corner gives it back, and all three arcs share one radius.
|
|
2194
|
+
|
|
2195
|
+
Each curved layer overlaps its neighbour by --notch-overlap. Mask layers composite with
|
|
2196
|
+
source-over, so two ANTIALIASED edges that merely abut sum to ~0.75 alpha and leave a hairline
|
|
2197
|
+
seam; a 1px overlap removes the seam without moving any curve (the overlap extends into territory
|
|
2198
|
+
the neighbouring layer already covers). The 0.5px feather on each colour stop is the other half of
|
|
2199
|
+
the same concern: a hard gradient stop is rasterised without antialiasing and gives a stepped arc.
|
|
2200
|
+
|
|
2201
|
+
⚠ MASKING CLIPS, IT DOES NOT REFLOW. The mask handles the SHAPE; the SPACE is each layout's own
|
|
2202
|
+
problem, because only the layout knows whether its figure holds a photograph (clipping it IS the
|
|
2203
|
+
effect) or words (which would silently lose a corner). See split.vue's `.is-panel.has-notch` and
|
|
2204
|
+
stack.vue's `.stack-band.is-panel`. */
|
|
2205
|
+
.deck-notch {
|
|
2206
|
+
/*
|
|
2207
|
+
★ DERIVED FROM THE OCCUPANT, never pinned beside it — the property that makes this a system
|
|
2208
|
+
feature rather than a shape someone drew once. The occupant's box is the input and the cut is
|
|
2209
|
+
that box plus one gap, so changing the mark (--deck-chrome-mark, scripts/deck.ts) moves both
|
|
2210
|
+
with no second edit here, and a slide that widens the occupant — a forest+partner pair
|
|
2211
|
+
(`partner:`), an image, a content block — widens the cut by the same amount with no code change
|
|
2212
|
+
at all.
|
|
2213
|
+
|
|
2214
|
+
The default is written as var()'s FALLBACK rather than as a declaration in deck.css: a custom
|
|
2215
|
+
property resolves once, where it is declared, so anything derived from an occupant declared at
|
|
2216
|
+
:root would keep the :root answer even on a slide that overrode the occupant further down.
|
|
2217
|
+
Undeclared, the override reaches every reader — this rule and slide-top.vue's own mark box.
|
|
2218
|
+
|
|
2219
|
+
--notch-r is clamped to half the cut on each axis. At the shipped numbers (108px cut, 48px
|
|
2220
|
+
radius) the straight segments are 12px and the profile is very nearly a pure S; a SMALLER
|
|
2221
|
+
occupant would otherwise ask for arcs that overlap each other, and min() degrades that to a
|
|
2222
|
+
clean S-curve instead of a self-intersecting shape.
|
|
2223
|
+
*/
|
|
2224
|
+
--notch-w: calc(var(--deck-notch-occupant-w, var(--deck-chrome-mark)) + var(--deck-notch-gap));
|
|
2225
|
+
--notch-h: calc(var(--deck-notch-occupant-h, var(--deck-chrome-mark)) + var(--deck-notch-gap));
|
|
2226
|
+
--notch-r: min(var(--deck-notch-radius), calc(var(--notch-w) / 2), calc(var(--notch-h) / 2));
|
|
2227
|
+
/*
|
|
2228
|
+
★★ THE CUT IS A CLIP, NOT A MASK — and that is a correctness decision, not a preference.
|
|
2229
|
+
|
|
2230
|
+
This was five mask layers composited `subtract, add, add, subtract, add`. It drew the right
|
|
2231
|
+
shape and it leaked: a hairline of panel painted out in the ground, tracing the figure's edges
|
|
2232
|
+
and the cut rectangle's inner corner. A mask is ALPHA, so every layer rasterises independently
|
|
2233
|
+
and any boundary that lands on a fractional device pixel resolves to partial coverage —
|
|
2234
|
+
`subtract` then turns that into `1 - partial`, a few percent of panel left painted where the
|
|
2235
|
+
cut should be empty. It is a colour artifact, so no geometric gate could see it, and it moved
|
|
2236
|
+
with the window size, which made it read like a scaling bug.
|
|
2237
|
+
|
|
2238
|
+
Four attempts to patch it boundary-by-boundary all failed, and each is recorded in the history
|
|
2239
|
+
rather than quietly dropped: widening the layer overlap, bleeding the cut layers past the box,
|
|
2240
|
+
bleeding the base layer too, and promoting the element to its own compositing layer. Promotion
|
|
2241
|
+
cleared the outer edges and left the inner corner, which is the tell — there was no single bad
|
|
2242
|
+
boundary, the arithmetic itself was wrong for the job.
|
|
2243
|
+
|
|
2244
|
+
A clip is GEOMETRY. One path, rasterised once, antialiased once, with no compositing arithmetic
|
|
2245
|
+
to leave a residue behind. That removes the whole class of defect rather than the instances of
|
|
2246
|
+
it, which is why this is the shape of the fix.
|
|
2247
|
+
|
|
2248
|
+
VERIFIED NUMERICALLY, not by eye — every earlier visual judgement in this investigation was
|
|
2249
|
+
wrong at least once. clip-path participates in hit-testing, so the rendered region can be
|
|
2250
|
+
sampled directly: 3,481 points per corner, on both corners, checked against an independent
|
|
2251
|
+
analytic model of the intended profile. Zero mismatches. The check was proved able to fail
|
|
2252
|
+
first — shrinking the inner radius from 48 to 30 produced 34 mismatches.
|
|
2253
|
+
|
|
2254
|
+
The profile, corner-local, is exactly what the mask drew: a convex return of radius r meeting
|
|
2255
|
+
the top edge at (w + r, 0), a straight run down x = w, a concave quarter-circle of radius r
|
|
2256
|
+
centred on (w - r, h - r), and the mirrored return meeting the side edge at (0, h + r). The
|
|
2257
|
+
two corner variants below differ only by mirroring x and flipping every sweep.
|
|
2258
|
+
*/
|
|
2259
|
+
}
|
|
2260
|
+
/*
|
|
2261
|
+
WHICH CORNER — always the figure's OUTER one: on a split, the corner against the slide edge and
|
|
2262
|
+
away from the text; on a stack's full-width band, the corner the mark is already in. That is where
|
|
2263
|
+
the mark can sit without crowding the content it shares the slide with.
|
|
2264
|
+
|
|
2265
|
+
The cut is anchored to the FIGURE, not to the slide: every offset above is measured from the
|
|
2266
|
+
figure's own edge, so it travels with the panel across split's six ratios (from half the row down
|
|
2267
|
+
to a quarter of it) and across stack's band heights with no per-ratio rule, and would still be
|
|
2268
|
+
correct if a seventh were added.
|
|
2269
|
+
|
|
2270
|
+
Three custom properties carry the corner, because a corner is not just a side: --notch-edge is
|
|
2271
|
+
which physical edge every offset is measured from, and the two *-x values move each disc's centre
|
|
2272
|
+
to the corresponding corner of its own tile. Physical (right/left), never logical: which side is
|
|
2273
|
+
"outer" flips with split's `side`, never with writing direction.
|
|
2274
|
+
|
|
2275
|
+
RIGHT is the default rather than a required attribute — it is where the running mark sits on every
|
|
2276
|
+
slide that has not mirrored — so a figure that forgets the attribute cuts a real corner rather
|
|
2277
|
+
than resolving --notch-edge to nothing and invalidating the whole mask declaration (which renders
|
|
2278
|
+
as an UNCUT figure: a silent failure, the exact shape this project keeps finding).
|
|
2279
|
+
*/
|
|
2280
|
+
.deck-notch,
|
|
2281
|
+
.deck-notch[data-notch-corner='right'] {
|
|
2282
|
+
--notch-edge: right;
|
|
2283
|
+
clip-path: shape(
|
|
2284
|
+
from calc(100% - var(--notch-w) - var(--notch-r)) 0,
|
|
2285
|
+
hline to var(--deck-radius-card),
|
|
2286
|
+
arc to 0 var(--deck-radius-card) of var(--deck-radius-card) ccw,
|
|
2287
|
+
vline to calc(100% - var(--deck-radius-card)),
|
|
2288
|
+
arc to var(--deck-radius-card) 100% of var(--deck-radius-card) ccw,
|
|
2289
|
+
hline to calc(100% - var(--deck-radius-card)),
|
|
2290
|
+
arc to 100% calc(100% - var(--deck-radius-card)) of var(--deck-radius-card) ccw,
|
|
2291
|
+
vline to calc(var(--notch-h) + var(--notch-r)),
|
|
2292
|
+
arc to calc(100% - var(--notch-r)) var(--notch-h) of var(--notch-r) ccw,
|
|
2293
|
+
hline to calc(100% - var(--notch-w) + var(--notch-r)),
|
|
2294
|
+
arc to calc(100% - var(--notch-w)) calc(var(--notch-h) - var(--notch-r)) of var(--notch-r) cw,
|
|
2295
|
+
vline to var(--notch-r),
|
|
2296
|
+
arc to calc(100% - var(--notch-w) - var(--notch-r)) 0 of var(--notch-r) ccw,
|
|
2297
|
+
close);
|
|
2298
|
+
}
|
|
2299
|
+
.deck-notch[data-notch-corner='left'] {
|
|
2300
|
+
--notch-edge: left;
|
|
2301
|
+
clip-path: shape(
|
|
2302
|
+
from calc(var(--notch-w) + var(--notch-r)) 0,
|
|
2303
|
+
hline to calc(100% - var(--deck-radius-card)),
|
|
2304
|
+
arc to 100% var(--deck-radius-card) of var(--deck-radius-card) cw,
|
|
2305
|
+
vline to calc(100% - var(--deck-radius-card)),
|
|
2306
|
+
arc to calc(100% - var(--deck-radius-card)) 100% of var(--deck-radius-card) cw,
|
|
2307
|
+
hline to var(--deck-radius-card),
|
|
2308
|
+
arc to 0 calc(100% - var(--deck-radius-card)) of var(--deck-radius-card) cw,
|
|
2309
|
+
vline to calc(var(--notch-h) + var(--notch-r)),
|
|
2310
|
+
arc to var(--notch-r) var(--notch-h) of var(--notch-r) cw,
|
|
2311
|
+
hline to calc(var(--notch-w) - var(--notch-r)),
|
|
2312
|
+
arc to var(--notch-w) calc(var(--notch-h) - var(--notch-r)) of var(--notch-r) ccw,
|
|
2313
|
+
vline to var(--notch-r),
|
|
2314
|
+
arc to calc(var(--notch-w) + var(--notch-r)) 0 of var(--notch-r) cw,
|
|
2315
|
+
close);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
/* ── Motion ───────────────────────────────────────────────────────────────────────────────
|
|
2319
|
+
★★ THE DECK CROSS-FADES THROUGH THE VIEW TRANSITIONS API, AND THAT IS A CONTRAST FIX RATHER
|
|
2320
|
+
THAN A FLOURISH. `transition: fade` (what this theme shipped) animates BOTH slides' opacity at
|
|
2321
|
+
once over two absolutely-positioned siblings whose own grounds are painted on the layout INSIDE
|
|
2322
|
+
them, so during the overlap neither is opaque and the page behind shows through. That page is
|
|
2323
|
+
white — #slide-content, #app and body all are — and the leak is the product of the two
|
|
2324
|
+
transparencies: measured stepping off a dark divider, 0.25/0.75 at 115ms of the 240ms fade, so
|
|
2325
|
+
18.8% of pure white across the whole frame, peaking near 25% at the crossover. Invisible cream
|
|
2326
|
+
to cream. A flash on every dark slide, and a strobe when an author pages fast.
|
|
2327
|
+
|
|
2328
|
+
`view-transition` is the fix because the UA default composites the pair CORRECTLY: measured on a
|
|
2329
|
+
live transition, `::view-transition-image-pair(root)` carries `isolation: isolate` and both
|
|
2330
|
+
halves `mix-blend-mode: plus-lighter`, so the two opacities SUM to 1 instead of stacking one
|
|
2331
|
+
over the other. Proved on a throwaway page with a magenta backdrop, which never appeared.
|
|
2332
|
+
|
|
2333
|
+
What the API does not bring is the ladder: it ships 0.25s on its own curve and never reads
|
|
2334
|
+
--slidev-transition-duration. Hence the two rules below. The Vue-transition curves are kept
|
|
2335
|
+
because a slide can still opt back into `fade` from its own frontmatter, and Slidev hard-codes
|
|
2336
|
+
`ease` and `ease-in` there — ease-in is the curve the doctrine bans outright, since it starts
|
|
2337
|
+
slow and reads as unresponsive at exactly the moment the audience is watching the slide change. */
|
|
2338
|
+
::view-transition-old(root),
|
|
2339
|
+
::view-transition-new(root) {
|
|
2340
|
+
animation-duration: var(--motion-slow);
|
|
2341
|
+
animation-timing-function: var(--motion-ease-out);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
/* The global floor in packages/tokens/motion/motion.css cannot reach these: it targets
|
|
2345
|
+
`*, *::before, *::after`, and a view-transition pseudo-element is none of the three. Without
|
|
2346
|
+
this block a reduced-motion viewer gets the full cross-fade, which is the one case the floor
|
|
2347
|
+
exists to prevent. Not `animation: none` — removing the animation would leave the old snapshot
|
|
2348
|
+
painted over the new slide, so the pair still has to run, just imperceptibly. */
|
|
2349
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2350
|
+
::view-transition-old(root),
|
|
2351
|
+
::view-transition-new(root) {
|
|
2352
|
+
animation-duration: 1ms !important;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
.slidev-vue-transition-enter-active,
|
|
2357
|
+
.slidev-vue-transition-leave-active,
|
|
2358
|
+
.slidev-page-enter-active,
|
|
2359
|
+
.slidev-page-leave-active {
|
|
2360
|
+
transition-timing-function: var(--motion-ease-out) !important;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/* ── Slidev's own chrome: the closed "go to slide" dialog ─────────────────────────────────
|
|
2364
|
+
UPSTREAM BUG, worked around here. Slidev hides #slidev-goto-dialog by parking it at a FIXED
|
|
2365
|
+
offset — `:class="showGotoDialog ? 'top-5' : '-top-20'"` (internals/Goto.vue) — so the closed
|
|
2366
|
+
dialog sits at top:-80px. That hides 80px. The dialog is not 80px tall: it also renders an
|
|
2367
|
+
autocomplete list under `v-if="result.length > 0"`, and in fuse.js 7.x `search('')` returns
|
|
2368
|
+
EVERY item, so with an empty input the list is every slide in the deck. MEASURED on this deck:
|
|
2369
|
+
563px tall, 483px of it hanging into the canvas at opacity 1 with pointer-events auto — which
|
|
2370
|
+
is why it cannot be dismissed. There is nothing to dismiss; it was never open.
|
|
2371
|
+
|
|
2372
|
+
It scales with slide count, so it is invisible on a two-slide demo and unmissable on a real
|
|
2373
|
+
deck, and it is NOT dev-only: play.vue mounts <Controls> under `v-if="!isPrintMode"` with no
|
|
2374
|
+
__DEV__ guard, so a built deck ships it too. (Slidev here is 0.50.0; upstream is 52.x.)
|
|
2375
|
+
|
|
2376
|
+
Keyed off the class Slidev itself toggles, so the dialog still opens normally. */
|
|
2377
|
+
#slidev-goto-dialog:not(.top-5) {
|
|
2378
|
+
visibility: hidden;
|
|
2379
|
+
pointer-events: none;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
/* v-click reveals: a fade at the near-element bucket, no travel. */
|
|
2383
|
+
.slidev-vclick-target {
|
|
2384
|
+
transition:
|
|
2385
|
+
opacity var(--motion-base) var(--motion-ease-out),
|
|
2386
|
+
color var(--motion-base) var(--motion-ease-out);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
/* ── Reduced motion ───────────────────────────────────────────────────────────────────────
|
|
2390
|
+
Nothing here. The system-wide floor arrives with the ladder, in
|
|
2391
|
+
@humanforest/tokens/motion/motion.css (imported by styles/index.ts): it targets
|
|
2392
|
+
`*, *::before, *::after`, which already covers every slide.
|
|
2393
|
+
|
|
2394
|
+
This file used to carry its own copy, scoped to `.slidev-layout *` and `[class*='slidev-']`.
|
|
2395
|
+
That copy was not merely redundant, it was NARROWER — its allow-list omitted `box-shadow`, so a
|
|
2396
|
+
deck honouring reduced motion also lost shadow fades the product keeps. Deleting it is the point
|
|
2397
|
+
of importing the real thing. */
|
|
2398
|
+
|
|
2399
|
+
/*
|
|
2400
|
+
── THE CHART TOOLTIP, THEMED AT THE ROOT BECAUSE IT IS MOUNTED ON THE BODY ────────────────────
|
|
2401
|
+
|
|
2402
|
+
★★ A TOOLTIP INSIDE A SLIDE IS POSITIONED WRONG, and the error grows with distance. Slidev draws
|
|
2403
|
+
the 1920x1080 canvas by SCALING it — `matrix(0.579687, …)` at the size this deck was measured at —
|
|
2404
|
+
and Unovis positions its panel by writing screen pixels into `left`/`bottom`. Inside the scaled
|
|
2405
|
+
slide those pixels are then scaled AGAIN: measured on the Grouped bars slide, a pointer at x 700
|
|
2406
|
+
put the panel at x 264, several columns to its left. Near the origin it looks almost right, which
|
|
2407
|
+
is what makes it a late discovery rather than an obvious one.
|
|
2408
|
+
|
|
2409
|
+
`document.body` is the only ancestor the slide's transform does not reach, which is why the
|
|
2410
|
+
product mounts its tooltip there too — for a different reason (clamping inside a short chart), and
|
|
2411
|
+
arriving at the same place.
|
|
2412
|
+
|
|
2413
|
+
⚠ THE COST, STATED: a panel in the body cannot inherit a slide's register, so a tooltip on a
|
|
2414
|
+
`deck-dark` slide is drawn in the light one. That is the right trade here — it reads as an overlay
|
|
2415
|
+
above the deck rather than a surface of it, and the alternative is a panel that points at the
|
|
2416
|
+
wrong column.
|
|
2417
|
+
*/
|
|
2418
|
+
/*
|
|
2419
|
+
★ AND IT IS SCALED BY HAND, because leaving the slide's transform also left its SIZE behind. The
|
|
2420
|
+
panel is themed at the deck's own rung — 36px type, room-scale padding — which is correct inside a
|
|
2421
|
+
1920x1080 canvas and far too large sitting in the body beside a canvas drawn at 0.58. Slidev
|
|
2422
|
+
publishes the factor it is using as `--slidev-slide-scale`, so the panel takes the same one and
|
|
2423
|
+
ends up the size it would have been had it stayed inside the slide.
|
|
2424
|
+
|
|
2425
|
+
★★ THE SCALE IS `zoom`, ON THE CONTENT, AND `transform` IS THE BUG IT REPLACES. Unovis places the
|
|
2426
|
+
panel from `this.element.offsetWidth/offsetHeight` — it subtracts the panel's own HEIGHT to sit it
|
|
2427
|
+
above the pointer, halves its WIDTH to centre it, and clamps both against the viewport. A
|
|
2428
|
+
transform paints a smaller box without changing either measurement, so the library reserved
|
|
2429
|
+
397x204 for a panel that drew 230x118 and placed the difference as empty air: measured on the
|
|
2430
|
+
scatter slide, the panel floated 103px above the pointer — exactly `5 + 12 + 0.42 x 204` — and the
|
|
2431
|
+
clamp fired at x 191 for a box that was never 397 wide.
|
|
2432
|
+
|
|
2433
|
+
`zoom` is laid out rather than painted, so the element's own measurements come back at the size it
|
|
2434
|
+
draws (231x119, both) and the library's arithmetic is right again — including the flip to below
|
|
2435
|
+
the pointer near the top edge, and the clamp. It sits on the CONTENT because zoom on the panel
|
|
2436
|
+
itself would scale the `left`/`top` the library just wrote.
|
|
2437
|
+
|
|
2438
|
+
Radius is the one thing left outside the zoom, on the panel's own box, so it carries the factor by
|
|
2439
|
+
hand or a 24px corner arrives at full size on a panel drawn at 58%.
|
|
2440
|
+
|
|
2441
|
+
Matched on the class Unovis emits rather than on `body > div`, and on the deck's own root so it
|
|
2442
|
+
cannot reach a chart rendered anywhere else.
|
|
2443
|
+
*/
|
|
2444
|
+
body > [class*='-tooltip'] > * {
|
|
2445
|
+
zoom: var(--slidev-slide-scale, 1);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
body > [class*='-tooltip'] {
|
|
2449
|
+
|
|
2450
|
+
/*
|
|
2451
|
+
★★ DECLARED ON THE PANEL, NOT ON `:root`, AND THAT IS NOT TIDINESS. Unovis injects its own
|
|
2452
|
+
stylesheet at RUNTIME, after this one, and its defaults sit on `:root` too — so a declaration
|
|
2453
|
+
there ties on specificity and loses on order. Measured: the corner came out at Unovis' own 5px
|
|
2454
|
+
while this file asked for 24. Inside the chart the same variables worked, because ChartFrame is
|
|
2455
|
+
a nearer ancestor than `:root` and proximity settles an inherited value; moving the panel to the
|
|
2456
|
+
body took that advantage away. Set on the element itself, nothing inherited can reach it.
|
|
2457
|
+
*/
|
|
2458
|
+
/*
|
|
2459
|
+
★ THE FADE WAS OFF THE LADDER IN BOTH HALVES. Unovis' root sets `transition: opacity` with a
|
|
2460
|
+
300ms default and no curve, so the panel ran the browser's `ease` — the same fault this file
|
|
2461
|
+
already corrects for Slidev's own slide transitions further up. The doctrine names this exact
|
|
2462
|
+
element: `fast` is "small / near elements — tooltips", and every curve in the system is
|
|
2463
|
+
ease-out. The shorthand in Unovis' emotion class is a single class, so declaring the curve here
|
|
2464
|
+
outranks it.
|
|
2465
|
+
|
|
2466
|
+
⚠ ONE DURATION FOR BOTH DIRECTIONS, where the product's tooltip enters at `fast` and leaves at
|
|
2467
|
+
`instant`. Unovis carries the two states on emotion classes built WITHOUT a label — they render
|
|
2468
|
+
as bare hashes (`css-eec57t`, `css-38lglc`) that move on any version bump — so splitting the
|
|
2469
|
+
two would mean pinning a hash, which is the landmine removed from ScatterChart in e8a893c. The
|
|
2470
|
+
enter bucket is the one an audience sees; the exit runs 60ms long and is not worth that.
|
|
2471
|
+
|
|
2472
|
+
The fade itself survives `prefers-reduced-motion`: the floor's allow-list keeps `opacity`,
|
|
2473
|
+
because this is a colour change rather than travel. It reaches the panel at all only because
|
|
2474
|
+
that floor targets `*` — the deck-local copy this file deleted was scoped to `.slidev-layout *`,
|
|
2475
|
+
which a body-mounted panel is not inside.
|
|
2476
|
+
*/
|
|
2477
|
+
--vis-tooltip-transition-duration: var(--motion-fast);
|
|
2478
|
+
transition-timing-function: var(--motion-ease-out);
|
|
2479
|
+
|
|
2480
|
+
/*
|
|
2481
|
+
★ GLASS, THE SAME AS THE PRODUCT'S. packages/tokens/dataviz/unovis.css themes the product's
|
|
2482
|
+
chart tooltip as `color-mix(… --ui-bg-elevated 80%, transparent)` over `blur(12px)
|
|
2483
|
+
saturate(1.4)`, and this deck does not import that adapter — it only takes dataviz.css for the
|
|
2484
|
+
palette — so the treatment has to be restated here in deck tokens rather than inherited. It was
|
|
2485
|
+
a flat opaque surface until now, which is the one thing that made the panel read as belonging to
|
|
2486
|
+
a different system.
|
|
2487
|
+
|
|
2488
|
+
The blur is free here in a way it is not elsewhere: a tooltip only exists under a live pointer,
|
|
2489
|
+
so it never reaches an export, where a backdrop-filter over a WebGL canvas is the kind of thing
|
|
2490
|
+
that composites differently.
|
|
2491
|
+
*/
|
|
2492
|
+
--vis-tooltip-background-color: color-mix(in oklab, var(--deck-surface-strong) 80%, transparent);
|
|
2493
|
+
--vis-tooltip-backdrop-filter: blur(12px) saturate(1.4);
|
|
2494
|
+
--vis-tooltip-border-color: var(--deck-border-strong);
|
|
2495
|
+
--vis-tooltip-text-color: var(--deck-fg);
|
|
2496
|
+
--vis-tooltip-border-radius: calc(var(--deck-radius) * 3 * var(--slidev-slide-scale, 1));
|
|
2497
|
+
--vis-tooltip-padding: 0;
|
|
2498
|
+
--vis-tooltip-title-color: var(--deck-fg);
|
|
2499
|
+
/* The separator is the panel's own edge, restated, so the register override reaches both.
|
|
2500
|
+
It sits inside the zoomed child, so the width divides the scale back out to land on the
|
|
2501
|
+
same device pixel the unzoomed panel border does. */
|
|
2502
|
+
--vis-tooltip-title-border-bottom: calc(1px / var(--slidev-slide-scale, 1)) solid
|
|
2503
|
+
var(--vis-tooltip-border-color);
|
|
2504
|
+
--vis-tooltip-title-font-size: var(--type-deck-label-size);
|
|
2505
|
+
--vis-tooltip-title-font-weight: var(--type-deck-label-weight);
|
|
2506
|
+
--vis-tooltip-title-padding: var(--deck-space-2) var(--deck-space-2) calc(var(--deck-space-2) / 2);
|
|
2507
|
+
--vis-tooltip-title-margin: 0 0 calc(var(--deck-space-2) / 2) 0;
|
|
2508
|
+
--vis-tooltip-title-text-transform: none;
|
|
2509
|
+
--vis-tooltip-label-color: var(--deck-fg-muted);
|
|
2510
|
+
--vis-tooltip-label-font-size: var(--type-deck-caption-size);
|
|
2511
|
+
--vis-tooltip-value-color: var(--deck-fg);
|
|
2512
|
+
--vis-tooltip-value-font-size: var(--type-deck-caption-size);
|
|
2513
|
+
--vis-tooltip-value-font-weight: var(--type-deck-label-weight);
|
|
2514
|
+
/* The product draws an 8px dot beside 14px type; this keeps that ratio against the deck's 36px,
|
|
2515
|
+
which the 8px default did not — it read as a speck. */
|
|
2516
|
+
--vis-tooltip-dot-size: calc(var(--type-deck-caption-size) * 4 / 7);
|
|
2517
|
+
--vis-tooltip-content-gap: calc(var(--deck-space-2) / 2) var(--deck-space-2);
|
|
2518
|
+
--vis-tooltip-content-padding: 0 var(--deck-space-2) var(--deck-space-2);
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
/*
|
|
2522
|
+
★★ AND IT FOLLOWS THE SLIDE'S REGISTER, WHICH A BODY-MOUNTED PANEL CANNOT DO ON ITS OWN. The
|
|
2523
|
+
block above notes the cost of mounting here: the tooltip is outside `.slidev-layout`, so a
|
|
2524
|
+
`deck-dark` slide used to be read through a light panel — near-black ink on cream, floating over a
|
|
2525
|
+
forest-950 ground.
|
|
2526
|
+
|
|
2527
|
+
★ THE VISIBLE SLIDE IS SELECTABLE, which is what makes this CSS rather than a watcher. Slidev
|
|
2528
|
+
mounts every slide and hides all but one with an INLINE `display: none`, so
|
|
2529
|
+
`:not([style*="display: none"])` names the one on screen — measured, 213 hidden and 1 not.
|
|
2530
|
+
|
|
2531
|
+
⚠ RE-POINTING THE DECK TOKENS, NOT THE --vis-* ONES, and the derived three have to be restated by
|
|
2532
|
+
hand. `--deck-surface-strong`, `--deck-border` and `--deck-border-strong` are color-mix results
|
|
2533
|
+
DECLARED at `:root, .dark, .deck-dark` — a custom property computes where it is declared, so
|
|
2534
|
+
setting `--deck-bg` here alone would leave them at their light values. Same trap deck.css records
|
|
2535
|
+
for a dark card. The mixes below are that file's own, at the same percentages.
|
|
2536
|
+
|
|
2537
|
+
Everything downstream then resolves for free: the `--vis-tooltip-*` declarations above read these
|
|
2538
|
+
on the SAME element, so they take whichever set is in force.
|
|
2539
|
+
*/
|
|
2540
|
+
body:has(.slidev-page:not([style*="display: none"]) .slidev-layout.deck-dark) > [class*='-tooltip'] {
|
|
2541
|
+
--deck-bg: var(--forest-950);
|
|
2542
|
+
--deck-fg: var(--neutral-0);
|
|
2543
|
+
--deck-fg-muted: var(--neutral-400);
|
|
2544
|
+
--deck-surface-strong: color-mix(in oklab, var(--forest-950), var(--neutral-0) 10%);
|
|
2545
|
+
--deck-border: color-mix(in oklab, var(--forest-950), var(--neutral-0) 14%);
|
|
2546
|
+
--deck-border-strong: color-mix(in oklab, var(--forest-950), var(--neutral-0) 26%);
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
|
|
2550
|
+
|
|
2551
|
+
/* ── `.deck-bare` — a chart slide stripped to its fills ───────────────────────────────────────
|
|
2552
|
+
A SLIDE CLASS, the same shape as `.deck-pairs`: the slide asks for it and every chart under it
|
|
2553
|
+
follows without knowing the class exists.
|
|
2554
|
+
|
|
2555
|
+
★★ IT IS HERE RATHER THAN AS A PROP ON FOUR COMPONENTS, and the reason is what it is FOR. This
|
|
2556
|
+
is one specimen slide whose whole subject is whether the palettes re-point on the dark register —
|
|
2557
|
+
a colour test, where a tick label, a row label and a printed total are three text elements
|
|
2558
|
+
competing with the only thing being tested. That is a presentation choice about one slide, not a
|
|
2559
|
+
capability a chart should advertise: `axes: false` on LineChart would be a chart with no scale,
|
|
2560
|
+
which is a worse chart, and every call site would then have to be told not to use it.
|
|
2561
|
+
|
|
2562
|
+
`display: none` rather than `visibility` or opacity, deliberately — the row has to COLLAPSE, or
|
|
2563
|
+
the bars keep a label gutter with nothing in it and the marks stay where the text put them. It
|
|
2564
|
+
also takes these elements out of the audit's reach, which is correct: an element that is not
|
|
2565
|
+
painted is not ink, and scoring a hidden tick against a ground it never touches would be noise.
|
|
2566
|
+
|
|
2567
|
+
Legends, axis titles and the donut's central label are NOT here — those come off through each
|
|
2568
|
+
component's own props (`legend: false`, no `xLabel`, no `centralLabel`), which is where a real
|
|
2569
|
+
switch already exists. Only the three with no prop are swept. */
|
|
2570
|
+
/* The axes are NOT here — they come off through `axes: false` on the components themselves, and
|
|
2571
|
+
they have to: Unovis reserves the axis band whether or not it paints, so hiding one in CSS left
|
|
2572
|
+
the plot inset by a tick row and a label column that were no longer drawn. Only the two with no
|
|
2573
|
+
prop are swept. */
|
|
2574
|
+
.deck-bare [class*='-central-label'],
|
|
2575
|
+
.deck-bare .bars-label,
|
|
2576
|
+
.deck-bare .bars-value {
|
|
2577
|
+
display: none;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
/* ★★ AND THE BAR TRACK HAS TO BE TOLD ITS WIDTH ONCE THE TEXT IS GONE. `.bars` is
|
|
2581
|
+
`auto minmax(0, 1fr) auto`, so the track is a fraction of whatever width the grid has — and the
|
|
2582
|
+
grid had that width because the label and the value were IN it. Remove both and it shrink-wraps
|
|
2583
|
+
to nothing, `1fr` resolves to 0, and every bar draws at the 2px `min-width` its fill declares.
|
|
2584
|
+
Five vertical ticks down the left margin, which is what the first version of this rule shipped. */
|
|
2585
|
+
/* ★★ `.slidev-layout` IS A SPECIFICITY COUNTERWEIGHT, NOT A SCOPE. Bars.vue sets
|
|
2586
|
+
`grid-template-columns` in a SCOPED block, which compiles to `.bars[data-v-…]` — (0,2,0), the
|
|
2587
|
+
same as `.deck-bare .bars`, and the component's stylesheet is injected after this file, so the
|
|
2588
|
+
tie went to the component and the columns never changed. Three class-level selectors settle it.
|
|
2589
|
+
(The `display: none` rules above need no such help: nothing declares `display` on those. */
|
|
2590
|
+
.slidev-layout.deck-bare .bars {
|
|
2591
|
+
width: 100%;
|
|
2592
|
+
/* ★★ AND THE COLUMNS HAVE TO BE RE-DECLARED, not just the width. `.bars` is
|
|
2593
|
+
`auto minmax(0, 1fr) auto` and each row is a `subgrid` spanning all three, so the track is the
|
|
2594
|
+
MIDDLE column — middle because the label holds the first. Hide the label and the row has one
|
|
2595
|
+
child left, which lands in column 1: an `auto` track sizing to a bar with no intrinsic width.
|
|
2596
|
+
Measured: `.bars` at its full 1728px with a 1px track, so every bar drew at the 2px `min-width`
|
|
2597
|
+
its fill declares — five vertical ticks down the left margin. One `1fr` column is the fix; the
|
|
2598
|
+
subgrid rows adopt it and the bar is the only thing left to fill it. */
|
|
2599
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
/* No hover, either. A tooltip is a reading aid, and this slide has nothing to read — the marks
|
|
2603
|
+
carry no labels and no scale, so a panel naming a value would be the only text on the slide and
|
|
2604
|
+
would be describing a chart the slide has deliberately stripped of its numbers. Killed at the
|
|
2605
|
+
POINTER rather than per component: it takes the crosshair with it (which would otherwise draw a
|
|
2606
|
+
rule and two circles over a bare line), and it needs no prop on four components. */
|
|
2607
|
+
.deck-bare .deck-chart,
|
|
2608
|
+
.deck-bare .bars,
|
|
2609
|
+
.deck-bare .deck-track {
|
|
2610
|
+
pointer-events: none;
|
|
2611
|
+
}
|