@humanforest/slidev-theme 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +577 -0
  3. package/assetUrl.js +38 -0
  4. package/components/Alert.vue +209 -0
  5. package/components/AreaBox.vue +152 -0
  6. package/components/AreaChart.vue +145 -0
  7. package/components/Avatar.vue +267 -0
  8. package/components/AvatarGroup.vue +94 -0
  9. package/components/BadgeStrip.vue +203 -0
  10. package/components/Bars.vue +207 -0
  11. package/components/BoroughShape.vue +127 -0
  12. package/components/BrowserFrame.vue +484 -0
  13. package/components/CalendarGrid.vue +480 -0
  14. package/components/Card.vue +70 -0
  15. package/components/Carousel.vue +266 -0
  16. package/components/ChartFrame.vue +426 -0
  17. package/components/ChordChart.vue +264 -0
  18. package/components/ComposedChart.vue +187 -0
  19. package/components/Contents.vue +86 -0
  20. package/components/DataTable.vue +244 -0
  21. package/components/DeckMap.vue +409 -0
  22. package/components/DeviceFrame.vue +331 -0
  23. package/components/DonutChart.vue +103 -0
  24. package/components/DottedMap.vue +126 -0
  25. package/components/Endorsement.vue +76 -0
  26. package/components/Grid.vue +169 -0
  27. package/components/GroupedBarChart.vue +141 -0
  28. package/components/IconTile.vue +116 -0
  29. package/components/Kpi.vue +188 -0
  30. package/components/LineChart.vue +133 -0
  31. package/components/Logo.vue +203 -0
  32. package/components/LondonMap.vue +348 -0
  33. package/components/MarkPair.vue +109 -0
  34. package/components/Marquee.vue +263 -0
  35. package/components/NestedDonutChart.vue +295 -0
  36. package/components/Placeholder.vue +88 -0
  37. package/components/SankeyChart.vue +223 -0
  38. package/components/ScatterChart.vue +142 -0
  39. package/components/StackedBarChart.vue +143 -0
  40. package/components/StatCard.vue +134 -0
  41. package/components/StatusTrack.vue +334 -0
  42. package/components/Timeline.vue +249 -0
  43. package/components/TimelineChart.vue +329 -0
  44. package/components/TreemapChart.vue +267 -0
  45. package/components/backgrounds.js +135 -0
  46. package/components/boroughGeometry.ts +59 -0
  47. package/components/chartData.ts +277 -0
  48. package/components/chartTip.ts +201 -0
  49. package/components/codeTheme.ts +239 -0
  50. package/components/deckPalette.ts +157 -0
  51. package/components/dottedMapGeometry.js +60 -0
  52. package/components/iconSlugs.js +55 -0
  53. package/components/iconUrl.js +51 -0
  54. package/components/mermaidTheme.ts +972 -0
  55. package/components/motionDuration.ts +41 -0
  56. package/components/tableRules.ts +257 -0
  57. package/confidential-mark.vue +151 -0
  58. package/custom-nav-controls.vue +79 -0
  59. package/endorsements/b-corp-black.svg +30 -0
  60. package/endorsements/b-corp-white.svg +30 -0
  61. package/global-top.vue +1662 -0
  62. package/icons/forest/bike-asterisk.svg +21 -0
  63. package/icons/forest/bike-down.svg +19 -0
  64. package/icons/forest/bike-off.svg +20 -0
  65. package/icons/forest/bike-up.svg +19 -0
  66. package/icons/forest/bike-x.svg +19 -0
  67. package/icons/forest/bike.svg +18 -0
  68. package/icons/forest/mark-fill.svg +6 -0
  69. package/icons/forest/mark.svg +6 -0
  70. package/icons/forest/parking.svg +3 -0
  71. package/icons/forest/star-fill.svg +4 -0
  72. package/icons/forest/star.svg +4 -0
  73. package/icons/forest/traffic-light-caution.svg +8 -0
  74. package/icons/forest/traffic-light-go.svg +8 -0
  75. package/icons/forest/traffic-light-lit.svg +8 -0
  76. package/icons/forest/traffic-light-stop.svg +8 -0
  77. package/icons/forest/traffic-light.svg +8 -0
  78. package/icons/lucide/battery.svg +1 -0
  79. package/icons/lucide/bike.svg +1 -0
  80. package/icons/lucide/chart-line.svg +1 -0
  81. package/icons/lucide/check.svg +1 -0
  82. package/icons/lucide/circle-check.svg +1 -0
  83. package/icons/lucide/clock.svg +1 -0
  84. package/icons/lucide/code.svg +1 -0
  85. package/icons/lucide/component.svg +1 -0
  86. package/icons/lucide/frame.svg +1 -0
  87. package/icons/lucide/layout-grid.svg +1 -0
  88. package/icons/lucide/map-pin.svg +1 -0
  89. package/icons/lucide/map.svg +1 -0
  90. package/icons/lucide/palette.svg +1 -0
  91. package/icons/lucide/table.svg +1 -0
  92. package/icons/lucide/triangle-alert.svg +1 -0
  93. package/icons/lucide/type.svg +1 -0
  94. package/icons/lucide/wrench.svg +1 -0
  95. package/layouts/bleed.vue +317 -0
  96. package/layouts/cover.vue +1514 -0
  97. package/layouts/default.vue +187 -0
  98. package/layouts/divider.vue +286 -0
  99. package/layouts/end.vue +384 -0
  100. package/layouts/quote.vue +218 -0
  101. package/layouts/split.vue +1875 -0
  102. package/layouts/stack.vue +944 -0
  103. package/layouts/statement.vue +67 -0
  104. package/manifest/chrome.ts +36 -0
  105. package/manifest/components.ts +49 -0
  106. package/manifest/index.ts +57 -0
  107. package/manifest/layouts.ts +37 -0
  108. package/manifest/manifest.json +6514 -0
  109. package/manifest/presets.ts +635 -0
  110. package/notch.js +458 -0
  111. package/notchPath.js +188 -0
  112. package/package.json +77 -0
  113. package/section.js +110 -0
  114. package/setup/mermaid-renderer.ts +2120 -0
  115. package/setup/mermaid.ts +30 -0
  116. package/setup/shiki.ts +44 -0
  117. package/setup/shortcuts.ts +28 -0
  118. package/slide-bottom.vue +305 -0
  119. package/slide-top.vue +268 -0
  120. package/styles/base.css +2611 -0
  121. package/styles/index.ts +71 -0
  122. package/styles/tokens.css +144 -0
  123. package/uno.config.ts +231 -0
  124. package/useNotchCuts.js +200 -0
@@ -0,0 +1,484 @@
1
+ <script setup>
2
+ /*
3
+ Desktop browser chrome — the deck's port of the design system's FBrowserFrame. A neutral
4
+ macOS/Safari window: traffic lights, one centred address pill, a new-tab glyph, wrapping a live
5
+ screen at a real display aspect.
6
+
7
+ ★★ NO CC-LICENSED ART HERE, AND THAT IS THE DIFFERENCE FROM DeviceFrame. A phone needs a photoreal
8
+ render of hardware, which is where the Wikimedia CC BY-SA vectors come in. A window is DRAWN, from
9
+ deck tokens, and owes nobody anything — so this file imports nothing. The traffic-light colours are
10
+ taken verbatim (#ff5f57 / #febc2e / #28c840): they are macOS's own, and a Forest-tinted
11
+ approximation would read as a mistake rather than as a window.
12
+
13
+ Redrawn rather than imported because FBrowserFrame.vue is written in Nuxt UI's semantic classes —
14
+ bg-default, bg-elevated, border-default, text-muted, text-dimmed — which this deck does not have.
15
+
16
+ ★★ THE PORT WENT WRONG TWICE, IN OPPOSITE DIRECTIONS, AND BOTH ARE WORTH RECORDING.
17
+
18
+ First it was too little: the structure was right but it lost the shadow the original carries, and
19
+ it kept the original's 12px lights beside the DECK'S type. Jose: "where's the shadow? and the
20
+ traffic light buttons are too small compared with the url displayed."
21
+
22
+ Then, reading "the frame should look like actual chrome of a browser", it was rebuilt as GOOGLE
23
+ CHROME — a tab strip, back/forward/reload, a menu. Wrong word, wrong browser, and wrong instinct:
24
+ "chrome" is the generic term for a window's furniture, and the instruction was to match what the
25
+ design system already draws, which is Safari's single bar. Inventing a richer window made the deck
26
+ disagree with the system it is a specimen OF.
27
+
28
+ So this is the original's ARRANGEMENT — lights, one centred address field, a new-tab glyph — at the
29
+ deck's scale, with its shadow.
30
+
31
+ ★★ AND ITS FINISH IS DELIBERATELY NOT THE ORIGINAL'S, WHICH IS A DIVERGENCE WORTH NAMING. Ported
32
+ literally it came out dated — Jose: "not like a real safari browser, not very modern" — and he was
33
+ right: FBrowserFrame draws a BORDERED RECTANGLE for the address field on a heavy grey slab, which
34
+ is macOS circa 2015. Current Safari is a capsule with a soft fill and no border, on a toolbar that
35
+ sits close to the page, under a wide low shadow.
36
+
37
+ A mock is judged on whether it reads as the thing it is imitating, so the finish follows Safari and
38
+ not the package. The arrangement still follows the package, so the two are the same window. If the
39
+ design system is refreshed, this is the direction it should take rather than the other way round.
40
+
41
+ ★★ THE WHOLE BAR IS SIZED FROM THE WINDOW, NOT FROM THE DECK'S TYPE RAMP, and that is what makes
42
+ it read as a browser rather than as a deck component shaped like one. Set from
43
+ `--type-deck-caption-size` the bar came out 125px tall on a 1080px window — 11.6% of its width,
44
+ where Safari's is about 4% — a chunky slab that no amount of finish would have fixed.
45
+
46
+ So the bar's type is `width * 0.022` and every metric in it is `em`. The window is a PICTURE of a
47
+ browser, and a picture keeps its subject's proportions; DeviceFrame makes the same move for the
48
+ same reason, rendering its screen at true logical points and scaling the whole thing. At 1080 wide
49
+ that is 24px text — smaller than the deck's caption, and still legible from the back of a room.
50
+
51
+ ★ THE LIGHTS ARE SIZED IN `em`, WHICH IS THE ACTUAL FIX rather than a bigger number. In the design
52
+ system the lights are 12px and the URL is 12px — one to one, and it reads correctly because both
53
+ are at a web page's scale. The port kept the 12 and inherited the deck's 36px caption type, so the
54
+ lights came out a third the height of the text beside them. Tying them to `em` makes the
55
+ relationship the thing that is preserved, so neither scale can drift from the other again.
56
+ */
57
+ import { computed } from 'vue';
58
+ import { assetUrl } from '../assetUrl.js';
59
+
60
+ const RATIOS = { '16:10': 16 / 10, '16:9': 16 / 9 };
61
+
62
+ const props = defineProps({
63
+ /**
64
+ * What the address bar shows. Unset with a `src`, it is that URL's own host — so a window pointed
65
+ * at a real page cannot caption itself with somebody else's address, which a literal default of
66
+ * `'forest.bike'` would have let it do silently.
67
+ */
68
+ url: { type: String, default: '' },
69
+ /**
70
+ * A LIVE PAGE to put in the screen, as a URL — rendered in an `<iframe>` at the window's own size.
71
+ *
72
+ * ★★ SLIDEV SUPPORTS THIS OUT OF THE BOX and this prop is not a re-implementation of it: the
73
+ * upstream theme ships `layout: iframe`, `iframe-left` and `iframe-right`, which give a URL a
74
+ * whole slide or half of one. This deck has its own nine layouts and none of them is that, and an
75
+ * iframe with no chrome around it is a rectangle of somebody else's website rather than a picture
76
+ * of a website — so the frame is what makes it read. A raw `<iframe>` in the slot still works
77
+ * anywhere, and is the escape hatch for a case this prop does not cover.
78
+ *
79
+ * ⚠ A LIVE PAGE IS A NETWORK DEPENDENCY ON A SLIDE. It draws nothing offline, it can change under
80
+ * the deck between rehearsal and the room, and `slidev export` renders whatever the site serves at
81
+ * export time. Where the page is the ARGUMENT rather than the illustration, `image` is the honest
82
+ * choice — a capture says what it said when it was taken. Reach for `src` when the point is that
83
+ * the thing is real and current.
84
+ *
85
+ * It is LIVE in every sense by default — see `interactive` below.
86
+ */
87
+ src: { type: String, default: '' },
88
+ /**
89
+ * Whether a `src` page can be scrolled and clicked.
90
+ *
91
+ * ★★ TRUE, BECAUSE `src` ALREADY MEANT "I WANT THE REAL THING". This was frozen at first, on the
92
+ * reasoning that a deck is a picture of a product — and that is the argument for `image`, not for
93
+ * a prop whose whole purpose is that the page is current. Somebody who reaches for `src` has asked
94
+ * for the site; handing them a still of it that happens to cost a network request is the surprise.
95
+ * Jose, on the first cut: "it is not interactable? at least scrollable?"
96
+ *
97
+ * ⚠ THE ONE REAL COST IS KEYBOARD FOCUS. Slidev advances on arrow keys and space, and a focus
98
+ * inside the frame takes those with it — click the page, and the next arrow scrolls the site
99
+ * rather than turning the slide. Clicking the slide outside the frame gives it back. Scrolling and
100
+ * clicking are otherwise free: Slidev does not scroll slides, and the frame's `sandbox` denies
101
+ * top-navigation, so a link inside cannot move the deck out from under the presenter.
102
+ *
103
+ * Pass `false` for a page that must sit still — a slide left on screen unattended, or one where a
104
+ * stray scroll would leave the audience looking at the middle of somebody's footer. It is set with
105
+ * `inert` as well as `pointer-events`, so the frozen frame is out of the focus order too.
106
+ */
107
+ interactive: { type: Boolean, default: true },
108
+ /**
109
+ * The screen's shape.
110
+ *
111
+ * ★ TWO VALUES, AND THE RESTRAINT IS THE DESIGN SYSTEM'S OWN: "the two shapes a desktop actually
112
+ * comes in". This port briefly offered 4:3 and 3:2 as well, which is a specimen of a monitor
113
+ * nobody has — and the frames package states why in its own comment: a hand-picked height drifts
114
+ * into ratios no display uses, which makes a mock's reflow decisions untestable against anything
115
+ * real. A phone gets its shape from a device registry; a desktop gets it from this pair.
116
+ */
117
+ ratio: {
118
+ type: String,
119
+ default: '16:10',
120
+ // Repeated inline: defineProps() is hoisted out of setup() at build time, so this validator
121
+ // cannot close over the RATIOS const above — that compiles in dev and breaks a production build.
122
+ validator: (r) => ['16:10', '16:9'].includes(r),
123
+ },
124
+ /**
125
+ * How wide to draw the window, in canvas pixels. Unset, it takes the room it is given — as wide as
126
+ * that room allows, or narrower where the room is too short for a window that wide.
127
+ *
128
+ * ★★ IT USED TO NEED ONE, AND THAT WAS THE DEFECT RATHER THAN THE CONTRACT. The SCREEN's height is
129
+ * derived from the width and the ratio, so a full-width window on the 1728px content band is 1080
130
+ * of screen plus its bar — the whole canvas and then some — and every caller had to compute a
131
+ * width that fit. deck-template's slide 25 did not, and hung 367px off the bottom until the audit's
132
+ * canvas pass could see it again.
133
+ *
134
+ * The wrapper now caps itself against the room's HEIGHT (see `.deck-browser-room` below, and the
135
+ * hosts that give it one in base.css), so an unset width is a working default rather than a trap.
136
+ * Pass one only to draw the window SMALLER than its room on purpose — a figure at a chosen size,
137
+ * not a fit correction.
138
+ */
139
+ width: { type: Number, default: 0 },
140
+ /** Draw the window on the dark register, for a mock of a dark-mode page. */
141
+ dark: { type: Boolean, default: false },
142
+ /**
143
+ * A screenshot to fill the screen with, as a URL — the same one-rule convention the layouts use for
144
+ * their figures. Cropped with `object-fit: cover`, so a capture taken at some other shape is
145
+ * trimmed rather than squashed. The slot wins for anything that is not a still.
146
+ */
147
+ image: { type: String, default: '' },
148
+ /** Alt text for `image`, and the accessible title of a `src` frame. */
149
+ alt: { type: String, default: '' },
150
+ });
151
+
152
+ /*
153
+ The host, with `www.` dropped — what a browser's own address field shows and what somebody reading
154
+ a slide from the back of a room can actually take in. Wrapped because `new URL()` throws on a bare
155
+ host ('forest.bike'), which is exactly what a caller writing an address rather than a source would
156
+ pass; the empty string then falls through to the literal default below.
157
+ */
158
+ const hostOf = (u) => {
159
+ try { return new URL(u).host.replace(/^www\./, ''); } catch { return ''; }
160
+ };
161
+ const address = computed(() => props.url || hostOf(props.src) || 'forest.bike');
162
+ </script>
163
+
164
+ <template>
165
+ <div class="deck-browser-room" :style="{ '--deck-browser-screen-ratio': String(RATIOS[ratio] ?? RATIOS['16:10']) }">
166
+ <div class="deck-browser-fit" :style="width ? { width: `${width}px` } : undefined">
167
+ <div class="deck-browser" :class="dark ? 'deck-dark is-dark' : ''">
168
+ <div class="deck-browser-bar">
169
+ <span class="deck-browser-lights" aria-hidden="true">
170
+ <i style="background: #ff5f57" /><i style="background: #febc2e" /><i style="background: #28c840" />
171
+ </span>
172
+ <span class="deck-browser-url">
173
+ <svg class="deck-browser-glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
174
+ <rect x="4" y="10.5" width="16" height="11" rx="2.5" /><path d="M8 10.5V7a4 4 0 0 1 8 0v3.5" />
175
+ </svg>
176
+ <span class="deck-browser-url-text">{{ address }}</span>
177
+ </span>
178
+ <!-- The new-tab glyph the original carries, and the reason the address pill sits optically
179
+ centred: it balances the lights on the other side. -->
180
+ <svg class="deck-browser-glyph deck-browser-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true">
181
+ <path d="M12 5v14M5 12h14" />
182
+ </svg>
183
+ </div>
184
+
185
+ <div class="deck-browser-screen">
186
+ <img v-if="image" :src="assetUrl(image)" :alt="alt" class="deck-browser-media" draggable="false">
187
+ <!--
188
+ `loading="lazy"` because a deck mounts more slides than it shows. This book sets
189
+ `preload: false`, so only the neighbours are built — but a deck that does not would
190
+ otherwise fetch every live page in it the moment it opens, and lazy defers an off-screen
191
+ frame in either case.
192
+
193
+ `sandbox="allow-scripts"` and nothing else: measured against forest.bike, scripts alone
194
+ render the page identically to an unsandboxed frame, and withholding `allow-same-origin`
195
+ gives it an opaque origin — no cookies, no storage — while the default denial of
196
+ top-navigation, forms, popups and downloads means an embedded page cannot move the deck
197
+ out from under the presenter. `referrerpolicy` keeps the deck's own URL out of the
198
+ embedded site's logs.
199
+ -->
200
+ <iframe
201
+ v-if="src"
202
+ class="deck-browser-live"
203
+ :class="interactive ? '' : 'is-frozen'"
204
+ :src="assetUrl(src)"
205
+ :title="alt || `A live view of ${address}`"
206
+ :inert="!interactive"
207
+ loading="lazy"
208
+ sandbox="allow-scripts"
209
+ referrerpolicy="no-referrer"
210
+ />
211
+ <slot />
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </template>
217
+
218
+ <style scoped>
219
+ /*
220
+ ★ THE SHADOW IS THE ORIGINAL'S, not a new idea: FBrowserFrame carries
221
+ `drop-shadow(0 25px 45px rgba(0,0,0,.28))` and the first port simply dropped it, leaving the window
222
+ flat on the slide. It is also the only drop shadow in this theme, and that is right — every other
223
+ object here is OF the page, where a window is a picture of something in front of it.
224
+
225
+ A box-shadow rather than a filter: the frame is a plain rounded rectangle, so the cheaper property
226
+ does the same job, and a filter would establish a containing block for anything positioned inside.
227
+ */
228
+ /*
229
+ ★★ A CONTAINER, SO THE CHROME SCALES WITH THE WIDTH IT IS ACTUALLY DRAWN AT.
230
+
231
+ The bar's type was `width * 0.022` as an inline style, which only exists when a caller passes
232
+ `width`. Put in a split's figure cell the window takes the CELL's width and that prop is absent —
233
+ so it fell back to the deck's 36px caption and the chrome came out chunky again, the same defect
234
+ in a new place.
235
+
236
+ `container-type: inline-size` here and `cqw` inside means the proportion holds however the width
237
+ arrives: an explicit prop, a grid cell, a figure column. The wrapper exists because an element
238
+ cannot query itself — the window has to be the container's CHILD to read `cqw`.
239
+ */
240
+ /*
241
+ ★★ AND A SECOND WRAPPER OUTSIDE IT, WHICH IS THE ROOM RATHER THAN THE WINDOW.
242
+
243
+ Two boxes because they answer two different questions and each needs its own container. `-fit` is
244
+ the WINDOW's width, and the chrome queries it. `-room` is the space the window was given, and the
245
+ fit box queries THAT — a box cannot query itself, so the height the cap is computed from has to
246
+ come from one level up.
247
+
248
+ The room only knows its height where something outside gives it one; base.css names the three
249
+ hosts that do (`.slidev-layout > `, split's figure cell, stack's band), makes it a size container
250
+ there and caps the fit box against `100cqh` of it. Everywhere else there is no cap at all and the
251
+ window behaves as it always did — width-driven, uncapped — which is the right answer inside a grid
252
+ cell that has a caption under it and no height to spare.
253
+ */
254
+ .deck-browser-room {
255
+ display: flex;
256
+ flex-direction: column;
257
+ justify-content: center;
258
+ align-items: center;
259
+ width: 100%;
260
+ }
261
+ .deck-browser-fit {
262
+ /*
263
+ THE BAR'S TYPE, PUBLISHED — because two elements need it and they resolve it against DIFFERENT
264
+ containers. `.deck-browser` uses it as `font-size`, where `cqw` is the fit box: the width the
265
+ window is actually drawn at, which is the whole point of the container below. The cap uses it
266
+ here, where `cqw` is the ROOM: wider than or equal to the window, so the height it reserves for
267
+ the bar is never an under-estimate. One declaration, two correct readings.
268
+ */
269
+ --deck-browser-chrome: clamp(14px, 2.2cqw, 30px);
270
+ /*
271
+ ★★ WHAT THE WINDOW ADDS ABOVE AND BELOW ITS SCREEN, WHICH IS THE ONE NUMBER THE FIT MATHS NEEDS.
272
+
273
+ `max-height` does not travel back through `aspect-ratio` — a box at `width: 100%` with a ratio
274
+ and a height cap keeps its width and simply loses its shape (measured: 500x300 in a 500x300 room
275
+ where 480x300 was wanted). So the cap has to be a MAX-WIDTH, and a max-width has to know how much
276
+ of the room the screen does not get.
277
+
278
+ 2.1 is an upper bound on the bar's own height in `em`, not its exact height: the bar is 2.02em of
279
+ padding and pill plus a 1px border, so it measures 2.05em at the type's 30px ceiling and 2.09em
280
+ at its 14px floor. The 2px is the window's own border, top and bottom. Rounding UP is what makes
281
+ the whole expression safe — over-reserving leaves a few pixels of room unused, under-reserving
282
+ hangs the window off the slide, and only one of those is a defect.
283
+
284
+ ponytail: a constant restating a height CSS computes. The bar is not pinned to it deliberately —
285
+ a pinned height would clip the bar's own contents silently, where an under-reserve fails loudly
286
+ in scripts/deckAudit.ts's canvas pass. If the bar is ever retuned past 2.1em, that gate is what
287
+ says so; raise the number then.
288
+ */
289
+ --deck-browser-frame-h: calc(2.1 * var(--deck-browser-chrome) + 2px);
290
+ container-type: inline-size;
291
+ width: 100%;
292
+ }
293
+ .deck-browser {
294
+ display: flex;
295
+ flex-direction: column;
296
+ width: 100%;
297
+ overflow: hidden;
298
+ /* 2.2cqw is the same factor the inline style used, expressed against the real width. The clamp is
299
+ a floor and a ceiling for the extremes: a thumbnail-sized window still needs legible chrome, and
300
+ a full-bleed one should not grow toolbar type without limit. Declared one level up so the fit
301
+ box can reserve the bar's height from the same expression — see `--deck-browser-chrome`. */
302
+ font-size: var(--deck-browser-chrome);
303
+ /*
304
+ ★★ THE WINDOW'S OWN CORNER, NOT THE DECK'S CARD RADIUS. This was `--deck-radius-card`, which is
305
+ `--deck-radius * 6` — the radius a CARD takes in this deck, and far rounder than any window has.
306
+ Jose: "keep the rounded corners of the component, dont force a slide doctrine corner radius."
307
+
308
+ He is right, and the reason is what this component is. A card is furniture the deck owns, so it
309
+ takes the deck's radius; a browser window is a PICTURE OF SOMETHING ELSE, and its corner is a
310
+ fact about macOS the same way the traffic-light colours are. Overriding it makes the mock read
311
+ as a Forest card with a toolbar drawn on it.
312
+
313
+ ★ AND IT IS THE CURRENT macOS CORNER, NOT THE OLD ONE. 0.55em landed at ~11px, which is Big Sur's
314
+ radius and the `rounded-xl` the design system's frame uses — and reads dated next to a machine
315
+ running anything recent, where windows are visibly rounder. Jose: "use a proper corner radius,
316
+ like how the newest macos do." 0.9em is about 20px on a 940px window, which is the modern shape.
317
+
318
+ In `em` so it scales with the window like every other metric in the chrome — the corner is a fact
319
+ about the subject, and it has to survive being drawn at any size.
320
+ */
321
+ border-radius: 0.9em;
322
+ border: 1px solid var(--deck-border);
323
+ background: var(--deck-bg);
324
+ /*
325
+ ★ TIGHTER AND FAINTER THAN THE DRAFT, AND IN `em` SO IT SCALES WITH THE WINDOW.
326
+
327
+ It was `0 40px 80px -24px / 0.30` plus a contact layer — which read as a spread-out haze under the
328
+ frame rather than as a window sitting on the page. Jose: "less prominent, not that spread out,
329
+ more subtle." The blur is roughly halved and the opacity cut, so it separates the window from the
330
+ slide without announcing itself from the back of a room.
331
+
332
+ In `em` for the same reason every other metric here is: the shadow is part of the object, and a
333
+ fixed 40px under a window drawn at 900 reads differently from the same 40px under one drawn at
334
+ 1400. Two layers still — a broad ambient one and a short contact one, which is what a real window
335
+ casts — just quieter.
336
+ */
337
+ box-shadow: 0 0.55em 1.1em -0.35em rgb(0 0 0 / 0.16), 0 0.1em 0.3em -0.12em rgb(0 0 0 / 0.10);
338
+ }
339
+
340
+ .deck-browser-bar {
341
+ display: flex;
342
+ align-items: center;
343
+ /* The address pill was setting the bar's height through its own line box — 71px on a 1000px
344
+ window, 7.1% where Safari's toolbar is about 4%. A tight line-height hands the height back to
345
+ the padding, which is the thing that was tuned. */
346
+ line-height: 1.15;
347
+ gap: 1em;
348
+ padding: 0.36em 0.7em;
349
+ /*
350
+ Close to the page, not a slab on top of it. The original's `bg-elevated/60` reads as a second
351
+ surface; a modern toolbar is barely separated from the content — a hairline and a couple of
352
+ percent of tint, which is what keeps the window feeling like one object.
353
+ */
354
+ border-bottom: 1px solid color-mix(in oklab, var(--deck-border), transparent 40%);
355
+ background: color-mix(in oklab, var(--deck-bg), var(--deck-fg) 3%);
356
+ color: var(--deck-fg-muted);
357
+ font-family: var(--font-sans);
358
+ }
359
+ /* One `em` each, so the lights and the address text keep the design system's one-to-one relationship
360
+ at whatever size this bar is set. See the note at the top — this is the whole fix. */
361
+ /*
362
+ ★ HALF AN `em`, AND THE FIRST em-BASED NUMBER WAS STILL WRONG. Tying the lights to the type fixed
363
+ the relationship but 0.9em then made them huge — Jose: "after scaling the traffic light buttons are
364
+ huge compared to the other things". Measured against the real thing rather than guessed again:
365
+ macOS draws a 12px light in a 52px toolbar, so a light is about 23% of the bar's height. At 0.9em
366
+ on a 48px bar these were 20px — 42%, nearly double. 0.5em lands at 11px, which is 23%.
367
+
368
+ The lesson is that the type was the wrong thing to size them against: `em` is only the MECHANISM
369
+ that keeps them in step. The proportion they actually owe is to the BAR.
370
+ */
371
+ .deck-browser-lights {
372
+ display: flex;
373
+ flex: none;
374
+ gap: 0.34em;
375
+ }
376
+ .deck-browser-lights i {
377
+ display: block;
378
+ width: 0.5em;
379
+ height: 0.5em;
380
+ border-radius: var(--deck-radius-full);
381
+ }
382
+ /*
383
+ `margin-inline: auto` with a max width, exactly as the original does it: the pill is centred in the
384
+ BAR rather than in the space left over, which is what keeps it centred under the window's own
385
+ midline whether or not the URL is long.
386
+ */
387
+ .deck-browser-url {
388
+ display: flex;
389
+ align-items: center;
390
+ justify-content: center;
391
+ gap: 0.42em;
392
+ width: 100%;
393
+ max-width: 22em;
394
+ min-width: 0;
395
+ margin-inline: auto;
396
+ padding: 0.18em 1em;
397
+ /*
398
+ A CAPSULE WITH A FILL, NOT A BORDERED RECTANGLE. The border is what dated the port: current Safari
399
+ draws the field as a soft capsule that sits INSIDE the toolbar rather than as a control drawn on
400
+ top of it, and an outline around it reads as a text input from a decade ago.
401
+ */
402
+ border-radius: var(--deck-radius-full);
403
+ background: color-mix(in oklab, var(--deck-bg), var(--deck-fg) 7%);
404
+ }
405
+ .deck-browser-url-text {
406
+ overflow: hidden;
407
+ /* Smaller than the bar's own type. Safari sets the address in a size below the rest of the
408
+ toolbar's, and at parity the URL was the loudest thing in the window — the address is a label,
409
+ not a headline. Applied to the TEXT rather than the pill so the field's own geometry, which is
410
+ tuned in `em`, does not shift with it. */
411
+ font-size: 0.82em;
412
+ text-overflow: ellipsis;
413
+ white-space: nowrap;
414
+ }
415
+ .deck-browser-glyph {
416
+ flex: none;
417
+ width: 0.9em;
418
+ height: 0.9em;
419
+ opacity: 0.6;
420
+ }
421
+ .deck-browser-plus {
422
+ width: 1.1em;
423
+ height: 1.1em;
424
+ }
425
+
426
+ .deck-browser-screen {
427
+ /* From the ratio prop, as a custom property rather than an inline `aspect-ratio`, because the fit
428
+ box one level up needs the same number to turn a height cap into a width. */
429
+ aspect-ratio: var(--deck-browser-screen-ratio);
430
+ min-height: 0;
431
+ overflow: hidden;
432
+ background: var(--deck-bg);
433
+ color: var(--deck-fg);
434
+ }
435
+ /* The screen is a fixed shape and a capture is whatever shape it was taken at, so it is cropped
436
+ rather than distorted. `video` included — `<SlidevVideo>` is what carries one into a print export,
437
+ where a plain `<video>` prints blank. */
438
+ /*
439
+ The live page fills the screen at the window's own size, so the site lays out for the width the
440
+ window is drawn at — a 1000px window gets a desktop page and a narrow one gets what a narrow
441
+ browser gets, which is the whole reason to embed a page rather than photograph one.
442
+
443
+ A white ground rather than the deck's, so a page that has not painted yet reads as a blank page
444
+ rather than as a cream panel with a toolbar.
445
+ */
446
+ .deck-browser-live {
447
+ display: block;
448
+ width: 100%;
449
+ height: 100%;
450
+ border: 0;
451
+ background: #fff;
452
+ }
453
+ /* `interactive: false`. `inert` on the element already blocks pointer events and focus in every
454
+ browser that ships it; this is the same statement in CSS, for the one that does not. */
455
+ .deck-browser-live.is-frozen {
456
+ pointer-events: none;
457
+ }
458
+ .deck-browser-screen :deep(img),
459
+ .deck-browser-screen :deep(video),
460
+ .deck-browser-screen :deep(picture),
461
+ .deck-browser-media {
462
+ display: block;
463
+ width: 100%;
464
+ height: 100%;
465
+ object-fit: cover;
466
+ }
467
+ /*
468
+ ★★ DARK IS NEUTRAL, NOT FOREST, AND THAT IS THE ONE PLACE THIS COMPONENT LEAVES THE REGISTER.
469
+
470
+ `.deck-dark` re-points every `--deck-*` role, which is the deck's one dark mechanism and is why
471
+ there are no per-colour rules here. But its ground is `--forest-950` — a dark GREEN — and a browser
472
+ window rendered in it comes out a green window. Jose: "for dark dont use the green, use the neutral
473
+ palette." A window is a picture of macOS, not a Forest surface: its dark mode is grey.
474
+
475
+ So the class is still applied — every text and border role should take its dark mapping, and
476
+ anything slotted into the screen themes correctly — and only the three GROUNDS are re-pointed onto
477
+ the neutral ramp. `--deck-fg` and friends are left alone, because deck.css already resolves those
478
+ through `--neutral-*` rather than through the forest hue.
479
+ */
480
+ .deck-browser.is-dark {
481
+ --deck-bg: var(--neutral-900);
482
+ --deck-border: var(--neutral-700);
483
+ }
484
+ </style>