@marver-design/marver 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +41 -19
  3. package/dist/{build-DmfxJc6L.mjs → build-ByYafIhj.mjs} +37 -5
  4. package/dist/cli.mjs +21 -7
  5. package/dist/{collab-BmaOjwP1.mjs → collab-mAiNZlDj.mjs} +232 -43
  6. package/dist/{comments-pNxjTrGN.mjs → comments-DHB_8BRa.mjs} +3 -3
  7. package/dist/{daemon-WjonKlPz.mjs → daemon-Bfucyf1o.mjs} +3 -3
  8. package/dist/{dev-BXq7yE1p.mjs → dev-yZMyQeUj.mjs} +5 -5
  9. package/dist/{events-C9k-ceOj.mjs → events-B3LBn74P.mjs} +3 -1
  10. package/dist/{init-BpitOqRQ.mjs → init-Dvaso7YO.mjs} +66 -2
  11. package/dist/{manifest-CS6krOTe.mjs → manifest-DvOmglFp.mjs} +7 -0
  12. package/dist/{marver-id-gate-DOjnZakC.mjs → marver-id-gate-D6By7XHj.mjs} +8 -5
  13. package/dist/{plugin-Boy7I_JH.mjs → plugin-BsmG5i2X.mjs} +25 -7
  14. package/dist/{profile-ChpTPd-X.mjs → profile-BjAPAJSb.mjs} +1 -1
  15. package/dist/{serve-BeLLizNg.mjs → serve-Bcwfpvhl.mjs} +4 -3
  16. package/dist/{share-C64b1tdN.mjs → share-Gqo_Ygqw.mjs} +14 -15
  17. package/dist/{shot-Cyv3GN79.mjs → shot-kbR_xzJH.mjs} +7 -1
  18. package/dist/{summary-Bf9tiGrE.mjs → summary-C7CAypbB.mjs} +1 -1
  19. package/dist/{sync-WYxDN9IT.mjs → sync-BZaCWqK-.mjs} +1 -1
  20. package/docs/live-jam.md +173 -0
  21. package/docs/publish.md +270 -0
  22. package/docs/sharing.md +333 -0
  23. package/docs/slides.md +134 -0
  24. package/package.json +3 -1
  25. package/src/client/const.ts +13 -0
  26. package/src/client/content/chart-engine.ts +33 -0
  27. package/src/client/content/chart.tsx +100 -0
  28. package/src/client/content/index.tsx +25 -4
  29. package/src/client/content/slide.tsx +238 -0
  30. package/src/client/content/video.tsx +126 -0
  31. package/src/client/shell/App.tsx +49 -15
  32. package/src/client/shell/Comments.tsx +94 -17
  33. package/src/client/shell/LockedApp.tsx +7 -2
  34. package/src/client/shell/Play.tsx +138 -24
  35. package/src/client/shell/Toolbar.tsx +12 -3
  36. package/src/client/shell/canvas/FrameNode.tsx +5 -3
  37. package/src/client/shell/comments-store.ts +59 -4
  38. package/src/client/shell/hash.ts +3 -1
  39. package/src/client/shell/icons.tsx +2 -0
  40. package/src/client/shell/mentions.ts +108 -1
  41. package/src/client/shell/play-order.ts +22 -0
  42. package/src/client/shell/sound.ts +32 -0
  43. package/src/client/shell/store.ts +41 -12
  44. package/src/client/shell/styles.css +58 -27
  45. package/src/client/stage/main.tsx +54 -3
  46. package/src/shared/events.ts +12 -3
  47. package/src/shared/utm.ts +3 -2
  48. package/templates/AGENTS-embedded.md +1 -0
  49. package/templates/AGENTS-studio.md +1 -0
  50. package/templates/instructions/publish.md +7 -0
  51. package/templates/instructions/reference/deck-layouts.md +230 -0
  52. package/templates/instructions/reference/deck-story.md +110 -0
  53. package/templates/instructions/slides.md +398 -0
@@ -21,6 +21,14 @@ const params = new URLSearchParams(location.search)
21
21
  // token systems, .dark for class-keyed ones (Tailwind/shadcn). Missing the class made
22
22
  // play render class-keyed apps light while the canvas showed them dark.
23
23
  const bootTheme = params.get('theme') ?? 'light'
24
+ // slides mode (v1.5): the shell says so in the URL; the stage stamps the ONE
25
+ // attribute the content primitives observe (data-sl-play lifts the rest-state
26
+ // motion reset; data-sl-entered arms the entrance presets after each swap
27
+ // settles). `tr=none` and prefers-reduced-motion both skip view transitions.
28
+ const slidesMode = params.get('slides') === '1'
29
+ const deckTransition = params.get('tr') ?? 'fade'
30
+ if (slidesMode) document.documentElement.setAttribute('data-sl-play', '')
31
+ const reducedMotion = typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches
24
32
  document.documentElement.dataset.theme = bootTheme
25
33
  document.documentElement.classList.toggle('dark', bootTheme === 'dark')
26
34
  const startId = params.get('at') ?? ''
@@ -95,9 +103,41 @@ function Stage() {
95
103
  // inspect.getId() never reports the new frame while the old DOM is still live -
96
104
  // a pick / anchor-resolve landing mid-swap then carries the old id and the shell
97
105
  // guards drop it instead of stamping it onto the wrong frame.
98
- const apply = () => { if (seq === swapSeq.current) { flushSync(() => { setErr(null); setMounted(next) }); current.current = id } }
99
- if (document.startViewTransition) document.startViewTransition(apply)
100
- else { apply(); document.getElementById('root')?.animate([{ opacity: 0.35 }, { opacity: 1 }], { duration: 180, easing: 'ease-out' }) }
106
+ // slides: the entrance presets re-arm per swap. The disarm (drop `entered`,
107
+ // strip data-animate from morph-owned elements) runs INSIDE the transition's
108
+ // update callback - after the new DOM commits, before the NEW-state capture.
109
+ // Disarming before startViewTransition would capture the OUTGOING slide with
110
+ // its [data-animate] elements at opacity 0 (they'd vanish from the old
111
+ // snapshot), and a morphed element still carrying data-animate would be
112
+ // captured transparent and pop in after the morph.
113
+ const disarm = () => {
114
+ if (!slidesMode) return
115
+ document.documentElement.removeAttribute('data-sl-entered')
116
+ // one transform owner: an element that morphs must not also run an
117
+ // entrance preset - CSS cannot see a computed view-transition-name,
118
+ // so the stage strips data-animate from morph-owned elements here
119
+ for (const el of document.querySelectorAll('[data-animate]'))
120
+ if (getComputedStyle(el).viewTransitionName !== 'none') el.removeAttribute('data-animate')
121
+ }
122
+ const apply = () => { if (seq === swapSeq.current) { flushSync(() => { setErr(null); setMounted(next) }); current.current = id; disarm() } }
123
+ const entered = () => {
124
+ if (!slidesMode || seq !== swapSeq.current) return
125
+ document.documentElement.setAttribute('data-sl-entered', '')
126
+ }
127
+ const skipVt = reducedMotion || (slidesMode && deckTransition === 'none')
128
+ if (document.startViewTransition && !skipVt) {
129
+ const vt = document.startViewTransition(apply)
130
+ vt.finished.then(entered, entered)
131
+ } else if (skipVt) { apply(); entered() }
132
+ else {
133
+ // no view transitions here: a plain crossfade at the deck's tempo, so
134
+ // the one-tempo contract holds even where morphs cannot
135
+ apply(); entered()
136
+ const raw = getComputedStyle(document.documentElement).getPropertyValue('--marver-slide-tempo').trim()
137
+ const m = /^(\d*\.?\d+)\s*(ms|s)?$/i.exec(raw)
138
+ const tempo = m ? parseFloat(m[1]) * (m[2]?.toLowerCase() === 's' ? 1000 : 1) : 350 // a CSS <time>: 350ms or .35s; 0 is honoured
139
+ if (tempo > 0) document.getElementById('root')?.animate([{ opacity: 0.35 }, { opacity: 1 }], { duration: tempo, easing: 'ease-out' })
140
+ }
101
141
  if (announce) post({ type: 'sh:stage-at', at: id })
102
142
  } catch (e) {
103
143
  if (seq !== swapSeq.current) return
@@ -129,6 +169,12 @@ function Stage() {
129
169
  // laser/comment click owns the press instead - it must not navigate.
130
170
  const onClick = (e: MouseEvent) => {
131
171
  if (inspect.modeActive()) return
172
+ // slides: a background click advances (posted as the Space key) - never
173
+ // on anything interactive, and data-goto (below) always wins
174
+ if (slidesMode && e.target instanceof Element
175
+ && !e.target.closest('a, button, input, textarea, select, video, .mv-video, [contenteditable], [data-goto]')) {
176
+ post({ type: 'sh:stage-key', key: ' ', code: 'Space' })
177
+ }
132
178
  const el = e.target instanceof Element ? e.target.closest('[data-goto]') : null
133
179
  if (!el) return
134
180
  e.preventDefault()
@@ -149,6 +195,11 @@ function Stage() {
149
195
  // l/c toggle laser/comment, C (shift+c) hides pins; the shell acts and broadcasts back.
150
196
  if (/^Digit[0-9]$/.test(e.code) || ['d', 'h', 'r', 'l', 'c', 'C', '[', ']', 'ArrowRight', 'ArrowLeft'].includes(e.key))
151
197
  post({ type: 'sh:stage-key', key: e.key, code: e.code })
198
+ // slides: Space advances - but never stolen from anything interactive
199
+ if (slidesMode && e.key === ' ' && !(e.target instanceof Element && e.target.closest('button, input, textarea, select, video, .mv-video, a, [contenteditable]'))) {
200
+ e.preventDefault()
201
+ post({ type: 'sh:stage-key', key: ' ', code: e.code })
202
+ }
152
203
  }
153
204
  window.addEventListener('keydown', onKey)
154
205
 
@@ -10,6 +10,13 @@ export type EventType = 'create' | 'reply' | 'edit' | 'resolve' | 'reopen' | 're
10
10
  // Live Jam: provenance stamped by the daemon on agent-authored events (who orchestrated the change).
11
11
  export interface AgentMeta { devUser?: string; harness?: string; model?: string; effort?: string }
12
12
 
13
+ /** A person named in a comment (sharing v1.1). Splits exactly like `author`:
14
+ * canonical logs carry `email`, browser transports carry `id` (the same
15
+ * opaque per-canvas HMAC) - exactly one of the two is present. `label` is the
16
+ * display name the composer inserted (bodies stay plain text; rendering
17
+ * highlights `@label` runs by matching it). */
18
+ export interface Mention { email?: string; id?: string; label: string }
19
+
13
20
  export interface CommentEvent {
14
21
  id: string // client-generated UUID - the idempotency key
15
22
  ts: number // ms epoch at creation
@@ -25,6 +32,7 @@ export interface CommentEvent {
25
32
  * travels to another viewer's browser. Exactly one of the two is present. */
26
33
  author?: { email?: string; id?: string; name?: string; avatar?: string }
27
34
  body?: string // plain text in v1
35
+ mentions?: Mention[] // create/reply only - people named in the body (v1.1)
28
36
  emoji?: string // react events
29
37
  addressedIn?: string // resolve events: the variant frame that answered
30
38
  // --- Live Jam additions ---
@@ -44,7 +52,8 @@ export interface Thread {
44
52
  addressedIn?: string
45
53
  agent?: boolean // root authored by the agent
46
54
  agentMeta?: AgentMeta
47
- replies: { id: string; author?: CommentEvent['author']; body?: string; ts: number; agent?: boolean; agentMeta?: AgentMeta }[]
55
+ mentions?: Mention[]
56
+ replies: { id: string; author?: CommentEvent['author']; body?: string; ts: number; agent?: boolean; agentMeta?: AgentMeta; mentions?: Mention[] }[]
48
57
  reactions: Record<string, string[]> // emoji -> author emails (toggle semantics)
49
58
  }
50
59
 
@@ -67,7 +76,7 @@ export function replay(events: CommentEvent[]): Thread[] {
67
76
  threads.set(ev.commentId, {
68
77
  id: ev.commentId, board: ev.board, nodeKey: ev.nodeKey, frame: ev.frame,
69
78
  anchor: ev.anchor, author: ev.author, body: ev.body, ts: ev.ts,
70
- resolved: false, agent: ev.agent, agentMeta: ev.agentMeta, replies: [], reactions: {},
79
+ resolved: false, agent: ev.agent, agentMeta: ev.agentMeta, mentions: ev.mentions, replies: [], reactions: {},
71
80
  })
72
81
  }
73
82
  for (const ev of ordered) {
@@ -75,7 +84,7 @@ export function replay(events: CommentEvent[]): Thread[] {
75
84
  case 'reply': {
76
85
  const t = ev.parentId ? threads.get(ev.parentId) : undefined
77
86
  if (!t || !ev.commentId || t.replies.some((r) => r.id === ev.commentId)) break
78
- t.replies.push({ id: ev.commentId, author: ev.author, body: ev.body, ts: ev.ts, agent: ev.agent, agentMeta: ev.agentMeta })
87
+ t.replies.push({ id: ev.commentId, author: ev.author, body: ev.body, ts: ev.ts, agent: ev.agent, agentMeta: ev.agentMeta, mentions: ev.mentions })
79
88
  break
80
89
  }
81
90
  case 'edit': {
package/src/shared/utm.ts CHANGED
@@ -4,12 +4,13 @@
4
4
  * utm_source = the surface class ('published-canvas' | 'dev-canvas')
5
5
  * utm_medium = the link unit ('powered-by')
6
6
  * utm_campaign = THIS canvas's name, slugged ("Marver tour" -> "marver-tour")
7
- * utm_content = the placement ('gate' badge | 'shell' wordmark | 'sign-in' finish page)
7
+ * utm_content = the placement ('gate' badge | 'shell' wordmark | 'sign-in' finish page |
8
+ * 'play-brand' the present/slides brand pill)
8
9
  */
9
10
  export function poweredByUrl(
10
11
  canvasName: string | undefined,
11
12
  source: 'published-canvas' | 'dev-canvas',
12
- content: 'gate' | 'shell' | 'sign-in',
13
+ content: 'gate' | 'shell' | 'sign-in' | 'play-brand',
13
14
  ): string {
14
15
  const slug = (canvasName ?? '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
15
16
  const q = new URLSearchParams({
@@ -22,6 +22,7 @@ file in design/instructions/ - they are short, strict, and part of this contract
22
22
  | Iterate | changing a frame the human has seen, or retiring explorations | instructions/iterate.md |
23
23
  | Review | before presenting anything | instructions/review.md |
24
24
  | Boards | creating a board, choosing what ships | instructions/boards.md |
25
+ | Slides | a deck is asked for, or `slide: true` frames exist | instructions/slides.md + design/slides.md |
25
26
  | Publish | deploying the canvas: gate, volume, accounts, invites | instructions/publish.md |
26
27
  | Live Jam | responding to an `@marver` comment (a spawned job); on by default - confirm it names YOUR tool | instructions/jam.md |
27
28
 
@@ -22,6 +22,7 @@ file in design/instructions/ - they are short, strict, and part of this contract
22
22
  | Iterate | changing a frame the human has seen, or retiring explorations | instructions/iterate.md |
23
23
  | Review | before presenting anything | instructions/review.md |
24
24
  | Boards | creating a board, choosing what ships | instructions/boards.md |
25
+ | Slides | a deck is asked for, or `slide: true` frames exist | instructions/slides.md + design/slides.md |
25
26
  | Publish | deploying the canvas: gate, volume, accounts, invites | instructions/publish.md |
26
27
  | Live Jam | responding to an `@marver` comment (a spawned job); on by default - confirm it names YOUR tool | instructions/jam.md |
27
28
 
@@ -51,6 +51,13 @@ vars if the host has no CLI.
51
51
  container disk is the one real deploy mistake - comments would vanish on
52
52
  redeploy; marver fails loudly if the dir cannot be created.
53
53
 
54
+ Then one line before you build: **name the canvas** -
55
+ `share: { name: "Your App" }` in `design/config.ts`. The name is the gate's
56
+ title, the brand pill's tooltip, and the `utm_campaign` on every powered-by
57
+ link the canvas emits (the wordmark, the gate, the play brand pill). With no
58
+ name it falls back to the ROOT DIRECTORY, which in a container is `app` -
59
+ every containerised canvas you never named reports as the same campaign.
60
+
54
61
  ## The serve contract (env vars, complete list)
55
62
 
56
63
  | Var | Meaning |
@@ -0,0 +1,230 @@
1
+ # Deck layouts - the atlas, the grid, the budgets
2
+
3
+ Required reading at step 4 of every deck. The doctrine's recipe list is the core;
4
+ this is the full atlas grouped by the JOB a slide does, the shared stage and the
5
+ optional banded grid recipes draw from, and the content budgets that keep type at design size. The atlas
6
+ is a vocabulary, not a fence: when nothing fits the concept you wrote for the
7
+ slide, compose it from divs.
8
+
9
+ Words used below: **kicker** = the small-caps `sl-caption` label above a title;
10
+ **hero** = a card's one-line headline in `sl-support`; **ghost numeral** = a large,
11
+ low-contrast number behind or beside a card that carries order; **display** =
12
+ `sl-display`, the one oversize role; **stat** = `sl-stat`, the row-of-figures size.
13
+
14
+ ## The stage and the banded grid (content box 1104×632px)
15
+
16
+ Stage margins are asymmetric: 88px at the sides, 44px top and bottom, in px
17
+ at every viewport. Every silhouette shares those margins and nothing else.
18
+ The BANDED grid below is the shell for grid and split recipes that carry a
19
+ title band; statements, heroes, fields, and bookends build their own
20
+ geometry inside the same margins. (The doctrine's 85% rule and spacing scale
21
+ govern every silhouette.)
22
+
23
+ - **Title band** - the top ~113px: kicker (18px, one line) over the assertion
24
+ (56px, one line), a hairline under. Within a visual group it sits in the
25
+ same place on each slide, so a travelling title lands where it left.
26
+ - **Body band** - full width, ~438px, starting 48px below the title block.
27
+ Content fills at most ~372px of it. This band is the slide.
28
+ - **Foot** - the source line (25px), 48px under the body. A takeaway bar sits
29
+ between them: 56px tall, 40px clear above it.
30
+ - **Split** - text left at 43% of the width, visual right at 48%, a 9% gutter.
31
+ Argument reads first, proof confirms it. A `sl-assertion` in a 43% column
32
+ holds ~20 characters a line - write to it.
33
+ - **Columns** - three equal at a 32px gap (~347px each); five narrow (~192px)
34
+ for spectrums; a 2×2 when the four cells are peers. Card padding 32px.
35
+ - **Dominance before containers.** Decide what is biggest on the slide before
36
+ choosing what holds the rest. Whitespace that makes the dominant object read
37
+ as dominant needs no defence; a companion panel added to "fill the other
38
+ half" does - if it is not evidence, leave the half empty.
39
+
40
+ The banded skeleton, in the project's own Tailwind - for the grid and split
41
+ slides that carry a title band, and only those:
42
+
43
+ ```tsx
44
+ <Slide>
45
+ {/* one child at flex:1 claims the box, so the bands land in the same
46
+ place across a visual group; gap 48 is the title-to-body / body-to-foot step */}
47
+ <div className="flex-1 min-h-0 flex flex-col gap-12">
48
+ <header className="shrink-0">
49
+ <p className="sl-caption uppercase tracking-[.14em]">Retention</p>
50
+ <h1 className="sl-assertion mt-2">Churn halved after onboarding v2</h1>
51
+ <hr className="mt-5 border-0 h-px bg-black/10" />
52
+ </header>
53
+ <div className="flex-1 min-h-0 grid grid-cols-[43fr_9fr_48fr] items-center gap-8">
54
+ <div className="sl-body space-y-6">…argument…</div>
55
+ <div />
56
+ <Chart option={…} />
57
+ </div>
58
+ <footer className="sl-caption shrink-0">Source: product analytics, Aug 2026</footer>
59
+ </div>
60
+ </Slide>
61
+ ```
62
+
63
+ ## The atlas (when · skeleton · budget · anchor)
64
+
65
+ A recipe without a named anchor does not morph - hard cut. Anchors are named
66
+ only inside a declared visual group or build; never a deck-wide fallback.
67
+ Budgets are heuristics: the rendered 1280×720 frame and the Slide's overflow
68
+ outline are the authority - when they disagree with a number here, the frame
69
+ wins. And a slide that merely FITS is not done: the 85% rule is the bar.
70
+
71
+ **Parallel points**
72
+ - **cards** - 2-6 equal cards (kicker · hero · body), ghost numerals for order,
73
+ at most ONE accented card for the emphasised option. Budget: 2-3 cards carry a
74
+ body (≤120 chars, 3 lines); 4-6 cards are kicker + hero only. Anchor: the row.
75
+ - **spectrum** - 3-7 narrow cards for a progression or maturity model, low to
76
+ high left to right. Budget: kicker ≤8 chars, hero ≤15, body ≤40 (≤5 cards) or
77
+ none (6-7).
78
+ - **columns** - N headers + descriptions + an optional metric strip beneath:
79
+ product lines, tracks, team areas. Budget: ≤4 columns with bodies, ≤6 without.
80
+ - **stacked list** - 4-6 items in the right field, argument on the left; the
81
+ numbered variant carries ghost numerals for ordered reasons. Budget: item ≤2
82
+ lines at 24px. Anchor: the list.
83
+ - **split** - argument left (1-3 short paragraphs, optional `sl-support`
84
+ sub-head), ONE visual right (metric, card, image, chart). The workhorse of
85
+ analytical slides. Anchor: the visual.
86
+ - **insight + evidence** - one large insight left (≤30 words, `sl-support`),
87
+ 3-4 evidence items right (one-line title + one line). Anchor: the insight.
88
+
89
+ **Proof**
90
+ - **metric** - one hero number (`sl-display`) with label + sub-line, enclosed
91
+ only when the boundary means something;
92
+ pair with a split or a stat row. Budget: value ≤8 chars, label ≤20, sub ≤30.
93
+ - **stat row** - 3-4 figures across in `sl-stat` with a label under each.
94
+ Budget: value ≤7 chars at 3 across, ≤5 at 4; label ≤4 words. Anchor: the row.
95
+ - **trajectory** - stacked from → to pairs with a label ("$100k → $480k MRR").
96
+ Budget: 3-4 pairs. Anchor: the arrows.
97
+ - **table** - header row with a rule under it, zebra rows, numbers right-aligned,
98
+ units in the header. Budget at 24px: ≤5 columns × ≤6 rows, header ≤15 chars,
99
+ cell ≤14; more than that is two slides or a chart.
100
+ - **mini grid** - N×M small value + label cells with hairline dividers, for a
101
+ dashboard glance. Budget: ≤12 cells (4×3).
102
+ - **takeaway bar** (modifier) - a full-width dark bar at the foot with the
103
+ so-what, centred, no trailing full stop, ≤12 words. Never a paraphrase of the
104
+ assertion - a different angle or nothing.
105
+
106
+ **Contrast**
107
+ - **before / after** - the doctrine's two-up, the "after" side accented.
108
+ - **scenarios** - bear / base / bull columns over a metric list, the recommended
109
+ column highlighted. Budget: 3 scenarios × ≤5 metrics.
110
+
111
+ **Process** (the subject, never the provenance)
112
+ - **flow** - step cards with forward arrows; ≤5 steps keep bodies, 6+ drop to
113
+ labels only. Budget: label ≤15 chars, body ≤40.
114
+ - **cycle** - 3-6 auto-numbered nodes around a centre; four nodes sit square at
115
+ the corners, others on a circle. Budget: label ≤15, body ≤40. Anchor: the ring.
116
+ - **chain** - primary chevrons for the value chain with support bars beneath (the
117
+ enabling activities). Budget: ≤6 chevrons, ≤3 bars.
118
+ - **swim lanes** - lanes (rows) × stages (columns) with mini-cards at the
119
+ intersections: hand-offs, RACI, cross-functional flow. Budget: ≤4 lanes × ≤5
120
+ columns, card ≤8 words.
121
+ - **funnel** - 3-8 narrowing tiers, the drop-off stated. Budget: label + one
122
+ line ≤5 tiers; labels only at 6-8.
123
+
124
+ **Time**
125
+ - **schedule** - sections × time columns, task bars, milestone diamonds. Budget:
126
+ ≤7 rows, ≤8 columns. Anchor: the time header.
127
+ - **timeline**, **roadmap phases** - the doctrine's; alternate event labels
128
+ above and below the spine when they crowd.
129
+
130
+ **Structure and position**
131
+ - **layers** - full-width stacked layers with tag pills; the foundation layer
132
+ dark. Budget: ≤5 layers, ≤4 tags each.
133
+ - **org** - boxes + connectors, two levels max; deeper goes to an appendix.
134
+ - **venn** - 2-3 circles with 2-3 items each and a named overlap.
135
+ - **concentric** - TAM / SAM / SOM rings, labels inside the rings, legend right.
136
+ - **pyramid** - 3-6 trapezoid tiers, widest at the top, each tier a label + 1-3
137
+ items. For priority, never for volume (that is the funnel).
138
+ - **number line** - ticks with labels, one highlighted range: pricing tiers, a
139
+ valuation range, benchmarks. Budget: ≤6 ticks.
140
+ - **capability matrix** - competitors × capabilities with empty / half / full
141
+ circles (CSS), us in the first column. Budget: ≤6 × ≤7.
142
+
143
+ **Status**
144
+ - **scorecard** - rows with a red / amber / green dot + a one-line note. ≤7 rows.
145
+ - **heat map** - rows × columns of RAG cells, a legend, no numbers inside cells.
146
+ Budget: ≤6 × ≤6.
147
+ - **tracker** - initiative · owner · phase · next milestone; or decision · owner ·
148
+ date · status. Budget: ≤6 rows, owners as initials badges.
149
+
150
+ **People and voice**
151
+ - **testimonials** - 1-6 quote cards with an initials avatar, name, company -
152
+ attributed voices (the doctrine's quote-wall is unattributed fragments, ≤15
153
+ words). Budget: ≤25 words a quote at ≤4 cards, ≤15 at 5-6. Anchor: the avatars.
154
+ - **team** - 1-8 people: initials, name (≤15 chars), role (≤22). Budget: ≤4
155
+ people carry a bio (≤30 words); 5-8 are name + role.
156
+ - **manifesto** - a single large claim in `sl-support` or `sl-display`, one
157
+ accent-coloured phrase, an attribution line. Budget: ≤20 words. Anchor: the
158
+ accent phrase.
159
+
160
+ **Images**
161
+ - **framed source** - a screenshot, a chart from a PDF, a product shot: the real
162
+ image on the right, framing text on the left saying what it shows. The bitmap
163
+ is at least 2× the CSS box it renders in. Never redraw a source chart as a
164
+ fake: rebuild it as a `Chart` when the underlying data is available, embed the
165
+ render when it is not.
166
+
167
+ ## Budgets that keep type at design size
168
+
169
+ | Element | Cap |
170
+ |---|---|
171
+ | card kicker · hero · body | 20 · 30 · 120 chars (bodies only at ≤3 cards) |
172
+ | narrow (spectrum) card | 8 · 15 · 40 chars |
173
+ | metric value · label · sub | 8 · 20 · 30 chars |
174
+ | table header · cell | 15 · 14 chars, ≤5 × ≤6 |
175
+ | flow / cycle label · body | 15 · 40 chars |
176
+ | timeline date · title · body | 8 · 20 · 20 chars |
177
+ | quote | 30 words; testimonial 25 (≤4) / 15 (5-6); quote-wall 15 |
178
+ | bio | 30 words at ≤4 people; name 15 chars, role 22 |
179
+ | takeaway bar | 12 words, one line |
180
+ | body paragraphs | 3-5 short paragraphs, ~600 chars total |
181
+
182
+ A breach is a different recipe or a split. Type never shrinks to fit - the review
183
+ gate reads shrunk type as the tell it is.
184
+
185
+ ## Rebuilding an existing deck
186
+
187
+ When the human hands you a finished deck to rebuild on the canvas, ask which
188
+ mode - and default to faithful:
189
+
190
+ - **Faithful** - their order, their words, exactly. You may normalise
191
+ punctuation (em dashes → commas or periods) and number formats; you may not
192
+ change a word. Label titles stay labels. Suggested rewrites go in a comment on
193
+ the frame, never on the slide.
194
+ - **Editorial** (opt-in) - order kept, copy passed through the doctrine's words
195
+ rules: jargon, hedges, filler out; numbers, names, dates verbatim; titles
196
+ turned into assertions where the source supports the claim.
197
+
198
+ Then map shapes - a companion visual ONLY where the source supplies it; a
199
+ paragraph with no metric, image, or chart behind it is a text-led composition,
200
+ and that whitespace is honest:
201
+
202
+ | Source shape | Layout |
203
+ |---|---|
204
+ | a paragraph | split when the source has a companion (metric, image, chart); else text-led |
205
+ | 3 bullets | cards |
206
+ | 4-6 bullets | stacked list (numbered if ordered) |
207
+ | up to 4 numbers | metric grid or stat row |
208
+ | a quote | quote |
209
+ | a table | table (or two slides past the budget) |
210
+ | a chart with its data | `Chart` from the data |
211
+ | an image, chart, schedule, diagram you cannot rebuild losslessly | framed source |
212
+
213
+ ## Charts and diagrams - the extra mile
214
+
215
+ - **Decision flows**: boil choices to yes / no, quantify the branches (%, volumes)
216
+ so the eye follows the path that matters, hang customer quotes on the node
217
+ they support.
218
+ - **Waterfalls** beat tables for build-ups and breakdowns: left to right in the
219
+ logical order, the one or two bars that matter highlighted, a few callouts that
220
+ pre-empt the room's questions.
221
+ - **When a slide must be complex**: large visual cues (boxes, highlights) on the
222
+ point, grouping and colour that steer interpretation, and the voiceover ON the
223
+ page - the slide must make sense with no presenter.
224
+ - **Aggregate.** The chart is not the model. Single series is fine. Overlay
225
+ detail (lines, shading) on the base chart instead of adding a second chart.
226
+ - **Formatting**: label bars directly and drop the value axis when the chart is
227
+ simple (keep the axis for dense or grouped series); growth rates visible; one
228
+ label size across the deck; series in logical order (base first, growth
229
+ next); same hue = same thing on every slide; charts aligned to the grid and to
230
+ each other across slides.
@@ -0,0 +1,110 @@
1
+ # Deck story - the argument before the slides
2
+
3
+ The slides doctrine (instructions/slides.md) gives the pipeline. This file is the
4
+ depth behind steps 1-3: how to find the answer, shape the argument, calibrate it to
5
+ the room, and write words that carry it. Pull it when the material is thin or
6
+ rich, the audience is senior, or the first slide list reads like a table of
7
+ contents.
8
+
9
+ ## Intake - seven questions, one message
10
+
11
+ Ask them all at once; skip any the human already answered (pasted bullets, a linked
12
+ doc). The first three are required - ask again if skipped, they shape everything.
13
+
14
+ 0. **The one thing.** If they remember nothing else, what? ("Approve the Q3 budget",
15
+ "Retention is the lever, not acquisition".)
16
+ 1. **The goal.** What happens after the last slide? (a decision, alignment, a meeting)
17
+ 2. **The audience.** Executives / cross-functional team / own team / external
18
+ (client, partner, board). Named people help.
19
+ 3. **Raw material.** Bullets, a doc, "start from scratch" - messy is fine.
20
+ 4. **Numbers.** Metrics and proof points. Numbers make slides land.
21
+ 5. **The energy.** Urgent / confident / informational / exploratory.
22
+ 6. **Constraints.** Time, slide cap, topics to avoid, must-have slides, who presents.
23
+
24
+ ## Find the answer first - five questions, in order
25
+
26
+ | Ask | Yields |
27
+ |---|---|
28
+ | What is top of mind for the audience - goal, worry, definition of success? | the objective |
29
+ | Twenty seconds in a lift: what do you tell them? | THE ANSWER |
30
+ | Which logical steps take them from where they are to the answer? | the storyline |
31
+ | Where will they disagree? What moves them to act? | the slides |
32
+ | What do you not know? What is the burden of proof? | the evidence |
33
+
34
+ Top-down, always. Never start from the slides or the analysis. The answer's shape:
35
+ context ("this matters, in these ways") · options ("two paths, here they are") ·
36
+ recommendation ("the first, for these reasons") · next steps ("what it takes, by
37
+ when"). "We don't know yet, but here is how we find out" IS an answer; a hedge is not.
38
+
39
+ **The pyramid.** Slides 1-2 carry the conclusion. Each argument is a slide group.
40
+ Evidence sits under its argument, never free-floating. A deck where the answer
41
+ arrives on slide 14 has been built bottom-up - rebuild it.
42
+
43
+ **Insights first, outputs second, provenance never.** How the model works,
44
+ what alternatives were considered, how long it took, what went wrong - none
45
+ of it belongs in the room. Time spent making a slide earns it nothing. (A
46
+ process that IS the subject - the operating model you propose, the rollout
47
+ plan - is content, and the atlas has layouts for it.)
48
+
49
+ ## Calibrate to the room
50
+
51
+ - **Executives** - so-what framing, 12-15 slides, one ask.
52
+ - **The team** - actionable detail: owners, dates, dependencies.
53
+ - **External (client, partner, investor)** - polished narrative, social proof,
54
+ evidence-heavy, the firm's specific language in the titles.
55
+ - **Board** - trajectory, risks, asks. Concise.
56
+ - **Summary vs full material.** A summary is the conversation about the
57
+ answer: sized to HALF the meeting (a 45-minute slot carries 12-20 slides;
58
+ the rest is discussion). The full deck is the reference ("see slide 53"),
59
+ organised behind the thesis and its drivers, as long as it needs to be.
60
+ Know which you are building; never blend them.
61
+ - **Wayfinding.** An agenda only when there are three or more sections, each
62
+ item a section name + the slide it starts on. Section dividers only past ~25
63
+ slides - below that, section changes live in the assertions themselves.
64
+
65
+ ## The evidence check - before designing anything
66
+
67
+ Walk every planned slide with one question: could I draft this without inventing a
68
+ single fact? Mark it has-enough or not. For the gaps, write SPECIFIC questions (not
69
+ "more on the team" but "which four people, prior firms, role on this project?"),
70
+ at most two or three per slide, only where the answer changes the slide. Then one
71
+ deck-level call:
72
+ - **ask** - the default when any slide lacks material; put the consolidated list
73
+ to the human before building;
74
+ - **shrink to the evidence** - when most slides lack material and the human
75
+ plainly has no more (early stage, the narrative is the product) - propose the
76
+ smaller deck;
77
+ - **proceed with placeholders** - only when the gaps are editorial (framing,
78
+ taglines), never data (numbers, names, dates).
79
+
80
+ Every claim traces to source, and the gap has a stage. In the scaffold (the
81
+ one-sentence slides on the canvas) a missing fact is a visible `[PLACEHOLDER:
82
+ what belongs here]`. In the build, a factual placeholder BLOCKS its slide -
83
+ answered, or the slide is cut - never filled with plausible prose. Editorial
84
+ gaps (a tagline, a caption, a transition line) you DO draft, labelled as
85
+ proposed in the frame's comment - that is writing, not invention.
86
+
87
+ ## Writing that carries
88
+
89
+ - **Distil the voice first.** Scan the material for concepts that recur on three
90
+ or more slides; give each a 2-4 word mnemonic, imperative verb preferred
91
+ ("Decide fast", "See everything"), and use it identically everywhere it applies.
92
+ - **Mine the material for specifics** and keep them verbatim: "19 of 25 operators"
93
+ beats "most operators"; "$100k to $480k MRR" (from X to Y) beats "grew strongly".
94
+ - **Assertions fit their box.** ~40 characters full-width, ~20 inside a split,
95
+ two lines the ceiling - compress the phrasing, keep the claim, never shrink
96
+ the type, and check the render.
97
+ - **Paragraphs over bullets for narrative.** 15-30 words, one to three per block.
98
+ Bullets are for parallel lists and action items; a bullet with "and" is two.
99
+ - **Tone follows the energy.** Urgent: "Double down now or miss the target."
100
+ Confident: "Retention hit 85%, the best quarter yet." Informational: "Retention
101
+ stands at 85%, up 12 points." Exploratory: "Three approaches, different trade-offs."
102
+ - **Voice follows the deck type.** Strategy commits to positions. A pitch is warm
103
+ and aspirational. A case study attributes results to named actions. A status
104
+ update is crisp - state, delta, next.
105
+ - **Kill list, extended.** Beyond the doctrine's: "utilize", "unlock", "harness",
106
+ "empower", "seamless", "delve", "unleash", "synergize", "operationalize",
107
+ "cutting-edge", "best-in-class", "at the end of the day", "in terms of".
108
+ - **The ask closes.** Decision, owner, date: "Approve $200k for Q3 retention by
109
+ Friday." The closing slide is that ask - if the house style ends on a bare
110
+ end card (mark on an image), the ask is the slide before it.