rikiki-deck 0.6.0 → 0.7.2

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 (174) hide show
  1. package/.claude/skills/rikiki-debug/SKILL.md +17 -7
  2. package/.claude/skills/rikiki-deck/SKILL.md +362 -77
  3. package/.claude/skills/rikiki-theme/SKILL.md +1 -1
  4. package/README.md +69 -50
  5. package/bin/lib/assemble.mjs +154 -0
  6. package/bin/lib/box-geometry.mjs +66 -0
  7. package/bin/lib/browser.mjs +302 -0
  8. package/bin/lib/check-api.d.ts +28 -0
  9. package/bin/lib/check-api.mjs +6 -0
  10. package/bin/lib/check-plugins.mjs +228 -0
  11. package/bin/lib/check.mjs +1347 -0
  12. package/bin/lib/cli-error.mjs +26 -0
  13. package/bin/lib/component-deps.mjs +69 -0
  14. package/bin/lib/diff.mjs +275 -0
  15. package/bin/lib/export-pdf.mjs +65 -0
  16. package/bin/lib/graph-hit.mjs +86 -0
  17. package/bin/lib/inline.mjs +137 -39
  18. package/bin/lib/narrative.mjs +77 -0
  19. package/bin/lib/prune-icons.mjs +104 -0
  20. package/bin/lib/render.mjs +195 -0
  21. package/bin/lib/scan-external.mjs +126 -0
  22. package/bin/lib/starter.mjs +27 -14
  23. package/bin/lib/visual.mjs +120 -0
  24. package/bin/rikiki.mjs +420 -35
  25. package/dist/annotation-marks.d.ts +60 -0
  26. package/dist/annotation-marks.js +1 -0
  27. package/dist/bar-segments.d.ts +28 -0
  28. package/dist/bar-segments.js +1 -0
  29. package/dist/browser-location.d.ts +3 -0
  30. package/dist/browser-location.js +1 -0
  31. package/dist/cards-syntax.d.ts +31 -0
  32. package/dist/cards-syntax.js +6 -0
  33. package/dist/{plugins/click-stages.d.ts → click-stages.d.ts} +1 -1
  34. package/dist/deck-agenda.d.ts +25 -0
  35. package/dist/deck-agenda.js +6 -0
  36. package/dist/deck-annotate.d.ts +108 -0
  37. package/dist/deck-annotate.js +18 -0
  38. package/dist/deck-bar.d.ts +32 -0
  39. package/dist/deck-bar.js +19 -0
  40. package/dist/deck-bento.d.ts +38 -0
  41. package/dist/deck-bento.js +4 -0
  42. package/dist/{molecules/deck-callout.d.ts → deck-callout.d.ts} +2 -0
  43. package/dist/deck-callout.js +1 -1
  44. package/dist/deck-cell.d.ts +19 -0
  45. package/dist/deck-cell.js +1 -0
  46. package/dist/deck-checklist.d.ts +20 -0
  47. package/dist/deck-checklist.js +1 -0
  48. package/dist/{layouts/deck-cover.d.ts → deck-cover.d.ts} +8 -0
  49. package/dist/deck-cover.js +9 -6
  50. package/dist/deck-csv.d.ts +38 -0
  51. package/dist/deck-csv.js +15 -0
  52. package/dist/deck-feature-cards.js +2 -2
  53. package/dist/deck-feature.d.ts +18 -0
  54. package/dist/deck-feature.js +2 -2
  55. package/dist/deck-figure.d.ts +26 -0
  56. package/dist/deck-figure.js +8 -0
  57. package/dist/deck-fit.d.ts +14 -0
  58. package/dist/deck-fit.js +1 -0
  59. package/dist/deck-flow.d.ts +41 -0
  60. package/dist/deck-flow.js +7 -0
  61. package/dist/deck-graph.d.ts +92 -0
  62. package/dist/deck-graph.js +25 -0
  63. package/dist/deck-grid.js +1 -1
  64. package/dist/deck-icon.d.ts +20 -0
  65. package/dist/deck-icon.js +1 -0
  66. package/dist/{atoms/deck-kicker.d.ts → deck-kicker.d.ts} +5 -0
  67. package/dist/deck-kicker.js +1 -1
  68. package/dist/deck-kpi-grid.d.ts +26 -0
  69. package/dist/deck-kpi-grid.js +4 -0
  70. package/dist/deck-link.d.ts +21 -0
  71. package/dist/deck-link.js +1 -0
  72. package/dist/{molecules/deck-md.d.ts → deck-md.d.ts} +3 -0
  73. package/dist/deck-md.js +8 -3
  74. package/dist/deck-mermaid.js +15 -3
  75. package/dist/deck-outline.d.ts +50 -0
  76. package/dist/deck-outline.js +1 -0
  77. package/dist/deck-overview.js +53 -39
  78. package/dist/deck-persona.d.ts +31 -0
  79. package/dist/deck-persona.js +6 -0
  80. package/dist/deck-photo.js +1 -1
  81. package/dist/deck-point.d.ts +22 -0
  82. package/dist/deck-point.js +1 -0
  83. package/dist/deck-presenter.js +120 -48
  84. package/dist/deck-pull.d.ts +13 -0
  85. package/dist/deck-pull.js +1 -0
  86. package/dist/{atoms/deck-punch.d.ts → deck-punch.d.ts} +6 -0
  87. package/dist/deck-punch.js +1 -1
  88. package/dist/deck-quote.d.ts +28 -0
  89. package/dist/deck-quote.js +6 -0
  90. package/dist/{runtime/deck-root.d.ts → deck-root.d.ts} +88 -8
  91. package/dist/deck-root.js +17 -13
  92. package/dist/deck-section.js +2 -2
  93. package/dist/deck-source.d.ts +12 -0
  94. package/dist/deck-source.js +2 -0
  95. package/dist/deck-split.d.ts +30 -0
  96. package/dist/deck-split.js +5 -3
  97. package/dist/{molecules/deck-stat.d.ts → deck-stat.d.ts} +2 -0
  98. package/dist/deck-stat.js +2 -2
  99. package/dist/{molecules/deck-step-list.d.ts → deck-step-list.d.ts} +8 -0
  100. package/dist/deck-step-list.js +4 -2
  101. package/dist/deck-table.d.ts +26 -0
  102. package/dist/deck-table.js +1 -0
  103. package/dist/deck-takeaway.d.ts +18 -0
  104. package/dist/deck-takeaway.js +2 -2
  105. package/dist/deck-timeline.d.ts +37 -0
  106. package/dist/deck-timeline.js +5 -0
  107. package/dist/deck-transition.js +3 -3
  108. package/dist/deck-versus.d.ts +18 -0
  109. package/dist/deck-versus.js +9 -0
  110. package/dist/deep-link.d.ts +29 -0
  111. package/dist/deep-link.js +1 -0
  112. package/dist/escape-html.d.ts +3 -0
  113. package/dist/escape-html.js +1 -0
  114. package/dist/fit-controller.d.ts +27 -0
  115. package/dist/fit-controller.js +1 -0
  116. package/dist/graph-layout.d.ts +35 -0
  117. package/dist/graph-layout.js +1 -0
  118. package/dist/grid-tracks.d.ts +17 -0
  119. package/dist/grid-tracks.js +1 -0
  120. package/dist/icon-set.d.ts +6 -0
  121. package/dist/icon-set.js +1 -0
  122. package/dist/index.d.ts +37 -31
  123. package/dist/index.js +95 -49
  124. package/dist/keymap.d.ts +40 -0
  125. package/dist/keymap.js +1 -0
  126. package/dist/mouse-nav.d.ts +12 -0
  127. package/dist/mouse-nav.js +1 -0
  128. package/dist/navigation.d.ts +25 -0
  129. package/dist/navigation.js +1 -0
  130. package/dist/parse-csv.d.ts +9 -0
  131. package/dist/parse-csv.js +3 -0
  132. package/dist/shared-styles.js +1 -1
  133. package/dist/shiki.d.ts +8 -0
  134. package/dist/signature.d.ts +2 -0
  135. package/dist/signature.js +1 -0
  136. package/dist/slide-fill.d.ts +8 -0
  137. package/dist/slide-fill.js +1 -0
  138. package/dist/standalone.js +301 -169
  139. package/dist/vendor/THIRD-PARTY-NOTICES.txt +4347 -0
  140. package/dist/vendor/inventory.json +3029 -0
  141. package/dist/vendor/lit.js +62 -2
  142. package/dist/vendor/mermaid.min.js +95 -95
  143. package/dist/vendor/shiki.js +1 -57
  144. package/dist/viewport.d.ts +42 -0
  145. package/dist/viewport.js +1 -0
  146. package/docs/llms/rikiki-reference.md +955 -64
  147. package/docs/llms/rikiki-workflow.md +536 -0
  148. package/llms.txt +39 -12
  149. package/package.json +33 -12
  150. package/themes/rikiki.css +173 -47
  151. package/themes/siliceum.css +171 -51
  152. package/dist/layouts/deck-feature.d.ts +0 -11
  153. package/dist/layouts/deck-split.d.ts +0 -18
  154. package/dist/layouts/deck-takeaway.d.ts +0 -11
  155. package/dist/plugins/shiki.d.ts +0 -8
  156. /package/dist/{runtime/color.d.ts → color.d.ts} +0 -0
  157. /package/dist/{atoms/deck-badge.d.ts → deck-badge.d.ts} +0 -0
  158. /package/dist/{molecules/deck-card.d.ts → deck-card.d.ts} +0 -0
  159. /package/dist/{atoms/deck-code-highlighter.d.ts → deck-code-highlighter.d.ts} +0 -0
  160. /package/dist/{atoms/deck-code.d.ts → deck-code.d.ts} +0 -0
  161. /package/dist/{layouts/deck-feature-cards.d.ts → deck-feature-cards.d.ts} +0 -0
  162. /package/dist/{molecules/deck-grid.d.ts → deck-grid.d.ts} +0 -0
  163. /package/dist/{runtime/deck-help.d.ts → deck-help.d.ts} +0 -0
  164. /package/dist/{molecules/deck-mermaid.d.ts → deck-mermaid.d.ts} +0 -0
  165. /package/dist/{molecules/deck-metric.d.ts → deck-metric.d.ts} +0 -0
  166. /package/dist/{runtime/deck-notes.d.ts → deck-notes.d.ts} +0 -0
  167. /package/dist/{runtime/deck-overview.d.ts → deck-overview.d.ts} +0 -0
  168. /package/dist/{layouts/deck-photo.d.ts → deck-photo.d.ts} +0 -0
  169. /package/dist/{runtime/deck-presenter.d.ts → deck-presenter.d.ts} +0 -0
  170. /package/dist/{layouts/deck-section.d.ts → deck-section.d.ts} +0 -0
  171. /package/dist/{molecules/deck-shortcut.d.ts → deck-shortcut.d.ts} +0 -0
  172. /package/dist/{molecules/deck-stack.d.ts → deck-stack.d.ts} +0 -0
  173. /package/dist/{molecules/deck-tier-list.d.ts → deck-tier-list.d.ts} +0 -0
  174. /package/dist/{runtime/deck-transition.d.ts → deck-transition.d.ts} +0 -0
@@ -1,10 +1,10 @@
1
1
  # Rikiki · LLM reference
2
2
 
3
- This reference documents rikiki v0.6.0.
3
+ This reference documents rikiki v0.7.2.
4
4
 
5
5
  Exhaustive, self-consistent reference for authoring valid **rikiki** decks. Every
6
6
  tag, attribute, slot, and token below was derived from the source in this repo
7
- (`src/index.ts` is the canonical component list; `themes/rikiki.css` is the
7
+ (`dist/index.js` registers every component; `themes/rikiki.css` is the
8
8
  canonical token list). Do not invent tags, attributes, or tokens · use only what
9
9
  is listed here.
10
10
 
@@ -34,7 +34,8 @@ navigation, hash routing, the progress bar, step dots, and the keyboard hint.
34
34
 
35
35
  ## 2 · Minimal deck
36
36
 
37
- The canonical skeleton (see `starter.html`):
37
+ The canonical skeleton · `rikiki init <name>.html` writes exactly this, with the
38
+ runtime copied into `./rikiki/` beside it:
38
39
 
39
40
  ```html
40
41
  <!doctype html>
@@ -226,13 +227,22 @@ Direct children of `<deck-root>`. Each is one slide.
226
227
 
227
228
  | Tag | Purpose | Key attributes | Slots |
228
229
  |-----|---------|----------------|-------|
229
- | `deck-cover` | Opening slide, dark, with brand + meta | `brand` (split on " · "), `brand-src` (logo URL), `speaker`, `company`, `duration`, `audience`, `runtime`; per-row label overrides `speaker-label`, `company-label`, `duration-label`, `audience-label`, `runtime-label` | default `<h1>`, `.sub`/`p[slot=sub]` |
230
+ | `deck-cover` | Opening slide, dark, with brand + meta | `brand` (split on " · "), `brand-src` (logo URL), `speaker`, `company`, `company-src` (client logo URL, shown before the company name), `duration`, `audience`, `runtime`; per-row label overrides `speaker-label`, `company-label`, `duration-label`, `audience-label`, `runtime-label` | default `<h1>`, `.sub`/`p[slot=sub]` |
230
231
  | `deck-section` | Chapter divider (also a chapter boundary for 2D nav) | `num` | default `<h1>` (may use `<em>`) |
231
232
  | `deck-feature` | Headline + lead + one focal block | `eyebrow` | `title` (`<h1>`), `lead`, default (focal block, e.g. `deck-code`) |
232
233
  | `deck-split` | Two or three columns side by side | `eyebrow`, `cols` (`1-1`/`1-2`/`2-1`/`3`), `gap` (1..6 or raw CSS length), `col-gap` (1..6 or raw CSS length) | `title`, `lead`; `left`/`right` (2-col) or `a`/`b`/`c` (3-col) |
233
234
  | `deck-feature-cards` | Hero focal block + two detail cards under it | `eyebrow` | `title`, `lead`, default (hero block), `left`, `right` |
234
235
  | `deck-photo` | Full-bleed image slide, content on overlay | `src` (required), `position` (CSS `object-position`, default `center`), `darken` (0..1 overlay alpha, default `0.35`), `align` (top/center/bottom, default center), `text-align` (left/center/right, default left) | default slot: any content; style slotted children with `.sub`/`.kicker` classes (these are **CSS classes**, not named slots) |
235
236
  | `deck-takeaway` | Centered punchline, dark | `kicker` | default (e.g. `p.display`, `p.caption`, a `deck-callout`) |
237
+ | `deck-bento` | Bento grid slide · multi-row/column cells share the space | `eyebrow`, `cols` (1..12 or template, default 2), `rows` (1..12 or template, default 1 full-height row), `gap` (1..6 or CSS, default 3), `align`, `justify` | `title` (`<h1>`), default (`deck-point` / `deck-cell` children) |
238
+
239
+ `eyebrow` renders as a short accent-coloured line above the title, in sentence
240
+ case. Write it as a word or two, the way you would say it · "numbers", not
241
+ "NUMBERS". It used to render as a filled pill holding tracked-out small caps,
242
+ which is unreadable at projection distance and is one of the plainest marks of a
243
+ generated page (ADR-002). The tokens are unchanged, so a deck that wants the
244
+ badge back sets `--deck-eyebrow-bg`, `--deck-eyebrow-color`,
245
+ `--deck-eyebrow-radius` and `--deck-eyebrow-padding-x` / `-y`.
236
246
 
237
247
  ---
238
248
 
@@ -240,23 +250,27 @@ Direct children of `<deck-root>`. Each is one slide.
240
250
 
241
251
  | Tag | Purpose | Key attributes | Slots / children |
242
252
  |-----|---------|----------------|------------------|
243
- | `deck-callout` | Highlighted note box | `type` (`info`/`warn`/`danger`/`ok`) | default (text / `deck-md`) |
253
+ | `deck-callout` | Highlighted note box | `type` (`info`/`warn`/`danger`/`ok`), `on-dark` (inverse text on a dark raised surface, for a dark slide) | default (text / `deck-md`) |
244
254
  | `deck-card` | Tinted card | `color` (`yellow`/`orange`/`green`/`red`), `center`, `compact` | default (`<h3>` + body) |
245
- | `deck-md` | Render Markdown (GFM) | · | default = raw Markdown text |
246
- | `deck-mermaid` | Render a Mermaid diagram (loads Mermaid from CDN) | `compact` | default = Mermaid source |
255
+ | `deck-md` | Render Markdown (GFM) · also expands `::: cards` blocks into a tinted card grid | · | default = raw Markdown text |
256
+ | `deck-mermaid` | Render a Mermaid diagram (uses the optional vendored Mermaid runtime) | `compact` | default = Mermaid source |
247
257
  | `deck-stat` | Big-number visual | `num`, `tone` (`yellow`/`orange`/`green`/`red`/`purple`/`lime`/`cyan`) | `claim` (`<h3>`), default = body line |
248
258
  | `deck-metric-list` | Wraps `deck-metric` rows | · | `deck-metric` children |
249
259
  | `deck-metric` | One metric row | `severity` (`bad`/`warn`/`ok`/`info`), `value`, `mono` (render the value in the mono font) | default = label |
250
260
  | `deck-tier-list` | Tier ladder | · | `deck-tier`, `deck-tier-arrow` children |
251
261
  | `deck-tier` | One tier row | `name`, `speed`, `severity` (`muted`/`warn`/`ok`/`hot`), `hot` | default = description text |
252
262
  | `deck-tier-arrow` | Separator note between tiers | · | default = text |
253
- | `deck-step-list` | Numbered step ladder | · | `deck-step` children |
254
- | `deck-step` | One step row | `n`, `note` | default = label |
263
+ | `deck-step-list` | Numbered step ladder | `direction` (`column` default, `row` for a chain across the width), `no-connectors` | `deck-step` children (each one carries its own `note-position`) |
264
+ | `deck-step` | One step row | `n`, `note`, `note-position` (`inline` default / `below`, the note under the label rather than beside it) | default = label |
255
265
  | `deck-shortcut-list` | Shortcut grid | `cols` (column count, e.g. `1`), `col-gap` (1..6) | `deck-shortcut` children |
256
266
  | `deck-shortcut` | One keyboard-shortcut row | `keys` (space-separated), `label`, `note`, `tone` (`accent`/`ok`) | default = note |
257
267
  | `deck-kbd` | Inline key chip | `tone` (`accent`/`ok`) | default = key text |
258
268
  | `deck-stack` | Flex stack helper | `gap` (1..6), `direction` (`row`/`column`), `align` (`start`/`center`/`end`/`stretch`), `justify` (`start`/`center`/`end`/`between`/`around`), `fill` (grow to fill the cross axis) | children |
259
269
  | `deck-grid` | CSS grid helper | `cols` (1..12 or template), `rows`, `gap` (1..6 or CSS), `align`, `justify`, `fill` | children |
270
+ | `deck-cell` | Bento grid item, sized BY THE GRID · a `container-type: size` box so child `cqw`/`cqh` type scales against the cell, not the slide. A slotted `img`/`svg`/`video` auto-fits the cell (object-fit contain); a slotted `table` fills the width | `span` (`"CxR"`, e.g. `2x1`, or a bare column count), `col`/`row` (per-axis override · integer → `span N`, else raw line syntax), `tone` (`info`/`warn`/`ok`/`danger`), `plain` (drop the card chrome), `flat` (keep the surface but drop the border), `align` (**horizontal**: `start`/`center`/`end`/`stretch`), `justify` (**vertical**: `start`/`center`/`end`/`between`) | default (`<h3>` + body, or any block) |
271
+ | `deck-point` | Bento grid item, sized BY ITS CONTENT · one point of a bento, for words. Not a size container, so a row of points is as tall as the tallest one and a painted point shows no hole under its text; a row made only of points, all with the same number of children and none claiming a span, shares the grid's rows, so a title that wraps to a second line no longer drags its own body text below its neighbours'. Reach for `deck-point` for words and `deck-cell` for anything measured (fit-to-cell text, a diagram, an image) | `span`, `col`/`row`, `tone` (`info`/`warn`/`ok`/`danger`), `plain` (stop painting the chrome · the gutter stays, so the reading edge survives), `flat`, `align` (**horizontal**) · no `justify`, a point has no leftover height to distribute | default (`<h3>` + body) |
272
+ | `deck-fit` | Shrink slotted content to fit its box by font-size (for non-`deck-punch` text content · not for images, which scale geometrically) | `min` (rem, default 1), `max` (rem, default 12) | default = any content |
273
+ | `deck-csv` | Render inline CSV as a styled table (cells are trimmed) | `delimiter` (default `,`), `no-header` (first row is data), `fit` (shrink the table to fit the cell), `fit-min`/`fit-max` (rem bounds, default 0.6/2), `highlight-rows` / `highlight-cols` (1-based, space-separated), `reveal` (one body row per step) | default = raw CSV text |
260
274
 
261
275
  ---
262
276
 
@@ -266,8 +280,9 @@ Direct children of `<deck-root>`. Each is one slide.
266
280
  |-----|---------|----------------|-------|
267
281
  | `deck-badge` | Small status badge | `type` (`bad`/`ok`/`info`/`warn`/`neutral`) | default = text |
268
282
  | `deck-kicker` | Uppercase eyebrow label | `on-dark` | default = text |
269
- | `deck-punch` | Short punchy line | `tone` (`warn`/`danger`/`ok`/`info`/`muted`/`accent`; inherits text color if absent), `size` (`lead`/`big`/`mega`/`stat`/`display`), `weight` (`700`/`800`/`900`), `align` (`left`/`center`/`right`) | default = text |
283
+ | `deck-punch` | Short punchy line | `tone` (`warn`/`danger`/`ok`/`info`/`muted`/`accent`; inherits text color if absent), `size` (`lead`/`big`/`mega`/`stat`/`display`), `weight` (`700`/`800`/`900`), `align` (`left`/`center`/`right`), `fit` (shrink to fit the box · overrides `size`/cqw fluid scaling), `fit-min`/`fit-max` (rem bounds, default 1/12) | default = text |
270
284
  | `deck-code` | Syntax-highlighted code | `lang`, `hero`, `nested`, `step-groups` | default = code text |
285
+ | `deck-source` | A source or credit line, placed under any evidence block (`deck-csv`, `deck-table`, `deck-bar`, `deck-kpi-grid`, `deck-annotate`, or plain prose) | `href` (turns the credit into a link) | default = the credit text |
271
286
 
272
287
  ### deck-code details
273
288
 
@@ -276,8 +291,8 @@ Direct children of `<deck-root>`. Each is one slide.
276
291
  `less`. **Any other value** (`python`, `rust`, `bash`, `go`, `sql`, …) is **not
277
292
  an error and produces no warning** — the block is silently colored with the JS
278
293
  rules, so the result looks plausible but is wrong. For any language outside the
279
- list above, install the Shiki plugin (see below); it adds Shiki's full set of
280
- grammars and is the only way to highlight non-built-in languages correctly.
294
+ list above, add or rebuild a compatible highlighter. Rikiki's optional Shiki
295
+ plugin covers a curated subset described below.
281
296
  - `hero` · centers the block vertically as the slide's focal element.
282
297
  - `nested` · lighter border, no shadow (for use inside a `deck-card`).
283
298
  - `step-groups` · a JSON array attribute that turns the snippet into a stepped
@@ -285,13 +300,12 @@ Direct children of `<deck-root>`. Each is one slide.
285
300
 
286
301
  Highlighting is done client-side with a built-in regex highlighter (no build
287
302
  step), limited to the languages listed under `lang` above. An opt-in **Shiki
288
- plugin** upgrades every `deck-code` block to Shiki's full grammar/theme set —
289
- **required** whenever a deck uses a language the built-in highlighter does not
290
- understand.
303
+ plugin upgrades every `deck-code` block to TextMate highlighting for a curated
304
+ set of common web languages.
291
305
 
292
306
  ### Shiki plugin (optional, opt-in)
293
307
 
294
- `src/plugins/shiki.ts` (`dist/shiki.js`) re-renders all `<deck-code>` blocks
308
+ The Shiki plugin (`dist/shiki.js`) re-renders all `<deck-code>` blocks
295
309
  through [Shiki](https://shiki.style), loaded from the vendored
296
310
  `dist/vendor/shiki.js` bundle on first use (offline · no CDN). Install it after
297
311
  the rikiki bundle:
@@ -300,7 +314,7 @@ the rikiki bundle:
300
314
  <script type="module" src="./dist/index.js"></script>
301
315
  <script type="module">
302
316
  import { installShiki } from './dist/shiki.js';
303
- await installShiki({ theme: 'one-dark-pro', langs: ['ts', 'tsx', 'html', 'css'] });
317
+ await installShiki({ theme: 'one-dark-pro', langs: ['ts', 'js', 'html', 'css'] });
304
318
  </script>
305
319
  ```
306
320
 
@@ -308,13 +322,15 @@ API:
308
322
 
309
323
  ```ts
310
324
  async function installShiki(opts?: {
311
- theme?: string; // any Shiki theme name (https://shiki.style/themes) · default 'one-dark-pro'
312
- langs?: string[]; // grammars to preload · default ['ts', 'js', 'html', 'css', 'json']
325
+ theme?: 'one-dark-pro';
326
+ langs?: Array<'ts' | 'typescript' | 'js' | 'javascript' | 'html' | 'css' | 'json'>;
313
327
  }): Promise<void>
314
328
  ```
315
329
 
316
- - Any Shiki theme/language works (not just the built-in highlighter's set); set
317
- `langs` to whatever your deck uses.
330
+ - The offline artifact contains only `one-dark-pro` and the TypeScript,
331
+ JavaScript, HTML, CSS and JSON grammars. Set `langs` to the subset your deck
332
+ uses. Supporting another grammar or theme requires rebuilding the vendor
333
+ entry with an explicit import.
318
334
  - Shiki owns the palette under this plugin: it colors each token with an inline
319
335
  style from the chosen `theme`, so pick a `theme` that suits your code
320
336
  background (e.g. `one-dark-pro` on a dark deck). The `--deck-code-syntax-*`
@@ -324,9 +340,8 @@ async function installShiki(opts?: {
324
340
  - **How it hooks in:** it registers a highlighter on the shared `<deck-code>`
325
341
  class via `setDeckCodeHighlighter` (resolved through `customElements.get`), so
326
342
  it never patches the component's internals · see *Writing a plugin* below.
327
- - **Trade-off:** the vendored Shiki bundle is large (every grammar + theme, JS
328
- engine, no wasm). That is why it is opt-in and lazy · the core bundle stays
329
- ~14 KB gzip.
343
+ - **Trade-off:** the curated runtime is about 113 KB when gzip-compressed. It remains opt-in and
344
+ lazy, so the core bundle stays ~43 KB gzip.
330
345
 
331
346
  ---
332
347
 
@@ -348,7 +363,7 @@ The step dots at the bottom of the deck reflect the active slide's step count.
348
363
 
349
364
  ### Click-stages plugin (per-element reveals)
350
365
 
351
- `src/plugins/click-stages.ts` adds Slidev-style `v-click` reveals. rikiki drives
366
+ The click-stages plugin (`dist/click-stages.js`) adds Slidev-style `v-click` reveals. rikiki drives
352
367
  them with **attributes** (`data-click` on any element) · it does **not** support
353
368
  Slidev's `<v-click>` / `<v-clicks>` wrapper elements. It is **opt-in** · not part
354
369
  of the core bundle. Install it after rikiki loads:
@@ -488,20 +503,20 @@ It is hidden in the deck itself; only the presenter window reads its text.
488
503
 
489
504
  ---
490
505
 
491
- ## 9 · Multi-deck assembly
506
+ ## 9 · Multi-file decks
492
507
 
493
- Split a long talk into small partial files and assemble them into one deck at
494
- build time. The assembler is `build/vite-deck.mjs` (pure Node · no runtime
495
- weight added).
508
+ A long talk is easier to write, review and diff in pieces. `rikiki assemble`
509
+ joins ordered partials into the single HTML file everything else expects.
496
510
 
497
511
  A `deck.config.js` (or `.json`) describes the deck:
498
512
 
499
513
  ```js
500
514
  export default {
501
515
  title: 'My talk',
502
- theme: '../../tokens.css', // theme href, relative to the OUTPUT file
503
- bundle: '../../dist/index.js', // rikiki bundle href, relative to OUTPUT
504
- transition: 'slide', // optional <deck-root transition="…">
516
+ theme: 'rikiki/tokens.css', // href, relative to the OUTPUT file
517
+ bundle: 'rikiki/dist/index.js', // runtime href, relative to OUTPUT
518
+ transition: 'slide', // optional <deck-root transition="…">
519
+ lang: 'fr', // optional <html lang="…">
505
520
  slides: [
506
521
  'parts/cover.html',
507
522
  'parts/intro.md',
@@ -514,38 +529,27 @@ export default {
514
529
  - **`.md` partials** can hold one or many slides. A line that is exactly `---`
515
530
  splits the file into separate slides (reveal.js convention); each chunk is
516
531
  wrapped into its own `<deck-feature><deck-md>…</deck-md></deck-feature>`. Use
517
- `***` for a horizontal rule inside a slide (since `---` is the slide break).
532
+ `***` for a horizontal rule inside a slide, since `---` is the slide break.
518
533
 
519
534
  Run it:
520
535
 
521
536
  ```bash
522
- node build/vite-deck.mjs decks/example/deck.config.js
523
- # or with an explicit output path:
524
- node build/vite-deck.mjs decks/example/deck.config.js dist-decks/example.html
537
+ npx rikiki assemble deck.config.js # → <title>.html next to the config
538
+ npx rikiki assemble deck.config.js out/talk.html # explicit output
539
+ npx rikiki assemble deck.config.js - # to stdout
525
540
  ```
526
541
 
527
- There are also npm scripts: `npm run deck <config>` and `npm run deck:example`.
528
- The default output file is named from `title` and written next to the config.
529
-
530
- ### Bundling caveat for assembled decks
531
-
532
- > The single-file export step (`bundle.mjs`, §12) only rewrites paths that use
533
- > the `rikiki/…` convention · specifically references matching
534
- > `rikiki/(dist|themes|tokens.css)` (as the decks under `examples/` do). It does
535
- > **not** resolve plain relative paths like `../../dist/index.js`.
536
- >
537
- > The in-repo `decks/example` deliberately uses `../../dist/index.js` /
538
- > `../../tokens.css` relative paths so it can be **served directly** for dev. As
539
- > a result, `decks/example`'s assembled output is meant for direct serving and
540
- > does **not** bundle via `bundle.mjs` as-is. To produce a bundleable assembled
541
- > deck, point its `deck.config.js` `theme`/`bundle` at the `rikiki/…`-style paths
542
- > that `bundle.mjs` rewrites.
542
+ `theme` and `bundle` default to the `rikiki/…` paths `rikiki init` writes, which
543
+ are the ones `rikiki bundle` inlines. Point them elsewhere and the deck still
544
+ serves, but the command says on stderr that the single-file export will leave
545
+ those references external.
543
546
 
544
- ---
547
+ Assembly is a one-way step: edit the partials, re-run, and keep the assembled
548
+ file as an artefact rather than a source.
545
549
 
546
550
  ## 10 · Livereload (authoring only)
547
551
 
548
- `src/livereload.ts` polls the `Last-Modified`/etag of the deck's files and
552
+ The livereload module (`dist/livereload.js`) polls the `Last-Modified`/etag of the deck's files and
549
553
  auto-reloads the page when any change (showing a brief toast and keeping the
550
554
  current slide via the hash). It watches: the deck's `<link rel="stylesheet">`
551
555
  hrefs, the rikiki component files in `dist/`, and the deck HTML itself.
@@ -553,7 +557,7 @@ hrefs, the rikiki component files in `dist/`, and the deck HTML itself.
553
557
  Enable it two ways:
554
558
 
555
559
  - **`?live`** on the deck URL · `dist/index.js` lazy-imports the poller only when
556
- this query param is present, e.g. `…/starter.html?live`.
560
+ this query param is present, e.g. `…/my-deck.html?live`.
557
561
  - **Load the module directly** · `<script type="module" src="./dist/livereload.js">`
558
562
  (it auto-starts on import).
559
563
 
@@ -613,27 +617,264 @@ Light-DOM helper classes the theme ships (use on slotted children):
613
617
 
614
618
  ## 12 · Bundling (single-file export)
615
619
 
616
- `bundle.mjs` (Vite + vite-plugin-singlefile) crawls a deck's `<link>` and
617
- `<script type="module">` references, bundles and inlines everything (Lit
618
- included) into one self-contained HTML file:
620
+ `rikiki bundle` crawls a deck's `<link>` and `<script type="module">`
621
+ references and inlines everything, lit included, into one self-contained HTML
622
+ file. It curates the runtime down to the components the deck actually uses.
619
623
 
620
624
  ```bash
621
- node bundle.mjs my-talk/index.html # → my-talk/index.bundle.html
622
- node bundle.mjs my-talk/index.html out.html # explicit output
623
- node bundle.mjs my-talk/index.html - # to stdout
624
- node bundle.mjs my-talk/index.html --no-fonts # strip Google Fonts @import (system fonts, zero network)
625
+ npx rikiki bundle my-talk/index.html # → my-talk/index.bundle.html
626
+ npx rikiki bundle my-talk/index.html out.html # explicit output
627
+ npx rikiki bundle my-talk/index.html - # to stdout
628
+ npx rikiki bundle my-talk/index.html --no-fonts # drop the web fonts, use system ones
629
+ npx rikiki bundle my-talk/index.html --with-mermaid # inline the mermaid runtime
630
+ npx rikiki bundle my-talk/index.html --with-shiki # inline the Shiki highlighter
625
631
  ```
626
632
 
627
- The bundler resolves rikiki references written with the `rikiki/(dist|themes|tokens.css)`
628
- path convention (see the §9 caveat about decks that use plain relative paths).
633
+ The command needs the optional peer `rolldown` (`npm i -D rolldown`); without
634
+ it, it says so and does nothing. It exits non-zero if the result would still
635
+ fetch something at runtime, so a file it accepts really opens offline.
636
+
637
+ It resolves rikiki references written in the `rikiki/(dist|themes|tokens.css)`
638
+ spelling · the one `rikiki init` writes.
629
639
 
630
640
  ---
631
641
 
642
+ ## 12b · Looking at a deck, and measuring it
643
+
644
+ You cannot see a deck you wrote. These two commands are the eyes and the ruler.
645
+ Both need the optional peer `playwright`.
646
+
647
+ ### `rikiki render` · pictures
648
+
649
+ ```bash
650
+ npx rikiki render talk.html # → talk.shots/, one PNG per slide
651
+ npx rikiki render talk.html --out previews/ # somewhere else
652
+ npx rikiki render talk.html --slides intro,4 # by id or by 1-based number
653
+ npx rikiki render talk.html --steps # every revealed state, not just the first
654
+ npx rikiki render talk.html --width 1280 --height 720
655
+ ```
656
+
657
+ It writes, beside the pictures:
658
+
659
+ - `index.html` · a plain gallery, no runtime, opens offline;
660
+ - `manifest.json` · the contract between a picture and the slide it came from.
661
+
662
+ ```json
663
+ {
664
+ "schema": 1,
665
+ "deck": "talk.html",
666
+ "canvas": { "width": 1920, "height": 1080 },
667
+ "slideCount": 12,
668
+ "captured": 12,
669
+ "stepsCaptured": false,
670
+ "shots": [
671
+ { "index": 1, "id": "intro", "tag": "deck-cover", "title": "…", "step": 0, "file": "01-intro.png" }
672
+ ]
673
+ }
674
+ ```
675
+
676
+ Read `stepsCaptured`. Without `--steps`, a stepped slide is photographed in its
677
+ opening state, which is usually the emptiest one it has: judging it then is
678
+ judging a slide nobody will see. File names are derived from the slide id and
679
+ are always safe, whatever the id contains; the manifest keeps the id verbatim,
680
+ which is what you edit against.
681
+
682
+ The command waits for the elements to upgrade, the fonts to load, the diagrams
683
+ to draw and every animation to finish before each shot. It does not sleep.
684
+
685
+ #### `--baseline <dir>` · what moved since last time
686
+
687
+ ```bash
688
+ npx rikiki render talk.html --out after/ --baseline before/
689
+ npx rikiki render talk.html --out after/ --baseline before/ --json > diff.json
690
+ npx rikiki render talk.html --out after/ --baseline before/ --threshold 0
691
+ ```
692
+
693
+ The deck is rendered as usual, then every PNG that has a same-named file in
694
+ `<dir>` is compared to it, in the browser that just took the pictures: both
695
+ images go on a canvas and `getImageData` counts the pixels whose worst channel
696
+ moved by more than 32 of 255. A file only one side has is reported as `added`
697
+ or `missing`, never as a diff; two captures of different sizes are `resized`,
698
+ with both sizes and no pixel count, because a ratio across a resize means
699
+ nothing.
700
+
701
+ Human output goes to stderr, one line per changed slide, most changed first:
702
+
703
+ ```
704
+ rikiki · diff · 1 slide(s) changed · most changed first
705
+ · 9.21% 01-intro.png · Le titre de la slide · box 101,383 1204×323
706
+ rikiki · diff · 1 changed · 11 stable · 0 added · 0 missing · 0 resized · baseline before/
707
+ ```
708
+
709
+ `--json` writes the whole report to stdout and nothing else; the same report is
710
+ always written to `diff.json`, beside `manifest.json`:
711
+
712
+ ```json
713
+ {
714
+ "schema": "rikiki.render-diff/1",
715
+ "baseline": "/abs/path/to/before",
716
+ "threshold": 0.5,
717
+ "slides": [
718
+ {
719
+ "file": "01-intro.png",
720
+ "slide": 1, "id": "intro", "title": "…", "step": 0,
721
+ "status": "changed",
722
+ "changedRatio": 0.0921, "changedPixels": 190941, "totalPixels": 2073600,
723
+ "box": { "left": 101, "top": 383, "width": 1204, "height": 323 }
724
+ },
725
+ { "file": "04-wide.png", "status": "resized",
726
+ "baselineSize": { "width": 1920, "height": 1080 }, "size": { "width": 1280, "height": 720 } },
727
+ { "file": "09-gone.png", "status": "missing" }
728
+ ],
729
+ "summary": { "changed": 1, "stable": 11, "added": 0, "missing": 1, "resized": 1 }
730
+ }
731
+ ```
732
+
733
+ `slides` is ranked by `changedRatio` descending, so the loudest slide is the
734
+ first thing read; the entries nobody could measure (`added`, `missing`,
735
+ `resized`) keep to the end, where the report names them one by one. `box` is
736
+ the smallest rect containing every changed pixel · it is where to look.
737
+
738
+ `--threshold` is a percentage of a slide's pixels: at or above it a slide is
739
+ `changed`, below it `stable`. The default `0.5` is there because re-rendering
740
+ an unchanged deck still repaints its anti-aliasing, and a report where 57 of 64
741
+ slides are "changed" hides the seven that really moved. `--threshold 0` lists
742
+ every slide where a single pixel moved; a slide where nothing moved stays
743
+ `stable` even then.
744
+
745
+ Exit code is 1 when at least one slide is `changed`, `missing` or `resized`, 0
746
+ otherwise · a slide the baseline never had is news, not a regression. A
747
+ `--baseline` pointing at a directory that is not there stops before the render,
748
+ with exit 2.
749
+
750
+ ### `rikiki check` · measurements
751
+
752
+ ```bash
753
+ npx rikiki check talk.html # a readable report on stderr
754
+ npx rikiki check talk.html --json # the report on stdout, nothing else
755
+ npx rikiki check talk.html --no-visual # skip the pixel pass (one shot per slide)
756
+ npx rikiki check talk.html --steps # measure every revealed state, not just the first
757
+ ```
758
+
759
+ Every slide is measured, in its opening state, whatever the mode: `check`
760
+ walks the deck one slide at a time, because `deck-root` lays out the slide on
761
+ screen and hides the rest · measuring the document in one shot would read empty
762
+ boxes for every slide but the first, and report a clean deck. The walk drives
763
+ the deck by its location hash; a deck it cannot navigate stops the walk at that
764
+ slide with `NAVIGATION_STALLED`, and the report still carries every slide
765
+ measured before it.
766
+
767
+ Without `--steps`, a stepped slide is only ever measured in its opening state
768
+ · `advanceStep`'s own geometry rarely changes with it (rikiki reveals dim and
769
+ highlight, it does not hide), but content wired to a step through other means
770
+ can still defect only once revealed. With `--steps`, each slide is walked from
771
+ its opening state through every state `advanceStep` reaches (`ArrowRight`),
772
+ running the same diagnostics on each. Every diagnostic then carries a `state`
773
+ (`0` for the opening state) both in the JSON and appended to the human line
774
+ (`· state 2`). A diagnostic identical on code, slide and path/message across
775
+ several states of one slide is reported once, at the lowest state it held.
776
+ `statesInspected` counts every state actually measured · without `--steps`
777
+ that is one per slide, so it reads the slide count.
778
+ The pixel pass still measures only the opening state of each slide either way
779
+ · `notChecked` says so.
780
+
781
+ The pixel pass photographs each slide with the engine's own chrome hidden, and
782
+ measures where the ink sits. It reports imbalance only · a slide whose content
783
+ fills the canvas is left alone whatever empty space remains. `visualMeasured`
784
+ in the report says whether it ran.
785
+
786
+ Exit codes: `0` nothing blocking, `1` defects found, `2` the deck could not be
787
+ looked at (bad invocation, unreadable file). An agent branches on those: `1`
788
+ means fix the deck, `2` means fix the call.
789
+
790
+ Each diagnostic carries a stable `code`, a `severity`, the slide it belongs to,
791
+ an `element` path that reaches into the Shadow DOM (`deck-feature#detail
792
+ ::shadow div`), the `measurement` that justifies it, and a `suggestion`.
793
+
794
+ | Code | Severity | What it means |
795
+ |---|---|---|
796
+ | `RUNTIME_NOT_LOADED` | error | the elements never registered · every slide is raw HTML |
797
+ | `NO_DECK_ROOT` / `NO_SLIDES` | error | nothing to show |
798
+ | `RESOURCE_MISSING` | error | a file the deck asked for did not arrive |
799
+ | `PAGE_ERROR` | error | the page threw during setup |
800
+ | `NAVIGATION_STALLED` | error | the deck never arrived at that slide · the walk stopped there and the report holds only what came before |
801
+ | `UNKNOWN_ELEMENT` | error | a misspelled `deck-*` tag · it renders as nothing at all |
802
+ | `STRAY_MARKUP` | warning | prose about markup that the parser turned into an element · escape the angle brackets |
803
+ | `CONTENT_CLIPPED` | error | the slide clips rather than scrolls · that content is lost |
804
+ | `CONTENT_ESCAPES_BOX` | error | a painted box runs past its nearest painted ancestor by more than 4px, and nothing clips · the box is too small for what is inside it |
805
+ | `CONTENT_OVERLAPS_SIBLING` | error | two unrelated painted boxes intersect by more than 8px on both axes · one paints over the other |
806
+ | `SLIDE_DENSE` | warning | nothing is cut yet, but there is no room left |
807
+ | `SLIDE_TOP_HEAVY` | warning | measured on the pixels · the ink sits in the top with a dead band under it |
808
+ | `TALK_SHORTER_THAN_ANNOUNCED` | warning | the notes carry far less speech than the cover announces |
809
+ | `TEXT_TOO_SMALL` | warning | below the readable floor once the canvas is scaled |
810
+ | `TEXT_LAST_LINE_ORPHAN` | warning | a block of prose ends on a stub under a quarter of the width above it · measured on the painted lines, headings and short blocks excepted · worst one per slide |
811
+ | `UNKNOWN_ATTRIBUTE` | warning | an attribute the element neither reads nor styles on · the value is dropped |
812
+ | `DUPLICATE_SLIDE_ID` | warning | two slides answer to the same name |
813
+ | `EXTERNAL_DEPENDENCY` | warning | the deck fetches from the network at runtime |
814
+ | `GRAPH_NODE_OUT_OF_BOUNDS` | error | a `deck-node` is painted outside its `deck-graph` canvas · move it inward with `at`, shorten its note, or constrain it with `width` |
815
+ | `GRAPH_EDGE_CROSSES_NODE` | warning | the line a `deck-edge` actually paints, bends and stroke width included, runs over a node it does not connect · move the obstructing node, or route the edge around it |
816
+ | `GRAPH_NODE_OVERLAPS_NODE` | error | two `deck-node` of the same `deck-graph` are painted on top of each other · one of them is unreadable |
817
+ | `GRAPH_NODE_COVERS_LABEL` | error | a `deck-node` is painted over a `deck-group` / `deck-lane` / edge caption · move the node with `at`, or the region with its own `at` |
818
+ | `GRAPH_EDGE_SKEWED` | warning | an edge misses horizontal or vertical by a few pixels · a frank diagonal is left alone, a three-degree slope is a slip · align the two `at` coordinates, or set `route="ortho"` |
819
+ | `GRAPH_NODES_OFF_AXIS` | warning | two nodes sit within the alignment slack of the same row or column without sharing it · worst offender per graph |
820
+ | `GRAPH_NODE_SIZES_MIXED` | warning | two nodes of one row (or column) differ by a few pixels in height (or width) · near-equal boxes read as a failed attempt at the same size, plainly different ones are left alone |
821
+ | `GRAPH_LAYOUT_NOT_SEMANTIC` | warning | every node of a graph of three or more sits on one axis, with no `layout` · the `at` coordinates re-do what `layout="row"` / `layout="column"` says |
822
+
823
+ The report also carries `notChecked`, which names what was **not** looked at:
824
+ revealed steps, accessibility, wording and facts, other viewports, text inside
825
+ a diagram, and the size of a component's own chrome. Silence about a check that
826
+ never ran would read as a clean bill.
827
+
828
+ Two things it will not do: call a slide bad for having empty space, and claim a
829
+ deck is accessible. Space is a choice, and a handful of measurements is not an
830
+ accessibility audit.
831
+
632
832
  ## 13 · Recipes / cookbook
633
833
 
834
+ For choosing a composition from what a slide has to say, see
835
+ [`rikiki-workflow.md`](./rikiki-workflow.md). This section is the wiring: what
836
+ to type once the composition is chosen.
837
+
634
838
  Copy-paste patterns. Every tag/attribute used here is defined above · combine
635
839
  them freely. Assume the deck head loads the theme then `dist/index.js` (§2).
636
840
 
841
+ ### An opt-in component slide (§20)
842
+
843
+ The opt-in components are not in `dist/index.js` · load the ones you use, one
844
+ script tag each, after the core bundle. They follow the direction in §20: one
845
+ filled area at most, two type sizes, no label above content.
846
+
847
+ ```html
848
+ <script type="module" src="dist/index.js"></script>
849
+ <script type="module" src="dist/deck-kpi-grid.js"></script>
850
+ <script type="module" src="dist/deck-flow.js"></script>
851
+ ```
852
+
853
+ ```html
854
+ <deck-feature eyebrow="Numbers" spread="center">
855
+ <h1 slot="title">Where the time went</h1>
856
+ <deck-kpi-grid cols="3">
857
+ <deck-kpi value="34" label="components in the default bundle"></deck-kpi>
858
+ <deck-kpi value="14" label="more, one script tag each" tone="accent"></deck-kpi>
859
+ <deck-kpi value="0" label="network calls at runtime" note="a bundled deck runs offline"></deck-kpi>
860
+ </deck-kpi-grid>
861
+ </deck-feature>
862
+
863
+ <deck-feature eyebrow="Chain" spread="center" data-steps="4">
864
+ <h1 slot="title">One stage at a time</h1>
865
+ <deck-flow cols="4" reveal>
866
+ <deck-flow-step label="Write" note="one HTML file"></deck-flow-step>
867
+ <deck-flow-step label="Preview" note="open it"></deck-flow-step>
868
+ <deck-flow-step label="Bundle" note="one command"></deck-flow-step>
869
+ <deck-flow-step label="Present" note="offline"></deck-flow-step>
870
+ </deck-flow>
871
+ </deck-feature>
872
+ ```
873
+
874
+ At step 0 the whole chain is visible and neutral · its shape is half the
875
+ message. Each step then moves the block to the stage being discussed. A running
876
+ version of both slides is in `examples/rikiki-tour/index.html`.
877
+
637
878
  ### Markdown + code feature slide
638
879
 
639
880
  ```html
@@ -684,6 +925,62 @@ them freely. Assume the deck head loads the theme then `dist/index.js` (§2).
684
925
  </deck-feature-cards>
685
926
  ```
686
927
 
928
+ ### Bento layout (adaptive cells + fit-to-box text)
929
+
930
+ A `deck-cell` stacks its children in a column, so its two alignment knobs run
931
+ on the axes their names do not suggest: **`align` moves content left/right**,
932
+ **`justify` moves it up/down**. To push a cell's content to the bottom, write
933
+ `justify="end"`, not `align="end"`.
934
+
935
+ Cells share a multi-row/column grid via `span`. Each `deck-cell` is a size
936
+ container, so `cqw`/`cqh` type (and `<deck-punch fit>`) adapts to the cell it
937
+ lands in, not the whole slide. Use `fit` when the text length is unknown and
938
+ must never overflow; use `size="display"` (or any `cqw`-based size) for plain
939
+ fluid scaling without the JS measure.
940
+
941
+ ```html
942
+ <deck-bento eyebrow="Overview" cols="3" rows="2" gap="3">
943
+ <h1 slot="title">Sharing the space</h1>
944
+ <deck-cell span="2x1" align="center" justify="center">
945
+ <deck-punch fit>Headline that shrinks to fit its cell</deck-punch>
946
+ </deck-cell>
947
+ <deck-cell span="1x2" tone="info"><h3>Side</h3><deck-md>Tall cell.</deck-md></deck-cell>
948
+ <deck-cell tone="ok"><h3>A</h3></deck-cell>
949
+ <deck-cell tone="warn"><h3>B</h3></deck-cell>
950
+ </deck-bento>
951
+ ```
952
+
953
+ ### Cards from markdown (`::: cards`)
954
+
955
+ Author a whole slide in markdown and let `deck-md` expand a `::: cards` fenced
956
+ block into a tinted card grid · the fast way to write a bento without HTML.
957
+ Inside the fence, `:: <tone> <span> | <title>` opens a card (both `tone` —
958
+ `info`/`warn`/`ok`/`danger` — and `span` like `2x1` are optional, any order);
959
+ the lines until the next `::` are its markdown body. The fence takes `cols=N`
960
+ and `gap=N` (default `cols` = number of cards).
961
+
962
+ ```html
963
+ <deck-feature>
964
+ <deck-md>
965
+ # You won't write it all yourself
966
+
967
+ Every dependency is a trade-off:
968
+
969
+ ::: cards cols=3
970
+ :: warn | Reproducibility
971
+ same versions on every machine, every CI run
972
+ :: warn | Build cost
973
+ source you compile vs prebuilt you link
974
+ :: ok 2x1 | Control
975
+ who owns the version, the flags, the patches
976
+ :::
977
+ </deck-md>
978
+ </deck-feature>
979
+ ```
980
+
981
+ For pixel control (true cell spans, `fit` text, images, `deck-csv`), author the
982
+ grid explicitly with `deck-bento` + `deck-cell` instead.
983
+
687
984
  ### Big-number stat
688
985
 
689
986
  ```html
@@ -826,6 +1123,95 @@ slide to the second.
826
1123
 
827
1124
  ## 14 · Authoring rules for LLMs
828
1125
 
1126
+ Everything above this section says what you MAY write. This one says what makes
1127
+ the result worth projecting, because the two are not the same question and
1128
+ decks assembled correctly from the tables above have been rejected in a room.
1129
+
1130
+ ### 14.1 · What the slide is
1131
+
1132
+ A slide is read from three to ten metres, for about forty seconds, while
1133
+ someone talks over it, and then again as a thumbnail in the overview grid and
1134
+ as a page in a PDF. What survives that trip is **area, size, position, one
1135
+ saturated colour and empty space**. What does not survive is a hairline, type
1136
+ under about twenty pixels on the wall, letter spacing, small caps, a
1137
+ low-contrast tint, and anything that has to be compared against something else
1138
+ to be understood. Compose from the first list. See
1139
+ `docs/design/adr-002-extras-visual-direction.md`.
1140
+
1141
+ ### 14.2 · The title is the message, the body is the proof
1142
+
1143
+ A content slide's title is a **full sentence stating what the slide argues**,
1144
+ about eight to fourteen words, and the body is its **evidence** · a figure, a
1145
+ diagram, a table, a number. Not a bullet list restating the title.
1146
+
1147
+ This is the assertion-evidence structure (Michael Alley, Penn State), and it is
1148
+ here because the comprehension gain over the usual topic-and-subtopic slide is
1149
+ measured and statistically significant, not because it reads better.
1150
+
1151
+ A topic title carries no message, so the body has to carry all of it, and the
1152
+ slide ends up with nothing to look at. "Graph" names a subject; "An architecture
1153
+ reads as boxes and arrows, never as paragraphs" says something the evidence can
1154
+ then support.
1155
+
1156
+ It costs nothing in type size: at the shipped title size the usable width holds
1157
+ about forty-four characters a line, so fourteen words fit on two lines. It does
1158
+ cost height · a two-line title takes a line back from the body, and
1159
+ the repository's slide-budget suite will say so.
1160
+
1161
+ `deck-cover` and `deck-section` are exempt. A chapter title is a boundary, not
1162
+ an assertion, and three words are right there.
1163
+ a test in the repository holds the band on every shipped deck.
1164
+
1165
+ ### 14.3 · The five rules
1166
+
1167
+ 1. **One loud thing.** A slide has one statement. Everything else on it is
1168
+ quiet. Two loud things means the eye picks the wrong one.
1169
+ 2. **Two sizes, and the gap between them is the design.** A statement and a
1170
+ reading size, nothing in between. The theme sets the statement at more than
1171
+ twice the reading size on purpose · a title at 1.5x reads as body text in
1172
+ bold, which is the single most common reason a deck looks flat.
1173
+ 3. **One mass at most.** One filled area per slide, carrying whatever the
1174
+ content actually marks. Nothing marked means nothing filled. On these themes
1175
+ a pale tint is invisible in a room; real emphasis is the inverse surface.
1176
+ 4. **A line only where two things would otherwise touch.** A table header, a
1177
+ timeline axis, an edge in a diagram. Never as decoration or as a signature.
1178
+ 5. **Left, ragged right.** The vertical edge down the left is what makes a
1179
+ glance cheap. Centre a whole slide if you mean to; never centre a column
1180
+ inside a row of columns, because it breaks that edge.
1181
+
1182
+ ### 14.4 · Filling the canvas
1183
+
1184
+ Most rejected slides put their content in the top fifth and leave the rest
1185
+ white. That is almost never a component problem · it is a deck that never asked
1186
+ for a distribution. `spread` and `fill` (§19) are the answer, and the choice
1187
+ between them is: `spread` when the type size is right and only the rhythm is
1188
+ wrong, `fill` when the slide is genuinely under-filled and the text should grow
1189
+ into it (pair it with `<deck-fit>`).
1190
+
1191
+ Prefer cutting to shrinking. If a slide needs a third type size or a smaller
1192
+ body to fit, it is two slides. `<deck-notes>` takes what does not fit, and a
1193
+ stepped reveal (§7) is the medium's own way of showing a lot without crowding.
1194
+
1195
+ ### 14.5 · Rows of items
1196
+
1197
+ Reach for **`<deck-point>`** when a bento item holds words, and **`<deck-cell>`**
1198
+ when it holds something that must be measured to its box · fit-to-cell text, a
1199
+ diagram, an image.
1200
+
1201
+ The difference is not cosmetic. A row of `deck-point` is as tall as its tallest
1202
+ point, and a row made only of points (same number of children each, no spans,
1203
+ no declared `rows`) shares the grid's bands, so a title that wraps to a second
1204
+ line does not drag its own body text below its neighbours'. A row of
1205
+ `deck-cell` takes a share of the slide instead, so a cell that carries a surface
1206
+ will show a box taller than its text. Mixing them in one row is allowed and
1207
+ costs the shared bands.
1208
+
1209
+ Whichever you use, do not reach for `plain` to remove the gutter · it keeps the
1210
+ gutter on purpose, so a painted item and a plain one start their text on the
1211
+ same edge.
1212
+
1213
+ ### 14.6 · Mechanics
1214
+
829
1215
  - **Never nest `<deck-root>`.** One per document.
830
1216
  - **Load theme CSS before `dist/index.js`.**
831
1217
  - Every direct child of `<deck-root>` is one slide; keep **one focal idea per
@@ -838,3 +1224,508 @@ slide to the second.
838
1224
  (or component `--deck-*-…` tokens on one host). Do not hardcode colors.
839
1225
  - Move detail into **`<deck-notes>`** rather than crowding the slide.
840
1226
  - Only use tags, attributes, and tokens listed in this document.
1227
+
1228
+ ---
1229
+
1230
+ ## 15 · Trust model
1231
+
1232
+ - **Deck content is trusted.** `deck-md` renders raw HTML from the markdown you
1233
+ write, on purpose. That is the contract: a deck is a page its author
1234
+ publishes. There is no `sanitize` attribute and none is planned.
1235
+ - **Derived text is not.** A renderer error message quotes the source that broke
1236
+ it, so it is escaped before display. Never assume an error string is inert.
1237
+ - **mermaid runs at `strict`.** HTML in diagram labels is encoded and
1238
+ click-bound scripts are refused. There is no permissive opt-in.
1239
+ - **Never render untrusted markdown or diagram source.** If the content comes
1240
+ from a form, an API or a CMS field a stranger can edit, sanitise it before it
1241
+ reaches `<deck-md>`. rikiki is a presentation engine, not a sandbox.
1242
+
1243
+ ---
1244
+
1245
+ ## 16 · The three ways a deck runs
1246
+
1247
+ Pick one deliberately · they make different promises.
1248
+
1249
+ **Served** · the folder as you wrote it. HTML, CSS and JS stay separate files,
1250
+ so editing a slide needs no build. ES modules mean it needs a **static HTTP
1251
+ server**, not a double-click: `npx serve .`, or anything that speaks HTTP.
1252
+ Offline once every asset is local.
1253
+
1254
+ **Standalone** · one HTML file, produced by `rikiki bundle deck.html out.html`.
1255
+ Opens straight from `file://`, so it survives a USB stick, an email attachment
1256
+ and an archive. Nothing is fetched at runtime: scripts, styles, fonts and images
1257
+ are all inside. The presenter works from it too.
1258
+
1259
+ - A deck using `<deck-mermaid>` needs `--with-mermaid` (+~3 MB).
1260
+ - A deck using Shiki needs `--with-shiki` (about +0.7 MB raw, 113 KB compressed).
1261
+ - Without the flag the command **fails** and names the missing runtime · it will
1262
+ not hand you a file that renders an empty diagram offline.
1263
+ - `rikiki init --standalone` produces a starter with the same guarantees.
1264
+
1265
+ **CDN** · two `<script>`/`<link>` tags from jsdelivr. Zero install, but the deck
1266
+ fetches the framework every time it runs, so it needs the network and is **not**
1267
+ an archival format. Always pin a version.
1268
+
1269
+ The self-containment of a standalone file is enforced, not assumed:
1270
+ the bundle suite writes each bundle outside the repository, opens it over
1271
+ `file://`, and fails if the page issues a single request beyond itself.
1272
+
1273
+ ---
1274
+
1275
+ ## 17 · Printing and PDF export
1276
+
1277
+ A deck carries its own print stylesheet · no mode to switch on, no separate
1278
+ build.
1279
+
1280
+ - **One slide, one page.** Every slide prints, in order, at the deck's canvas
1281
+ size. The `@page` box is written from `--deck-canvas-w/h`, so a 16:9 deck
1282
+ prints 16:9 · forcing A4 crops it.
1283
+ - **Backgrounds are kept** (`print-color-adjust: exact`) · a tinted layout means
1284
+ nothing in black and white.
1285
+ - **Chrome is dropped** · counter, progress bar, nav arrows, key hints, step dots.
1286
+ - **Steps do not multiply pages.** A slide with click-stages prints once, fully
1287
+ revealed. Speaker notes stay out.
1288
+
1289
+ From the browser: print, backgrounds on. From the command line:
1290
+
1291
+ ```sh
1292
+ rikiki export deck.html --output deck.pdf
1293
+ ```
1294
+
1295
+ The command serves the deck over HTTP (ES modules need it), waits for
1296
+ `document.fonts.ready` and for every `<deck-mermaid>` to settle, then prints. It
1297
+ reports any asset it could not load rather than handing back a silently
1298
+ incomplete PDF. It needs **Playwright**, an optional peer dependency:
1299
+ `npm i -D playwright && npx playwright install chromium`.
1300
+
1301
+ The whole contract is verified by a print suite that reads the produced
1302
+ PDF back with poppler.
1303
+
1304
+ ---
1305
+
1306
+ ## 18 · Embedding a deck in a page
1307
+
1308
+ A `<deck-root>` that is a direct child of `<body>` **is** the page: it owns the
1309
+ scroll, the rem baseline, the URL hash, the keyboard and the wheel. Anywhere
1310
+ else it is a widget, and owns none of that.
1311
+
1312
+ An embedded deck:
1313
+
1314
+ - **leaves the host's styling alone.** The theme's reset, page background and
1315
+ helper classes (`.accent`, `.lead`, `.display`, `table.dense` …) are scoped to
1316
+ the deck subtree. Importing a theme does not restyle the page around it.
1317
+ - **does not touch the URL.** `#section-3` stays the host's anchor. Deep links
1318
+ work on a full-page deck only.
1319
+ - **takes the keyboard only while focused.** It gets `tabindex="0"`, so a reader
1320
+ tabs to it and then navigates with the arrow keys. Until then the arrows
1321
+ belong to the host page.
1322
+ - **lets the wheel scroll the host.** Ctrl/⌘ + wheel still zooms, and panning
1323
+ still works once a slide is magnified.
1324
+
1325
+ **Several decks per document are supported.** Each keeps its own canvas, its own
1326
+ slide index and its own navigation; focus decides which one the keyboard drives.
1327
+ Only one of them can be the page. Pinned by the multi-deck suite.
1328
+
1329
+ Recommended for a thumbnail or an inline demo:
1330
+
1331
+ ```html
1332
+ <div style="width: 640px; height: 360px">
1333
+ <deck-root no-hint no-arrows>…</deck-root>
1334
+ </div>
1335
+ ```
1336
+
1337
+ ---
1338
+
1339
+ ## 19 · Filling the slide vertically
1340
+
1341
+ A slide body takes the height left under the title, but its blocks stack at the
1342
+ top. A short slide therefore leaves most of the canvas empty · measured at 62%
1343
+ on a three-line `deck-feature`. Two opt-in attributes on `deck-feature`,
1344
+ `deck-split` and `deck-takeaway` share that space. Both are no-ops when absent,
1345
+ so existing decks are untouched.
1346
+
1347
+ | Attribute | Effect |
1348
+ |---|---|
1349
+ | `spread="between"` | push the blocks apart over the full height |
1350
+ | `spread="around"` / `"evenly"` | distribute the space around / between evenly |
1351
+ | `spread="center"` / `"end"` / `"start"` | group the blocks, centred / bottom / top (default) |
1352
+ | `fill` | give the height to the blocks themselves rather than to the gaps |
1353
+
1354
+ An unknown `spread` value falls back to `start` rather than dropping the
1355
+ layout.
1356
+
1357
+ `fill` on its own makes the blocks taller, not the text. Pair it with
1358
+ `<deck-fit>` to grow the text into the box it now has:
1359
+
1360
+ ```html
1361
+ <deck-feature fill>
1362
+ <h1 slot="title">Filled</h1>
1363
+ <deck-fit max="5">One line that grows to fill its share of the slide.</deck-fit>
1364
+ <deck-fit max="5">And a second one.</deck-fit>
1365
+ </deck-feature>
1366
+ ```
1367
+
1368
+ Which to reach for: `spread` when the type size is right and only the rhythm is
1369
+ wrong; `fill` when the slide is genuinely under-filled and the text should be
1370
+ bigger. `deck-bento` remains the answer when the content wants a grid rather
1371
+ than a stack.
1372
+
1373
+ ---
1374
+
1375
+ ## 20 · Opt-in components
1376
+
1377
+ Some components are **not** in `dist/index.js`. A deck that does not use them
1378
+ pays nothing for them · manifesto principle 3, light by default. Each is its own
1379
+ module, loaded next to the bundle:
1380
+
1381
+ ```html
1382
+ <script type="module" src="dist/index.js"></script>
1383
+ <script type="module" src="dist/deck-bar.js"></script>
1384
+ <script type="module" src="dist/deck-quote.js"></script>
1385
+ ```
1386
+
1387
+ `rikiki bundle` folds a loaded module into the single file like any other
1388
+ script, so a standalone deck keeps them and stays offline. Forget the `<script>`
1389
+ and the tag stays an unknown element: it renders its text content, logs nothing,
1390
+ and the rest of the deck is unaffected.
1391
+
1392
+ **Past about four added modules, stop adding script tags and bundle.** Each
1393
+ module is compiled on its own and carries its own copy of the shared code, and
1394
+ gzip compresses one large repetitive file far better than several small ones,
1395
+ so script tags stop paying off quickly. Measured on this release, gzipped:
1396
+
1397
+ | what the deck loads | gzip |
1398
+ |---|---|
1399
+ | `dist/index.js` alone | 26 KB |
1400
+ | `dist/index.js` + 3 modules | 30 KB |
1401
+ | `dist/index.js` + 5 modules | 33 KB |
1402
+
1403
+ `rikiki bundle` carries only the components the deck actually writes, in one
1404
+ file, so it is smaller than the plain bundle for any deck that does not use
1405
+ every component · and it is the only option that gets *smaller* as the deck
1406
+ gets simpler, rather than larger. Script tags are the convenient path for one
1407
+ or two extras, not the cheap one.
1408
+
1409
+ Every evidence block below (`deck-csv`, `deck-table`, `deck-bar`,
1410
+ `deck-kpi-grid`, `deck-annotate`, and plain prose) takes a `deck-source`
1411
+ underneath it for the credit line · `deck-source` is a core atom (§6), not an
1412
+ opt-in module, because so many of these need it. `deck-figure` renders one
1413
+ internally for its own `source` / `source-href` attributes, so the two
1414
+ authoring paths render identically and cannot drift.
1415
+
1416
+ | Tag | Purpose | Key attributes | Slots |
1417
+ |-----|---------|----------------|-------|
1418
+ | `deck-bar` | A proportion, drawn · one value against a total, or a stack of categories on one track | `value`, `total`, `label`, `tone` (`accent`/`ok`/`warn`/`danger`/`info`/`muted`), `segments` (`label:value:tone` triples separated by `\|`), `no-value`, `no-legend` | · |
1419
+ | `deck-icon` | A symbol · one of 24 drawn glyphs by `name`, or any `<svg>` you slot in. Nothing is vendored; `rikiki bundle` keeps only the glyphs the deck writes | `name`, `size` (`sm`/`md`/`lg`/`xl`), `tone`, `label` (absent means decorative, and it is hidden from assistive technology) | default = a fallback `<svg>` |
1420
+ | `deck-checklist` / `deck-check` | What works and what does not, told apart by shape as well as colour | list: `cols` · item: `no` | item default = the text |
1421
+ | `deck-kpi-grid` / `deck-kpi` | Several figures that read as one family · the grid owns the value / label / note rows and every figure adopts them, so all the values share one baseline and all the labels sit on one line | grid: `cols`, `ruled` (a visible divider between figures) · figure: `value` (statement size · `--deck-kpi-value-size` for a fluid one), `label`, `note`, `tone` (`accent`/`ok`/`warn`/`danger` put the figure on the inverse surface and colour its label with the tone; `default` and `muted` paint nothing) | · |
1422
+ | `deck-pull` | An excerpt lifted out of a dense slide · text wraps around it when floated | `side` (`full`/`left`/`right`) | default = the excerpt |
1423
+ | `deck-persona` | Who is speaking, or who the case study is about · the portrait block is the inverse surface, so the person has a place on the slide | `name`, `person-role` (**not** `role`), `org`, `context` (the quiet line, gapped away from the identity), `src` (a portrait; initials in inverse ink stand in without one), `on-dark` (the block flips to paper with ink initials), `compact` (shrinks the block and the name together, for a supporting persona), `inline` (name, role and context on one wrapping row) | · |
1424
+ | `deck-versus` | A directed comparison as a BLOCK inside a slide (`deck-split` covers the case where the comparison is the whole slide) | `pivot`, `winner` (`left`/`right`), `slide` (make the comparison a deck-root slide of its own, with `title` and `lead` slots), `eyebrow` (context label above the title, slide mode only) | `title`, `lead`, `left`, `right`, `footer` (full width, under both sides, slide mode only; a slotted `deck-callout` keeps its own size) |
1425
+ | `deck-flow` / `deck-flow-step` | A chain across the width · numbered stages, and only the active one takes the block | flow: `cols`, `reveal` · stage: `label`, `note` | stage default = extra content |
1426
+ | `deck-timeline` / `deck-milestone` | A trajectory in time, on an axis | timeline: `direction` (`row`/`column`), `alternate` (row only · milestones alternate above and below the axis, each twice as wide), `reveal` · milestone: `date`, `label`, `note`, `tone` | · |
1427
+ | `deck-graph` / `deck-node` / `deck-edge` / `deck-group` / `deck-lane` | Nodes, edges, regions and bands · the primitive behind every boxes-and-arrows slide | graph: `layout` (`free`/`row`/`column`), `reveal` · node: `at` (`x,y` in percent), `label`, `note`, `boxed`, `tone`, `icon`, `width` (an explicit CSS width such as `18ch` or `240px`, so a long label wraps instead of colliding) · edge: `from`, `to`, `label`, `dashed`, `arrow` (`end` default / `start` / `both` / `none`), `route` (`straight` default / `ortho` for right-angle segments), `label-offset` (`x,y` in pixels, moves the label off the line) · each edge publishes the polyline it paints back onto itself as `data-path` (`x1,y1 x2,y2[ ...]`, graph-relative CSS pixels), which is what `rikiki check` reads · group: `at` (`x,y,w,h`), `label`, `solid` · lane: `at` (`top,height`), `label` | node default = extra content |
1428
+ | `deck-table` | A hand-authored table with the hierarchy `deck-csv` has · the table stays in your light DOM, so its cells may carry markup | `highlight-rows`, `highlight-cols`, `reveal` | default = your `<table>` |
1429
+ | `deck-annotate` | A screenshot the speaker can point at · numbered markers positioned in percent, revealed one per step through the engine's own step mechanism · a real `<figure>`/`<figcaption>`, so it can carry its own caption and source like `deck-figure` | `src`, `alt`, `marks` (`x,y,label` triples separated by `\|`, coordinates in percent), `all-at-once`, `no-legend`, `leader` (draw a line from the target point to a displaced badge), `offset` (`x,y` in pixels, or a named side `above` / `below` / `left` / `right` : the badge is displaced by its own rendered diameter plus `--deck-annotate-anchor-gap` in that direction, and its leader turns on regardless of `leader`), `offsets` (per-mark displacements separated by `\|`, mixing pixel and keyword forms, e.g. `above\|0,-40\|right`; a missing entry falls back to `offset`), `caption`, `source`, `source-href` (turns the credit into a link) | `caption`, `source` |
1430
+ | `deck-agenda` | The running order and where the talk is · reads the deck's own chapter structure, so adding a `deck-section` grows a line | `no-numbers`, `no-jump` | · |
1431
+ | `deck-quote` | Someone else's words, attributed · distinct from `deck-punch`, which is the speaker's own line | `author`, `author-role` (**not** `role`, which belongs to ARIA), `size` (`lead`/`big`/`mega`), `plain` (drop the rule beside the quote), `on-dark`, `no-mark` | default = the quoted text |
1432
+ | `deck-figure` | A screenshot, a diagram or a chart as content · the image keeps its ratio, and its caption and credit stay attached to it semantically | `src`, `alt` (required unless `decorative`), `caption`, `source`, `source-href` (turns the credit into a link), `decorative` (the image carries no information · produces `alt=""`) | `image`, `caption`, `source` |
1433
+
1434
+ Tokens follow the usual per-component convention and every default routes to a
1435
+ semantic `--rik-*` token, so both shipped themes are covered:
1436
+ `--deck-bar-track`, `--deck-bar-height`, `--deck-bar-radius`, `--deck-bar-fill`,
1437
+ `--deck-bar-divider`, `--deck-bar-legend-color`, `--deck-bar-label-color`,
1438
+ `--deck-bar-value-color`; `--deck-quote-color`, `--deck-quote-size`,
1439
+ `--deck-quote-rule`, `--deck-quote-mark-color`, `--deck-quote-author-color`,
1440
+ `--deck-quote-role-color`, `--deck-quote-max-width`;
1441
+ `--deck-annotate-mark-bg`, `--deck-annotate-mark-color`, `--deck-annotate-mark-size`,
1442
+ `--deck-annotate-mark-ring`, `--deck-annotate-radius`, `--deck-annotate-legend-color`,
1443
+ `--deck-annotate-leader`, `--deck-annotate-leader-width`, `--deck-annotate-anchor-gap`
1444
+ (defaults to `--rik-space-2`, the gap a keyword offset leaves between the
1445
+ target point and the badge), `--deck-annotate-gap`
1446
+ (defaults to `--deck-figure-gap`), `--deck-annotate-caption-color` (defaults to
1447
+ `--deck-figure-caption-color`);
1448
+ `--deck-agenda-current-color`, `--deck-agenda-done-color`, `--deck-agenda-rule`,
1449
+ `--deck-agenda-marker`, `--deck-agenda-size`, `--deck-agenda-gap`;
1450
+ `--deck-icon-size`, `--deck-icon-color`, `--deck-icon-stroke`;
1451
+ `--deck-check-yes`, `--deck-check-no`, `--deck-check-size`,
1452
+ `--deck-check-no-color`, `--deck-checklist-rule`;
1453
+ `--deck-kpi-value-size` (statement size by default · a slide with room can go
1454
+ fluid with `clamp(2.25rem, 6cqw, 7rem)`, where `cqw` measures the nearest
1455
+ declared container and falls back to the viewport when the deck declares
1456
+ none), `--deck-kpi-value-color`,
1457
+ `--deck-kpi-label-color`,
1458
+ `--deck-kpi-note-color`, `--deck-kpi-mass`, `--deck-kpi-mass-text`,
1459
+ `--deck-kpi-block-pad-x` (set it on the grid or above, never on one figure ·
1460
+ the grid offsets itself by this value so the first column's ink lands on the
1461
+ slide's text edge), `--deck-kpi-block-pad-y`,
1462
+ `--deck-kpi-grid-cols`, `--deck-kpi-grid-gap`, `--deck-kpi-grid-row-gap`,
1463
+ `--deck-kpi-grid-rule`, `--deck-kpi-grid-rule-width`;
1464
+ `--deck-pull-rule`, `--deck-pull-size`, `--deck-pull-width`, `--deck-pull-font`;
1465
+ `--deck-persona-avatar-size`, `--deck-persona-avatar-bg`,
1466
+ `--deck-persona-initials-color`, `--deck-persona-initials-size`,
1467
+ `--deck-persona-name-size`, `--deck-persona-name-color`,
1468
+ `--deck-persona-role-color`, `--deck-persona-context-color`,
1469
+ `--deck-persona-gap`, `--deck-persona-name-lift`, `--deck-persona-compact-avatar-size`,
1470
+ `--deck-persona-compact-name-size`;
1471
+ `--deck-versus-winner-ring`, `--deck-versus-loser-opacity`, `--deck-versus-pivot-color`,
1472
+ `--deck-versus-slide-bg`, `--deck-versus-eyebrow-color`, `--deck-versus-side-align`,
1473
+ `--deck-versus-footer-gap` (defaults to `--rik-space-3`);
1474
+ `--deck-flow-gap`, `--deck-flow-step-accent`;
1475
+ `--deck-timeline-axis`, `--deck-milestone-dot`;
1476
+ `--deck-graph-edge`, `--deck-graph-edge-width`, `--deck-graph-ratio`, `--deck-node-rule`,
1477
+ `--deck-node-bg`, `--deck-group-border`, `--deck-lane-rule`;
1478
+ `--deck-table-mark-bg`, `--deck-table-mark-color`, `--deck-table-col-color`,
1479
+ `--deck-table-col-on-mark`;
1480
+ `--deck-figure-gap`, `--deck-figure-radius`, `--deck-figure-border`, `--deck-figure-bg`,
1481
+ `--deck-figure-image-fit`, `--deck-figure-image-position`, `--deck-figure-max-height`,
1482
+ `--deck-figure-caption-color`, `--deck-figure-source-color` (forwarded to the
1483
+ `deck-source` it renders internally, alongside `--deck-source-color` and
1484
+ `--deck-source-gap`, §6).
1485
+
1486
+ Five are shared and change every component here at once: `--rik-extras-mass`
1487
+ and `--rik-extras-mass-text` (the one filled area), `--rik-extras-statement` and
1488
+ `--rik-extras-reading` (the two sizes), and `--rik-extras-edge` (the load-bearing
1489
+ line).
1490
+
1491
+ ### The look these share
1492
+
1493
+ One direction, four rules, repeated by every component here. They come from the
1494
+ medium rather than from a catalogue: a slide is read from three to ten metres in
1495
+ about forty seconds while someone talks over it, and at that distance area, size
1496
+ and position survive while hairlines, small caps, thin strokes and pale tints do
1497
+ not.
1498
+
1499
+ 1. **One mass at most.** A single filled area per component, carrying whatever
1500
+ the markup marks. Nothing marked means nothing filled.
1501
+ 2. **Two sizes.** A statement size and a reading size, with nothing in between.
1502
+ The absent third step is why no component here has a micro-label.
1503
+ 3. **No label above content.** Context is written after the thing, at reading
1504
+ size, in sentence case. No all-caps tag, no tracked-out eyebrow.
1505
+ 4. **A line only where two things would otherwise touch.** A table header, a
1506
+ timeline axis, a graph edge. Never as decoration.
1507
+
1508
+ Numbers appear in exactly two components, `deck-flow` and `deck-agenda`, because
1509
+ those two genuinely are sequences. Numbering anything else labels an order the
1510
+ content does not have.
1511
+
1512
+ `reveal` on `deck-flow`, `deck-timeline` and `deck-graph` **emphasises**, it
1513
+ does not hide: at step 0 the whole chain, span or diagram is visible and
1514
+ neutral, because its shape is half the message. `deck-annotate` is the
1515
+ exception and shows nothing at step 0 · there the screenshot must speak first.
1516
+
1517
+ A block is `boxed`. The theme's raised surface measures 1.10 against the page,
1518
+ which is invisible on a projector, so a node that must READ as a block uses the
1519
+ inverse surface: dark on light is the one high-contrast ground this palette has.
1520
+ `tone` fills it with an accent or a status colour instead.
1521
+
1522
+ ```html
1523
+ <deck-graph>
1524
+ <deck-lane at="4,40" label="edge"></deck-lane>
1525
+ <deck-group at="46,50,50,44" label="vpc · eu-west-3"></deck-group>
1526
+ <deck-node id="cdn" at="32,22" boxed icon="cloud" label="CDN" note="edge cache"></deck-node>
1527
+ <deck-node id="api" at="58,68" boxed tone="accent" icon="code" label="API"></deck-node>
1528
+ <deck-edge from="cdn" to="api" label="origin"></deck-edge>
1529
+ </deck-graph>
1530
+ ```
1531
+
1532
+ ```html
1533
+ <deck-graph layout="row">
1534
+ <deck-node id="a" label="Collect" note="raw"></deck-node>
1535
+ <deck-node id="b" label="Decide" note="verdict"></deck-node>
1536
+ <deck-edge from="a" to="b" label="rules"></deck-edge>
1537
+ </deck-graph>
1538
+
1539
+ <deck-graph>
1540
+ <deck-node id="client" at="8,50" label="Client"></deck-node>
1541
+ <deck-node id="api" at="64,25" label="API" note="node 24"></deck-node>
1542
+ <deck-edge from="client" to="api" label="https"></deck-edge>
1543
+ </deck-graph>
1544
+ ```
1545
+
1546
+ `deck-graph` places nodes where the author puts them and never runs a layout
1547
+ solver: an automatic layout moves every node when you add one, which breaks
1548
+ "source = output" and makes the file unreadable a year later. `at="x,y"` in
1549
+ percent is the whole layout language, plus `row` and `column` for the two cases
1550
+ that would otherwise be typed out every time. `layout="row"` and
1551
+ `layout="column"` space nodes evenly along one axis and **ignore `at`
1552
+ entirely** · there is no automatic placement beyond these two canned
1553
+ arrangements, and no `serpentine`, `fan-in` or `fan-out` layout, for the same
1554
+ reason: placement is the diagram, not a computed side effect. Hand-placed
1555
+ nodes that end up on top of each other are caught by `rikiki check`, which
1556
+ reports `GRAPH_NODE_OVERLAPS_NODE` (§12b) · that check is the net for hand
1557
+ placement, not a layout solver.
1558
+
1559
+ ```html
1560
+ <deck-annotate
1561
+ src="dashboard.png"
1562
+ alt="The ops dashboard"
1563
+ marks="20,30,Queue depth|55,60,Latency spike|82,25,Retries"
1564
+ ></deck-annotate>
1565
+
1566
+ <deck-agenda></deck-agenda>
1567
+ ```
1568
+
1569
+ `deck-annotate` publishes one step per marker onto its slide, so the reveal
1570
+ works with the arrow keys like any other step · no plugin, no configuration. On
1571
+ paper every marker prints. `deck-agenda` marks the chapter in progress with
1572
+ `aria-current` and each line is a real button, so a reader can tab to a chapter
1573
+ and jump.
1574
+
1575
+ ```html
1576
+ <deck-bar value="160" total="538" label="Worth a second look"></deck-bar>
1577
+
1578
+ <deck-bar
1579
+ label="Four thousand findings"
1580
+ segments="blocker:137:danger|major:921:warn|minor:1544:info|info:812:ok|noise:586:muted"
1581
+ ></deck-bar>
1582
+
1583
+ <deck-quote author="Marie Dupont" author-role="CTO, Acme">
1584
+ We stopped arguing about the diff and started arguing about the design.
1585
+ </deck-quote>
1586
+ ```
1587
+
1588
+ A stacked bar's printed percentages always add to exactly 100 · the rounding
1589
+ drift is absorbed by the largest slice, where it is least visible. A bar given
1590
+ an explicit `total` keeps the honest figure instead: `160 / 538` reads 30% and
1591
+ leaves the rest of the track empty, which is the whole point of drawing it.
1592
+
1593
+ ---
1594
+
1595
+ ## 21 · Comparison, chains, tables and density
1596
+
1597
+ Four knobs added to components that already existed, chosen over four new
1598
+ elements that would have duplicated a vocabulary the project already has.
1599
+
1600
+ **A directed comparison** · `deck-split` takes `pivot` (a symbol or word between
1601
+ the two columns) and `winner` (`left` or `right`, which rings that side in the
1602
+ accent colour). A neutral split stays neutral: both are absent by default.
1603
+
1604
+ ```html
1605
+ <deck-split pivot="→" winner="right">
1606
+ <h1 slot="title">Before and after</h1>
1607
+ <deck-card slot="left"><h3>Before</h3><p>Four hours of manual review.</p></deck-card>
1608
+ <deck-card slot="right"><h3>After</h3><p>Twelve minutes, same coverage.</p></deck-card>
1609
+ </deck-split>
1610
+ ```
1611
+
1612
+ **A chain across the width** · `deck-step-list direction="row"` lays the steps
1613
+ side by side with a connector between them. `no-connectors` drops the rules.
1614
+
1615
+ **A table with a point of view** · `deck-csv` takes `highlight-rows` and
1616
+ `highlight-cols` (1-based, space-separated) and `reveal`, which shows one body
1617
+ row per step. Hidden rows keep their space, so the slide never jumps under the
1618
+ audience. A revealing table publishes the step count it needs onto its slide.
1619
+
1620
+ The marked row is a dark band and the marked column takes colour and weight ·
1621
+ one fill per table, never two overlapping tints. Both `deck-csv` and
1622
+ `deck-table` follow the same rule, and both are measured: a fill that a room
1623
+ cannot tell from the page fails the theme-contrast test. Knobs:
1624
+ `--deck-csv-mark-bg`, `--deck-csv-mark-color`, `--deck-csv-mark-col-color`,
1625
+ `--deck-csv-mark-col-on-mark`, `--deck-csv-header-bg`, `--deck-csv-header-rule`.
1626
+
1627
+ **A row of figures** · `deck-stat compact` drops the scale so three or four sit
1628
+ together in a `deck-grid` and read as one family, instead of each claiming the
1629
+ slide.
1630
+
1631
+ ### Default density
1632
+
1633
+ `spread` and `fill` (§19) are per-slide, because density is a per-slide
1634
+ judgement. When a whole deck wants a different default, set it once in the theme
1635
+ rather than on every slide:
1636
+
1637
+ ```css
1638
+ :root { --rik-slide-spread: space-between; }
1639
+ ```
1640
+
1641
+ A per-slide `spread` still wins, and `spread="theme"` says "use the default"
1642
+ explicitly.
1643
+
1644
+ ---
1645
+
1646
+ ## 22 · Recipes for things that are NOT components
1647
+
1648
+ Four requests deliberately answered by composition. Each is a layout problem,
1649
+ not a missing element, and a component would freeze one arrangement.
1650
+
1651
+ **A checklist of what works and what does not**
1652
+
1653
+ ```html
1654
+ <deck-grid cols="2" gap="5">
1655
+ <deck-card color="green">
1656
+ <h3><deck-badge type="ok">Yes</deck-badge> Reviewed in CI</h3>
1657
+ <p>Every merge request, no exception.</p>
1658
+ </deck-card>
1659
+ <deck-card color="red">
1660
+ <h3><deck-badge type="bad">No</deck-badge> Reviewed on the branch</h3>
1661
+ <p>Only when someone remembers.</p>
1662
+ </deck-card>
1663
+ </deck-grid>
1664
+ ```
1665
+
1666
+ **Several figures side by side**
1667
+
1668
+ ```html
1669
+ <deck-grid cols="3" gap="5">
1670
+ <deck-stat compact num="34">components</deck-stat>
1671
+ <deck-stat compact num="23 KB">gzip</deck-stat>
1672
+ <deck-stat compact num="3">engines</deck-stat>
1673
+ </deck-grid>
1674
+ ```
1675
+
1676
+ **A pull quote inside a dense slide** · a `deck-punch` in one column of a split,
1677
+ or a `deck-quote` (§20) when the words belong to someone else.
1678
+
1679
+ ```html
1680
+ <deck-split cols="2-1">
1681
+ <deck-md slot="left">The long explanation…</deck-md>
1682
+ <deck-punch slot="right" tone="accent" fit>The one line that matters.</deck-punch>
1683
+ </deck-split>
1684
+ ```
1685
+
1686
+ **A single record, field by field** · one entity's fields read top to bottom,
1687
+ each with the value and what backs it, rather than pasted sideways as a
1688
+ rotated CSV. `deck-table` (§20) already carries this: `highlight-rows` marks
1689
+ the field that carries the argument, `reveal` discloses one field per step,
1690
+ and a `deck-source` underneath credits the record. No header-row emphasis is
1691
+ needed · the field column sits at reading weight, the value column carries
1692
+ the statement, and the marked row is the one dark band the table already
1693
+ draws. Reach for this composition instead of a `deck-record` component
1694
+ because a record is a table with one row per field, and `deck-table` already
1695
+ has the emphasis and reveal a record needs; a dedicated component would only
1696
+ duplicate `highlight-rows` and `reveal` under a new name.
1697
+
1698
+ ```html
1699
+ <deck-table highlight-rows="3" reveal>
1700
+ <table>
1701
+ <thead><tr><th>Field</th><th>Value</th><th>What makes it authoritative</th></tr></thead>
1702
+ <tbody>
1703
+ <tr><td>Incident</td><td>INC-4471</td><td>Assigned by the tracker on file</td></tr>
1704
+ <tr><td>Detected</td><td>2026-09-03 02:14 UTC</td><td>Pager timestamp, not a recollection</td></tr>
1705
+ <tr><td>Root cause</td><td>Connection pool exhaustion</td><td>Confirmed by the on-call engineer, not guessed</td></tr>
1706
+ <tr><td>Owner</td><td>Platform team</td><td>Assignment recorded in the same tracker</td></tr>
1707
+ </tbody>
1708
+ </table>
1709
+ </deck-table>
1710
+ <deck-source href="https://example.com/tracker/INC-4471">Incident tracker, INC-4471</deck-source>
1711
+ ```
1712
+
1713
+ ---
1714
+
1715
+ ## 23 · The slide budget
1716
+
1717
+ Two ways a slide fails a room, and what the suite does about each.
1718
+
1719
+ **Too full is a defect.** The engine never lets content overflow: every slide
1720
+ shell and every cell carries `overflow: hidden`, so an over-filled slide
1721
+ silently loses its last lines and nobody in the room knows.
1722
+ The slide-budget suite walks every shipped deck slide by slide and fails
1723
+ when a clipping box loses more than a few pixels of content.
1724
+
1725
+ **Too empty is a judgement.** A section title is meant to be sparse. The same
1726
+ spec measures how much of the canvas each slide uses and attaches the report to
1727
+ the run, without failing · the numbers are advice, and §19 is the answer.
1728
+
1729
+ ## Presentation previews (0.7.2)
1730
+
1731
+ Overview prepares thumbnails incrementally while idle and keeps its grid for reopening. Content or theme changes invalidate the cache. Presenter previews retain their documents and mirror the current slide step, including annotations; the footer reports the current step. Cover layouts display slide numbers unless `no-counter` is set. Custom components can implement `applyStep(step)` to synchronize their reveal state.