-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Expand file tree
/
Copy pathdeck-framework.ts
More file actions
540 lines (472 loc) Β· 30.1 KB
/
Copy pathdeck-framework.ts
File metadata and controls
540 lines (472 loc) Β· 30.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
/**
* Stable deck framework injected into the system prompt when the active skill
* mode is `deck`. The whole point: stop regenerating the scale-to-fit JS, the
* keyboard handler, the slide visibility toggle, the counter, and the print
* rules each turn β every regeneration has subtly different bugs (focus is
* wrong, scaling drifts inside the iframe wrapper, arrow keys swallowed).
*
* Two pieces ship together:
* - DECK_SKELETON_HTML : the literal scaffold the model copies verbatim.
* - DECK_FRAMEWORK_DIRECTIVE : the prompt fragment that tells the model
* what is fixed and what they're allowed to change.
*
* Pattern: 1920Γ1080 fixed canvas anchored at the shell's top-left,
* centered into the viewport by `fit()` with `transform-origin: top left`
* and an explicit `translate(tx, ty) scale(s)` whose factor is recomputed
* on every resize. The shell is intentionally NOT a grid/flex container β
* any extra centering layer would stack with the explicit translate and
* push the scaled stage off-screen (see the OD srcdoc bridge's deck-fix
* placement note in `apps/web/src/runtime/srcdoc.ts:injectDeckBridge`).
* Slides are `<section class="slide">` inside the stage, only
* `.slide.active` is visible. Prev/next + counter live OUTSIDE the scaled
* stage so they don't shrink with it.
*
* Why this pattern (not horizontal scroll-snap):
* - It matches what the model has the strongest prior on, so the framework
* gets adopted verbatim instead of being "blended" with the model's own
* instincts (which is what produced the drift in the first place).
* - 1920Γ1080 is the canonical slide canvas. Designs scale predictably.
* - Print becomes trivial: render every slide as block, page-break between.
*
* Drift fixes baked in:
* - `transform-origin: top left` with an explicit
* `translate(tx, ty) scale(s)`. The shell is plain block flow (no
* grid/flex/place-content), so the stage's natural top-left is (0, 0)
* and the translate centers it correctly even inside the OD viewer's
* nested transform wrapper.
* - Capture-phase keydown on BOTH window and document so iframe focus
* quirks can't swallow arrow keys.
* - Auto-focus body on load and on every click.
* - localStorage position restored on load.
* - Print stylesheet shows every slide as a 1920Γ1080 page-broken block,
* producing a multi-page vertical PDF on Save-as-PDF.
*/
export const DECK_SKELETON_HTML = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><!-- SLOT: deck title --></title>
<style>
/* ===========================================================
Deck framework β DO NOT EDIT the rules in this <style> block.
Edit only inside the second <style> block below (per-deck
styles) and inside <section class="slide"> bodies.
Contract this framework provides:
- 1920Γ1080 fixed canvas, scaled to fit the viewport
- Only .slide.active is visible at a time
- Programmatic prev/next + counter elements kept outside the scaled
stage but hidden by default so the host can render the UI chrome
- Keyboard (β β space PgUp PgDn Home End R), half-slide click, and stored
position survive iframe focus quirks
- "Save as PDF" produces a multi-page vertical PDF, one slide
per page, by toggling every slide visible under @media print
=========================================================== */
:root {
/* SLOT: theme tokens β the only top-level CSS the agent edits.
Add or override --bg / --fg / --accent / etc. here. */
--bg: #ffffff;
--fg: #1c1b1a;
--muted: #6b6964;
--accent: #c96442;
--surface: #ffffff;
--shell: #08090d;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: var(--shell);
color: var(--fg);
font: 18px/1.5 -apple-system, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.deck-shell {
position: fixed;
inset: 0;
overflow: hidden;
}
.deck-stage {
width: 1920px;
height: 1080px;
background: var(--bg);
position: relative;
transform-origin: top left;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35);
}
.slide {
position: absolute;
inset: 0;
overflow: hidden;
}
/* Visibility toggle hardened with :not(.active) + !important so cascade
order can't break it. The previous \`.slide { display:none }\` rule
lost the cascade whenever a per-slide variant class (e.g.
\`.s-cold { display:grid }\`) was declared after it on the same
element β every slide silently became visible at once. The
\`!important\` is a belt-and-suspenders against agent code that adds
\`!important\` on variant classes too. */
.slide:not(.active) { display: none !important; }
/* The active default uses :where() so it has zero specificity. Per-slide
variant classes like \`.s-cold { display:grid }\` or
\`.s-magazine { display:block }\` can override the default flex layout
just by declaring \`display\` β no need for the variant to be more
specific. The hide rule above still wins for inactive slides. */
:where(.slide.active) { display: flex; flex-direction: column; }
/* Programmatic chrome β counter + prev/next live outside the scaled
stage so the host bridge can read/update them, but they stay hidden
in preview, presentation, fullscreen, and new-tab modes. */
.deck-counter {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
display: none;
align-items: center;
gap: 4px;
background: rgba(10, 14, 26, 0.92);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 6px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.08);
color: #fff;
font: 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: 0.18em;
z-index: 1000;
}
.deck-counter button {
width: 36px; height: 36px;
background: transparent;
color: #fff;
border: 0;
border-radius: 50%;
font-size: 18px;
line-height: 1;
cursor: pointer;
display: grid;
place-items: center;
transition: background 0.15s;
}
.deck-counter button:hover { background: rgba(255, 255, 255, 0.12); }
.deck-counter button[disabled] { opacity: 0.3; cursor: default; }
.deck-counter .deck-count {
padding: 0 14px;
letter-spacing: 0.22em;
}
.deck-counter .deck-count .total { color: rgba(255, 255, 255, 0.5); }
.deck-hint {
position: fixed;
bottom: 26px;
right: 28px;
color: rgba(255, 255, 255, 0.4);
font: 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: 0.2em;
text-transform: uppercase;
z-index: 999;
pointer-events: none;
display: none;
}
/* Print / PDF stitching β every slide stacks top-to-bottom, one per
page. The viewer's "Share β PDF" relies on this; do not remove. */
@media print {
@page { size: 1920px 1080px; margin: 0; }
html, body {
width: 1920px !important;
height: auto !important;
overflow: visible !important;
background: #fff !important;
}
.deck-shell {
position: static !important;
display: block !important;
inset: auto !important;
}
.deck-stage {
width: 1920px !important;
height: auto !important;
transform: none !important;
box-shadow: none !important;
position: static !important;
}
.slide {
display: flex !important;
position: relative !important;
inset: auto !important;
width: 1920px !important;
height: 1080px !important;
page-break-after: always;
break-after: page;
}
.slide:last-child { page-break-after: auto; break-after: auto; }
.deck-counter, .deck-hint { display: none !important; }
}
</style>
<style>
/* SLOT: per-deck styles β typography, layout helpers, slide variants.
Add classes used by the slide content below, e.g. .title, .big-stat,
.grid-3. Do not redefine .deck-shell / .deck-stage / .slide /
.deck-counter / .deck-hint or anything inside @media print. */
</style>
</head>
<body>
<div class="deck-shell">
<div class="deck-stage" id="deck-stage">
<!-- SLOT: slides β one <section class="slide"> per slide. The first
slide must have class="slide active". The framework auto-counts
them and toggles .active as the user navigates. -->
<section class="slide active" data-screen-label="01 Title">
<!-- SLOT: slide 1 content -->
</section>
<section class="slide" data-screen-label="02">
<!-- SLOT: slide 2 content -->
</section>
<!-- ... add as many <section class="slide"> blocks as the brief asks
for. The first one is .active; the rest are not. -->
</div>
</div>
<!-- Framework chrome β DO NOT EDIT below this line. -->
<nav class="deck-counter" role="navigation" aria-label="Deck navigation">
<button type="button" id="deck-prev" aria-label="Previous slide">βΉ</button>
<span class="deck-count"><span id="deck-cur">01</span> <span class="total">/ <span id="deck-total">01</span></span></span>
<button type="button" id="deck-next" aria-label="Next slide">βΊ</button>
</nav>
<div class="deck-hint">β / β Β· space Β· R reset</div>
<script>
(function () {
var stage = document.getElementById('deck-stage');
var slides = Array.prototype.slice.call(document.querySelectorAll('.slide'));
var prev = document.getElementById('deck-prev');
var next = document.getElementById('deck-next');
var cur = document.getElementById('deck-cur');
var total = document.getElementById('deck-total');
var STORE = 'deck:idx:' + (location.pathname || '/');
var idx = 0;
// ---- scale-to-fit ---------------------------------------------------
// The stage is 1920Γ1080 and sits at .deck-shell's (0, 0) in normal
// block flow β the shell is intentionally NOT a grid/flex container,
// so the stage's natural top-left is (0, 0). We scale via transform
// with transform-origin:top-left, then translate by the remainder to
// center the scaled box in the viewport. This survives nested
// transforms (e.g. when the OD viewer wraps the iframe in its own
// scale wrapper at zoom != 100%).
function fit() {
var sw = window.innerWidth;
var sh = window.innerHeight;
var pad = 32;
var s = Math.min((sw - pad) / 1920, (sh - pad) / 1080);
if (!isFinite(s) || s <= 0) s = 1;
var tx = (sw - 1920 * s) / 2;
var ty = (sh - 1080 * s) / 2;
stage.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(' + s + ')';
}
// ---- navigation -----------------------------------------------------
function pad2(n) { return (n < 10 ? '0' : '') + n; }
function paint() {
slides.forEach(function (el, i) { el.classList.toggle('active', i === idx); });
if (cur) cur.textContent = pad2(idx + 1);
if (total) total.textContent = pad2(slides.length);
if (prev) prev.toggleAttribute('disabled', idx <= 0);
if (next) next.toggleAttribute('disabled', idx >= slides.length - 1);
}
function go(i) {
idx = Math.max(0, Math.min(slides.length - 1, i));
paint();
try { localStorage.setItem(STORE, String(idx)); } catch (_) {}
}
function onKey(e) {
if (e.__odDeckKeyHandled) return;
var t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
if (e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ') { e.__odDeckKeyHandled = true; e.preventDefault(); go(idx + 1); }
else if (e.key === 'ArrowLeft' || e.key === 'PageUp') { e.__odDeckKeyHandled = true; e.preventDefault(); go(idx - 1); }
else if (e.key === 'Home' || String(e.key).toLowerCase() === 'r') { e.__odDeckKeyHandled = true; e.preventDefault(); go(0); }
else if (e.key === 'End') { e.__odDeckKeyHandled = true; e.preventDefault(); go(slides.length - 1); }
}
// Capture phase + listen on both targets β inside the OD iframe,
// focus may be on window OR document; a single non-capture listener
// silently misses presses.
window.addEventListener('keydown', onKey, true);
document.addEventListener('keydown', onKey, true);
if (prev) prev.addEventListener('click', function () { go(idx - 1); });
if (next) next.addEventListener('click', function () { go(idx + 1); });
document.addEventListener('click', function (e) {
if (e.defaultPrevented) return;
if (e.button !== undefined && e.button !== 0) return;
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
var t = e.target;
while (t && t !== document.body && t !== document.documentElement) {
var tag = String(t.tagName || '').toUpperCase();
if (
tag === 'A' ||
tag === 'BUTTON' ||
tag === 'INPUT' ||
tag === 'TEXTAREA' ||
tag === 'SELECT' ||
t.isContentEditable ||
t.getAttribute('role') === 'button' ||
t.getAttribute('role') === 'link'
) return;
t = t.parentElement;
}
focusDeck();
if (e.clientX < window.innerWidth / 2) go(idx - 1);
else go(idx + 1);
}, true);
// Auto-focus body so arrow keys work without an initial click.
document.body.setAttribute('tabindex', '-1');
document.body.style.outline = 'none';
function focusDeck() { try { window.focus(); document.body.focus({ preventScroll: true }); } catch (_) {} }
document.addEventListener('mousedown', focusDeck);
window.addEventListener('load', focusDeck);
// Restore last position.
try {
var saved = parseInt(localStorage.getItem(STORE) || '0', 10);
if (!isNaN(saved) && saved >= 0 && saved < slides.length) idx = saved;
} catch (_) {}
window.addEventListener('resize', fit);
fit();
paint();
focusDeck();
})();
</script>
</body>
</html>`;
export const DECK_FRAMEWORK_DIRECTIVE = `# Slide deck β fixed framework (this is non-negotiable for deck mode)
Decks regress when each turn re-authors the scale-to-fit logic, the keyboard handler, the slide visibility toggle, the counter, and the print rules. The user has hit this enough times that we now ship a **fixed framework**: 1920Γ1080 canvas, scale-to-fit, hidden programmatic prev/next + counter, capture-phase keyboard with R reset-to-first-slide, half-slide click navigation, localStorage position restore, and a print stylesheet that emits a multi-page vertical PDF on Save-as-PDF β all baked in.
**You do not write any of that. You do not modify any of that.** Your job is to fill content slots only.
## Workflow β copy framework first, then fill content
When the user asks for slides, your TodoWrite plan **must** start with "copy the deck framework verbatim" before any content step. The intended order is:
\`\`\`
1. Bind the active direction's palette + fonts to :root in the framework
2. Copy the canonical skeleton below as a semantically named deck HTML file, such as \`investor-pitch-deck.html\` (nothing else first)
3. Plan the slide arc and theme rhythm (state aloud before writing)
4. Add per-deck classes inside the second <style> block
5. Replace each <section class="slide"> SLOT with real content
6. Self-check (no rewriting framework chrome / @media print / nav script)
7. Summarize the written or changed deck file in a short ordinary assistant message
\`\`\`
If you find yourself writing \`<style>\` rules for \`.deck-shell\`, \`.deck-stage\`, \`.slide\`, \`.canvas\`, \`fit()\`, \`@media print\`, or a keyboard handler β STOP. The framework already has them. Re-read this directive, then keep going from "fill SLOT content".
## The contract
When you start a new deck, your output is a single semantically named HTML file built from the canonical skeleton below. **Copy the skeleton verbatim**, including its first \`<style>\` block, the \`.deck-shell\` / \`.deck-stage\` / hidden \`.deck-counter\` / \`.deck-hint\` programmatic chrome, and the entire trailing \`<script>\`. Do not name every deck \`index.html\`; use \`index.html\` only if the user is editing an existing \`index.html\` deck or a fixed runtime convention requires that path.
You may edit only inside slots marked \`SLOT:\`:
- \`SLOT: deck title\` β the \`<title>\` element.
- \`SLOT: theme tokens\` β the \`:root\` CSS custom properties (\`--bg\`, \`--fg\`, \`--accent\`, \`--shell\`, β¦). Add new tokens here if needed.
- \`SLOT: per-deck styles\` β the second \`<style>\` block. Define classes used by your slide content (e.g. \`.title\`, \`.big-stat\`, \`.grid-3\`, custom typography). **Never redefine** \`.deck-shell\`, \`.deck-stage\`, \`.slide\`, \`.deck-counter\`, \`.deck-hint\`, or anything inside \`@media print\`.
- \`SLOT: slides\` β the \`<section class="slide">\` blocks. Add as many as the brief calls for. The first slide MUST be \`<section class="slide active" β¦>\`; the rest are \`<section class="slide" β¦>\` (no \`active\`). The script auto-counts them.
- \`SLOT: slide N content\` β content inside each \`<section>\`.
## Common drift modes β DO NOT DO THESE
These are the failure patterns we just spent days debugging. Each one looks "equivalent" but breaks something specific:
- β Don't write your own \`fit()\` function or \`transform: scale()\` script. The framework already does it, and ad-hoc versions drift inside the OD viewer's nested transform wrapper.
- β Don't use \`transform-origin: center center\` on the stage. The framework uses \`top left\` plus an explicit translate so scaled content lands at the same place every render.
- β Don't use \`document.addEventListener('keydown', β¦)\` alone. Inside an iframe, focus is sometimes on window. The framework adds capture-phase listeners on **both** targets β replacing this with a single listener silently swallows arrow keys.
- β Don't replace the localStorage key, the slide-visibility toggle (\`.slide.active\`), or the counter element IDs (\`#deck-cur\`, \`#deck-total\`, \`#deck-prev\`, \`#deck-next\`). The framework reads them by ID.
- β Don't put the prev/next buttons or the counter **inside** \`.deck-stage\`. They must live outside the scaled element so the host bridge can manage slides without scaling or clipping the control surface.
- β Don't redefine \`.slide\`, \`.slide.active\`, or \`.slide:not(.active)\` directly. The framework owns the visibility toggle through those exact selectors. If you want a non-flex layout on a slide, **add a variant class to the same \`<section class="slide β¦">\` element** (e.g. \`.s-cold\`, \`.s-magazine\`) and declare \`display: grid\` / \`display: block\` on the variant. The framework's active default is wrapped in \`:where(...)\` so it has zero specificity β your variant always wins for the active slide. Variant classes do NOT need to be more specific than \`.slide.active\`. (The inactive-hide rule still wins because it uses \`:not(.active) { display: none !important; }\`.)
- β Don't strip or "tidy" the \`@media print\` block. It is how Share β PDF stitches every slide into a multi-page document. Without it, PDF export collapses to a single screenshot.
## Why this matters (so you can judge edge cases)
The framework is a contract with the host viewer. The OD iframe sits inside a transformed wrapper (the zoom control); the keyboard handler needs capture phase + dual targets; "Share β PDF" reads the print stylesheet; the position survives reloads via localStorage. If a turn rewrites any of these β even with "equivalent" code β the next turn diverges, and three turns in the deck has subtly broken nav and a one-page PDF. Treat the framework as load-bearing infrastructure.
If the user asks for something the framework genuinely doesn't support (vertical decks, custom slide transitions, multi-column simultaneous slides), say so and ask before forking. **Default answer: keep the framework, change the slide content.**
## Each slide
Each \`<section class="slide" data-screen-label="NN Title">\` is one slide rendered onto the 1920Γ1080 canvas. Inside the section, lay out content with your own \`SLOT: per-deck styles\` classes. Slide labels are 1-indexed (\`01 Title\`, \`02 Problem\`β¦). The first slide gets \`class="slide active"\`; the others just \`class="slide"\`.
Real copy only β no lorem ipsum, no invented metrics, no generic emoji icon rows. If you don't have a value, leave a short honest placeholder.
## Density and overflow discipline (the #1 cause of ugly decks)
Even with the visibility toggle working, slides go ugly when content overflows the 1920Γ1080 canvas. Specific failure modes that ship today:
- β Title slides with a display headline β₯ 160px **plus** a multi-line subtitle/deck paragraph **plus** an absolutely-positioned \`.footer\` at \`bottom: ~56px\`. The flow content grows downward, the absolute footer occupies the bottom band, and the two collide in the last ~100px of the slide.
- β Stat slides with three numbers + three captions + a footer. Split into three stat slides β the framework counts slides for you, more slides cost nothing.
- β "Magazine spread" attempts that pack masthead + display headline + body grid + sidebar + absolute footer all into a single 1080px slide.
Rules β non-negotiable:
1. **Display headlines on cover/title slides: max ~140px font-size, max 8 words, max 3 lines.** If the headline doesn't fit those bounds, the slide is the wrong shape β split it, don't shrink the font and pack more in.
2. **Reserve a footer safe-zone.** If you use \`.footer { position: absolute; bottom: Npx; }\`, flow content above the footer must stop at least 80px before \`1080 β footer_height β N\`. Practically: don't let flow content extend into the bottom 200px of the slide. Easiest enforcement: make the slide's main content area its own \`<div style="height: 760px;">\` (or \`max-height\`), and the footer absolute below it.
3. **Body slides: β€ 3 paragraphs, β€ 56ch lead text width, β€ 12 words per line.**
4. **One idea per slide.** Two ideas = two slides.
## Data chart discipline (hand-written bar charts)
Hand-written div/CSS charts fail in two ways users report as "the chart is lying": bar lengths eyeballed as magic numbers that don't match the data, and value labels clipped away inside fixed-height bars. If the active template family ships a chart reference (e.g. the \`html-ppt\` family's Chart.js \`chart-bar.html\` template), prefer it over a hand-written div chart. When you do hand-write a bar chart (horizontal or vertical), build it from this skeleton:
\`\`\`html
<div class="chart" style="--max: 5.0">
<div class="bar-row">
<span class="bar-label">2024</span>
<div class="bar-track"><div class="bar" style="--v: 5.0"></div></div>
<span class="bar-value">5.0 δΈδΊΏ</span>
</div>
<!-- one .bar-row per data point; put the REAL numeric value in --v -->
</div>
\`\`\`
\`\`\`css
.bar { width: calc(var(--v) / var(--max) * 100%); }
\`\`\`
Rules β same weight as the density rules above:
1. **Bar lengths are computed, never eyeballed.** Every bar carries its value as an inline \`--v\`; declare \`--max\` ONCE on the chart container so all bars share one baseline. \`--v\` / \`--max\` must be unitless numbers β \`calc()\` division needs a plain number, so units ("δΈδΊΏ", "%", "$") live only in the \`.bar-value\` text. Vertical variant: \`.bar { height: calc(var(--v) / var(--max) * 100%); }\`, and give \`.bar-track\` an explicit height (a percentage height inside an auto-height parent computes to 0 and every bar collapses).
2. **Every data point gets a visible category label AND value label.** Render the value in its own element outside the bar (like \`.bar-value\` above), never inside a fixed-height \`overflow: hidden\` bar where a short bar clips it away.
- β Don't hand-write eyeballed \`height: 62%\` / \`width: 45%\` magic numbers on bars.
- β Don't let bars in the same chart imply different baselines β one \`--max\` per chart.
- β Don't nest value labels inside a clipping fixed-height bar.
- β Don't omit any data point's label, however short its bar.
## Nested / concentric diagram discipline
Nested shapes may share a center; their text blocks may not. Treat geometry and labels as separate layout responsibilities.
- Put at most one short KPI in the shared center. Never center a label/value/description stack inside two or more concentric layers.
- Put every other label in a separate legend, external callout, or visibly reserved non-overlapping region. If those regions do not fit, use a stacked comparison, flow, or table instead.
- When rendering cannot confirm the layout and static review leaves any collision risk, choose the separate-legend structure below.
\`\`\`html
<div class="nested-market">
<div class="nested-market__rings" aria-hidden="true"><!-- shapes only; no text --></div>
<dl class="nested-market__legend">
<div><dt>Outer layer</dt><dd>Value β explanation</dd></div>
<div><dt>Middle layer</dt><dd>Value β explanation</dd></div>
<div><dt>Inner layer</dt><dd>Value β explanation</dd></div>
</dl>
</div>
\`\`\`
## Mermaid diagram theme discipline (dark decks)
Mermaid's default theme is built for white pages: near-black labels (\`#333\`), pale node fills, black strokes, and a TRANSPARENT svg background. Embedded in a dark-themed deck it produces the failure users report as "the diagram text is unreadable in dark mode": dark labels sitting directly on the dark slide background. Prefer a hand-written HTML/CSS/SVG diagram styled with the deck's own tokens (\`--bg\`, \`--fg\`, \`--accent\`) β it never drifts from the theme and needs no external JS. When you do embed Mermaid, pick the theme from the slide background at initialize time β never leave the default (light) theme on a dark deck:
\`\`\`html
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'dark', // dark slide background
// theme: 'default', // light slide background
});
</script>
\`\`\`
For brand fidelity, \`theme: 'base'\` + \`themeVariables\` reuses the deck palette β pass concrete color values (Mermaid cannot resolve CSS \`var()\` references). \`darkMode: true\` alone does NOT darken node fills β \`base\` keeps its cream \`primaryColor\` default, so always set \`primaryColor\` to a dark surface tone alongside the light text:
\`\`\`js
mermaid.initialize({
startOnLoad: true,
theme: 'base',
themeVariables: {
darkMode: true, // match the slide background
background: '#101014', // the deck's --bg value, as a literal
primaryColor: '#1c1c24', // node fill β dark surface tone, NOT the cream default
primaryTextColor: '#e8e8ec', // the deck's --fg value, as a literal
primaryBorderColor: '#8a8a94',
lineColor: '#8a8a94',
},
});
\`\`\`
Rules β same weight as the density rules above:
1. **Diagram text color follows the slide background, not Mermaid's default.** Dark background β \`theme: 'dark'\` or \`base\` + dark \`themeVariables\`; light background β the default is fine.
2. **Never rely on the SVG bringing its own backdrop.** Mermaid emits a transparent-background SVG, so every label sits directly on the slide. If the diagram cannot be themed, give its container an explicit light plate (e.g. \`background: #fff\`, padding, radius) instead of shipping unreadable labels.
- β Don't call \`mermaid.initialize()\` without a \`theme\` on a dark deck.
- β Don't pass \`var(--fg)\` strings into \`themeVariables\` β Mermaid needs literal colors.
- β Don't hand-recolor a single label to "fix" contrast; theme the whole diagram.
## Pre-handoff self-check β run this BEFORE the final file summary
For every \`<section class="slide">\`, mentally render at 1920Γ1080 and answer:
- [ ] Does the slide's content fit inside the canvas without clipping or overflowing the bottom?
- [ ] If there's an absolutely-positioned footer/header, does flow content stop before the footer's reserved band? (See Rule 2 above.)
- [ ] Is the display headline β€ 140px and β€ 8 words?
- [ ] Does the slide carry β€ one big idea? (No mashed-together masthead + display headline + subtitle + absolute footer + sidebar.)
- [ ] If the slide has a chart: does every data point show a visible category label and value label?
- [ ] Are bar lengths computed from \`--v\` / \`--max\` so proportions match the data? (Mentally spot-check two bars.)
- [ ] If the slide embeds a Mermaid diagram: is \`mermaid.initialize\` themed to the slide background (dark background β \`dark\`/\`base\` theme), leaving no dark-on-dark labels?
If any answer is "no", redesign the slide BEFORE handoff. Decks that overflow are the most common single failure mode reported by users; the user has rejected one before and will reject one again.
## Prefer the simple-deck skill's layout vocabulary when reachable
If \`plugins/_official/examples/simple-deck/assets/template.html\` and its \`references/layouts.md\` are readable from the project workspace, **prefer those layouts over inventing your own**. The simple-deck skill ships eight paste-ready slide skeletons (cover, body, big-stat, three-point row, pipeline, dark quote, before/after, closing) with tested type scales, density rules, and a P0/P1/P2 checklist. Re-inventing those layouts is the source of most density / overflow bugs the framework can't catch.
## Canonical skeleton (this is exactly what the file you write looks like)
\`\`\`html
${DECK_SKELETON_HTML}
\`\`\`
When the brief is "make me a deck", your output is this skeleton with theme tokens tuned, per-deck classes added, and \`<section class="slide">\` blocks filled in β nothing more, nothing less. Skill-specific guidance (typography, theme presets, layout vocabulary) layers *on top of* this framework, not in place of it.
`;