@jokerized/decksmith 0.9.3 → 0.9.4

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 (59) hide show
  1. package/README.md +449 -90
  2. package/dist/animate/LICENSE +21 -0
  3. package/dist/animate/NOTICE +32 -0
  4. package/dist/animate/core.js +530 -0
  5. package/dist/animate/cut-paper.js +206 -0
  6. package/dist/animate/morph.js +131 -0
  7. package/dist/cli.js +13368 -8864
  8. package/dist/ds-animate.js +1 -0
  9. package/dist/index.js +13182 -8678
  10. package/dist/mcp.js +9805 -7587
  11. package/dist/types/bespoke/art.d.ts +172 -6
  12. package/dist/types/bespoke/cache.d.ts +10 -0
  13. package/dist/types/bespoke/cards.d.ts +66 -0
  14. package/dist/types/bespoke/contract.d.ts +0 -8
  15. package/dist/types/bespoke/databuild.d.ts +50 -0
  16. package/dist/types/bespoke/depth.d.ts +127 -0
  17. package/dist/types/bespoke/grammar.d.ts +73 -0
  18. package/dist/types/bespoke/inspect.d.ts +40 -1
  19. package/dist/types/bespoke/pipeline.d.ts +195 -11
  20. package/dist/types/bespoke/probe.d.ts +2 -2
  21. package/dist/types/bespoke/prompt.d.ts +110 -34
  22. package/dist/types/bespoke/references.d.ts +13 -14
  23. package/dist/types/bespoke/repair.d.ts +23 -29
  24. package/dist/types/bespoke/scene.d.ts +55 -10
  25. package/dist/types/bespoke/select.d.ts +28 -21
  26. package/dist/types/bespoke/sheet.d.ts +66 -1
  27. package/dist/types/bespoke/shots.d.ts +225 -22
  28. package/dist/types/build/files.d.ts +7 -0
  29. package/dist/types/build/piece.d.ts +8 -0
  30. package/dist/types/emit/animate-runtime.d.ts +149 -0
  31. package/dist/types/emit/archetypes/claim-figure.d.ts +84 -1
  32. package/dist/types/emit/archetypes/equation-walk.d.ts +2 -2
  33. package/dist/types/emit/archetypes/hero-number.d.ts +21 -0
  34. package/dist/types/emit/archetypes/index.d.ts +2 -2
  35. package/dist/types/emit/archetypes/kinetic.d.ts +2 -0
  36. package/dist/types/emit/archetypes/pipeline.d.ts +14 -2
  37. package/dist/types/emit/archetypes/stage.d.ts +57 -0
  38. package/dist/types/emit/backdrop.d.ts +114 -0
  39. package/dist/types/emit/kit.d.ts +8 -0
  40. package/dist/types/emit/look.d.ts +14 -0
  41. package/dist/types/emit/motion.d.ts +78 -46
  42. package/dist/types/emit/type.d.ts +48 -5
  43. package/dist/types/images/providers.d.ts +6 -0
  44. package/dist/types/plan/codex.d.ts +4 -2
  45. package/dist/types/plan/direct.d.ts +1 -1
  46. package/dist/types/plan/duration.d.ts +1 -1
  47. package/dist/types/plan/prompt.d.ts +1 -1
  48. package/dist/types/plan/refs.d.ts +2 -2
  49. package/dist/types/plan/variety.d.ts +74 -0
  50. package/dist/types/prefs.d.ts +20 -1
  51. package/dist/types/render/captions.d.ts +4 -5
  52. package/dist/types/render/capture.d.ts +7 -0
  53. package/dist/types/types.d.ts +680 -4
  54. package/dist/types/verify/drift.d.ts +5 -0
  55. package/dist/types/verify/fidelity.d.ts +15 -1
  56. package/dist/types/verify/index.d.ts +17 -0
  57. package/dist/types/verify/scenes.d.ts +185 -5
  58. package/package.json +1 -1
  59. package/dist/types/bespoke/callouts.d.ts +0 -76
package/README.md CHANGED
@@ -110,7 +110,9 @@ decksmith unpack talk.deck -o reopened/
110
110
  from the viewer. `--subtitles burn` makes it part of the picture, and it is only
111
111
  accepted over a deck built with `--reserve-captions` — the layout is fixed at build
112
112
  time, so a deck that made no room cannot be given any an hour later, and the band would
113
- land on the slide's own text. `render` refuses that rather than shipping it.
113
+ land on the slide's own text. `render` refuses that rather than shipping it. A burned
114
+ caption is a quiet lower third: 40px white type with a close dark halo, no box
115
+ (2026-10-10; it was 4% of the width — 71px at 1920 — on a 70% black bar).
114
116
  - **drift** — renders the deck twice and compares every frame. `--identical` fails on any
115
117
  differing byte, which is only honest for an image-free deck with no camera — the demo
116
118
  deck is not one, and fails it on both hyperframes pins (see
@@ -281,7 +283,7 @@ without driving scene timelines or clip visibility. See
281
283
  ### What the gates do not check
282
284
 
283
285
  The failure this project keeps producing is a **green gate over wrong output**, and there
284
- are now ten documented cases. Nearly every one was caught by a human looking at the
286
+ are now eleven documented cases. Nearly every one was caught by a human looking at the
285
287
  artifact. Three are worth reading as patterns rather than bugs:
286
288
 
287
289
  - A camera move that the video renderer replaced with a still, for exactly the right
@@ -840,7 +842,7 @@ invalidates a storyboard you have already edited.
840
842
  | `images.provider` | `codex` | illustrate | `auto` · `codex` · `svg` — where the chain of rungs starts |
841
843
  | `images.model` | unset | illustrate | model for the separate backend only; the Codex rung uses the account's own |
842
844
  | `images.style` | `flat vector illustration` | illustrate | one phrase folded into every picture prompt |
843
- | `images.max` | `4` | illustrate | most pictures drawn through the chain; the rest are the tool's SVG |
845
+ | `images.max` | `10` | illustrate | most pictures drawn through the chain; the rest are the tool's SVG |
844
846
 
845
847
  Three layers, in increasing precedence: these defaults, then the nearest
846
848
  `decksmith.config.json` found by walking up from the working directory, then flags.
@@ -905,17 +907,31 @@ byte for byte. See `.planning/2026-10-07-v2-layout-director.md`.
905
907
  eyebrow and headline, every seam the same 0.4s dissolve, nothing moving once a slide has
906
908
  built — with a grammar planned per deck (`src/emit/motion.ts`):
907
909
 
908
- - **Entrances.** Six verbs — `rise`, `slide`, `snap`, `focus`, `wipe`, `mask` — one per
909
- scene, chosen from the source id and beat id, never the same twice in a row, with the
910
- stock `rise` capped at a quarter of the deck.
911
- - **Seams.** `dissolve`, `push`, `lift`, `wipe`, `zoom`, picked from how two neighbouring
910
+ - **Entrances.** One verb, `fade`: a part's opacity reveal at its own time and length,
911
+ with no travel, scale, clip or blur. There were six (`rise`, `slide`, `snap`, `focus`,
912
+ `wipe`, `mask`), and the founder's verdict on plates, chips, cards and labels sliding
913
+ and popping in was "graphic animation by animated UI elements is old-fashioned"
914
+ (2026-10-10). On a fallback archetype the motion is the picture's — the backdrop's
915
+ drift, the field, the camera — and the seams'; no panel or slab is lifted as it is
916
+ read, and split-compare's divider no longer sweeps a highlight.
917
+ - **Seams.** `dissolve`, `push`, `lift`, `zoom`, picked from how two neighbouring
912
918
  beats relate (same family → push, a role boundary → zoom, a title → lift, into the close
913
919
  → dissolve), never repeated back to back, and at least three kinds in a deck of ten or
914
- more beats. A beat `inside` the one before keeps the camera dive.
915
- - **Emphasis while the narrator talks.** The part a sentence is about pulses, glows or is
916
- underlined, starting on a cue boundary of that sentence inside the quiet stretch after
917
- its stop, and is back at rest before the next reveal — so every frame a gate captures
918
- at a stop is unchanged.
920
+ more beats. A beat `inside` the one before keeps the camera dive. Every seam fades the
921
+ outgoing eyebrow and headline out in the first 0.16s and starts the incoming ones at
922
+ 0.3s — a bespoke scene's chrome included — so two headlines never share a frame. The
923
+ `wipe` (a clip sweeping the slide) was cut on 2026-10-10: at 4fps it left half a picture
924
+ and a lone label on an empty slide.
925
+ - **Emphasis while the narrator talks.** The part a sentence is about glows (light only;
926
+ the scale `pulse` and the `underline` went with the entrances), starting on a cue
927
+ boundary of that sentence inside the quiet stretch after its stop, and is back at rest
928
+ before the next reveal — so every frame a gate captures at a stop is unchanged.
929
+
930
+ **Type** (`TYPE_SCALE` in `src/emit/type.ts`, the founder's scale of 2026-10-10): headline
931
+ and title slide 56px, kicker and burned captions 40px, body, labels and notes 40-44px, a
932
+ display equation asked at 64px at most (its scripts then land at 44.8px), nothing under
933
+ 40px. Every v2 pack sets it; archetypes no longer grow their type into the region (their
934
+ bars, boxes and figures still grow). `--design classic` keeps its own sizes.
919
935
 
920
936
  All of it is `fromTo` tweens with no callbacks, seeded rather than random, and keeps every
921
937
  hold, every scene window and `timing.json` exactly as `classic` writes them. In `deck.html`
@@ -924,32 +940,77 @@ already built) and, while a stop's audio plays, seeks the scene through that sto
924
940
  stretch on the audio clock so the emphasis lands on the same word as in the video. Both
925
941
  are off under `prefers-reduced-motion`.
926
942
 
927
- ## v2 bespoke scenes (`--bespoke`)
943
+ ## v2 bespoke scenes (on by default under v2; `--no-bespoke`)
928
944
 
929
- The archetypes finish building in about five seconds and then hold still while the voice
930
- keeps talking. `build --design v2 --bespoke` hands four to six beats per deck to Codex
931
- instead: for each, a GSAP + SVG scene written for that beat, keyed to its narration cues,
932
- that keeps explaining for as long as the sentence runs. Everything else stays v2. It is off
933
- unless asked for; v2 without it, and classic, emit the bytes they did.
945
+ The archetypes are the same handful of layouts on every slide, and they finish building in
946
+ about five seconds and then hold still while the voice keeps talking. A narrated
947
+ `build --design v2` hands EVERY beat to Codex instead — title, numbers, comparisons and
948
+ closing claims as well as mechanisms: for each, a GSAP + SVG scene written for that beat,
949
+ keyed to its narration cues, built on its own visual device. The archetype is only the
950
+ fallback for a scene that fails its gates. `--no-bespoke` (or `"bespoke": {"enabled":
951
+ false}`) keeps every archetype and makes no call; classic never runs it and emits the bytes
952
+ it did. A v2 build without narration has no cues to key a scene to, and says so.
934
953
 
935
954
  ```bash
936
- decksmith build storyboard.json --source source.json -o deck --design v2 --bespoke
937
- # bespoke: b03-action-loop.draft in 121s …
955
+ decksmith build storyboard.json --source source.json -o deck --design v2
956
+ # bespoke: devices (codex) — b01 particle-assembly, b02 fog-lift+art, b03 edge-sweep, …
938
957
  # bespoke: b05-backbone — every gate passed and the rubric probe is clean; no critique call
939
- # bespoke: 4 of 5 beats drawn bespoke, 7 Codex call(s), 190985 tokens, 343s
958
+ # bespoke: b07 (bar-compare) FALLBACK · device draining-light-bars · motion graphics — failed the gates after the critique round: error card_row: …
959
+ # bespoke: 12 of 14 beats drawn bespoke (86%; 2 fell back, 0 not eligible), 19 scene call(s) + 1 device call + 6 illustration(s), …
940
960
  ```
941
961
 
942
- **Which beats.** A deterministic rule (`src/bespoke/select.ts`): mechanism archetypes
943
- (pipeline, equation-walk/-morph, line-chart, stack, grid, split-/bar-compare, and figures
944
- last), mechanism words in en/ko/ja/zh, a cited equation, and the beat's weight; ties
945
- broken by a hash. Never a title, callout or table, a beat with no narration, or a beat a
946
- camera dives through. `plan --bespoke` lets the planner mark beats `bespoke: true|false`;
947
- `false` is obeyed, `true` is a strong hint.
948
-
949
- **Per beat:** illustration → draft → static contract → probe deck (a real build with every
962
+ **Round 6: the picture moves, not UI over it.** After the founder's verdict on round 5
963
+ ("graphic animation by animated UI elements is old-fashioned, and the fonts are too
964
+ large"), every bespoke scene with a picture is a FULL-FRAME SHOT: the picture covers the
965
+ frame behind the deck's own headline (which keeps its place and entrance, over a soft
966
+ scrim), and its motion is the picture's own — a camera moving through its depth, focus
967
+ pulled to the subject the voice names, a slow light sweep, the subjects breathing. No
968
+ plates, chips, label pills or leader-line callouts animate in, and nothing a scene writes
969
+ renders above the 56px headline (`TYPE_SCALE` in `src/emit/type.ts`); the narration and the
970
+ subtitles carry the words. Every beat with two cues gets a picture, data beats included
971
+ (their subjects ARE the quantities, drawn in proportion), up to `--bespoke-art` — and the cap
972
+ goes to data beats first: a drawn chart is where the gates refuse most (all five fallbacks of
973
+ the rebased branch's two smoke builds were drawn data beats, whose archetype grows its bars),
974
+ while every pictured scene passed on its first draft.
975
+
976
+ **Which beats** (`src/bespoke/select.ts`): every beat with a narration cue, except one a
977
+ camera dives into or out of (the dive is aimed at a part the archetype drew) and one the
978
+ planner marked `bespoke: false` (`plan --bespoke`) that cites a figure or table of the
979
+ paper. A `false` on a beat citing neither is not kept: it was how r3's b12 stayed six grey
980
+ row plates for 16 seconds. When `--bespoke-calls` cannot pay two
981
+ calls a beat, the most mechanical win (mechanism archetypes and words in en/ko/ja/zh, a cited
982
+ equation, weight; ties by a hash) and the rest keep their archetype.
983
+
984
+ **The device pass** (`assignDevices`, `src/bespoke/pipeline.ts`), once per deck before any
985
+ picture or scene is asked for: one Codex call names, for every beat from its content, the
986
+ visual device its scene is built on (`spike-train`, `fog-lift`, `edge-sweep`,
987
+ `draining-light-bars`, `track-race` …) — never a layout, and never type or a UI element
988
+ (`kinetic-title`, `word-cascade`, `fill-gauges`, anything named for a card, chip, label,
989
+ plate, tile, badge, gauge or stamp: `isUiDevice`). The founder's rule of 2026-10-10 is in
990
+ the prompt: the motion is the picture's (camera, parallax, light, particles, things in it
991
+ moving), never labels or boxes sliding or popping in, and no giant lettering. A cached
992
+ answer that breaks it is asked again for that beat alone — and which beats get an
993
+ illustration (never a data beat or a one-cue beat, at most `--bespoke-art`; the rest are pure
994
+ motion graphics, so the deck varies). A name the model repeats, garbles or leaves out, and
995
+ every name when there is no call, comes from a rule catalogue; no two beats of a deck share
996
+ one. With each name the pass writes an `idea` — how the scene is composed and moves — and is
997
+ told to vary compositions, not only names (four comparison beats drawn as "two big numbers
998
+ over a shape" was the failure), and to keep metaphors truthful (the heavier side of a balance
999
+ sinks; a length that stands for a number starts from zero). Each scene prompt gets its own
1000
+ device and idea only, and is told a row of cards, boxes and arrows, bullet columns or a tile
1001
+ grid is not a main visual. The answer is cached by the whole deck's words AND per beat: a
1002
+ rerun after one beat changed (or `--bespoke-calls` picked a different set) keeps every
1003
+ unchanged beat's device and idea — the call is told they are decided — so the scenes cached
1004
+ under them still hit; only the changed beat is drawn again.
1005
+
1006
+ **Per beat:** device → illustration (or none) → draft → static contract → probe deck (a real build with every
950
1007
  `verify` gate plus the motion gates, photographed at every cue) → the rubric probe → at most
951
1008
  one critique-and-fix call with the contact sheet and the measures attached (`codex exec -i`)
952
- → static contract → probe again → repair rounds. A draft that passed every gate and is clean
1009
+ → static contract → probe again → repair rounds → for a beat that would otherwise fall back
1010
+ (its critique's scene fails and its draft did not pass), ONE second critique with that
1011
+ scene's own frames and findings, if the call cap and 420s of the clock allow, then its gates
1012
+ and repairs. r1 (2026-10-10) fell back on its three core mechanism beats, 24% of the video,
1013
+ each on a collision or two its critique had moved rather than removed. A draft that passed every gate and is clean
953
1014
  by the rubric probe is kept without the critique call; one that fails only on what the
954
1015
  repair fixes, and is otherwise clean, is repaired instead of critiqued. A beat that does not
955
1016
  pass keeps its archetype — the deck is never failed. If the fix round breaks a draft that
@@ -981,33 +1042,99 @@ boxes are known in box px before the scene is written. It is read only from
981
1042
  12 MB. `--bespoke-art <n>` (default 6; `bespoke.art`) caps pictures per deck, counted apart
982
1043
  from `--bespoke-calls`; a beat whose picture cannot be had is drawn without one.
983
1044
 
984
- **Data beats** (line-chart, bar-compare, data-table) get no picture: their scene is asked
985
- for the chart that builds with the voice — axes draw, bars grow with their counters, the
1045
+ Since round 5 a picture is TWO LAYERS from one art call: a backdrop of the setting (an
1046
+ environment with depth, no subjects, quiet in the top fifth), then the subjects drawn with
1047
+ the image tool's `transparent_background` and the backdrop as a reference image, so they
1048
+ stand in it. The subjects are inspected laid on the pack's ground; the backdrop is read for
1049
+ writing too. Round 5's shell drew the backdrop behind the camera's wrapper and moved it at half
1050
+ the camera's zoom; round 6 cuts both pictures into depth planes instead (below). The deck's pictures are planned together in the up-front
1051
+ device pass — a different setting and different subjects per illustrated beat — and stock
1052
+ stand-ins (robots, mascots, brains, light bulbs, gears, screens) are forbidden unless the
1053
+ beat names one. A picture that draws one, shares a subject noun with another picture of the
1054
+ deck, or sits closer than `SIMILAR_MAX` to one by Vision's feature print is redrawn once,
1055
+ told what the deck already shows; `bespoke.json` reports the closest pair (`repetition`).
1056
+
1057
+ **Depth planes** (round 6, `src/bespoke/depth.ts`, `src/bespoke/sheet.ts`). The kept
1058
+ picture is cut into a multiplane image (Zhou et al. 2018; Tucker & Snavely 2020): the
1059
+ backdrop's depth is estimated by Depth Anything V2 Small (Apple's Core ML build, F16, run on
1060
+ the CPU by a Swift helper compiled once; 1.5s and 156 MB a picture, measured), the backdrop
1061
+ is sliced at its depth quantiles into up to three planes (none closer in depth than 0.12),
1062
+ what a nearer plane uncovers on the one behind is filled by push-pull at quarter
1063
+ resolution, and each subject is cut onto its own plane at the depth where its foot meets
1064
+ the backdrop's floor, held in front of the farthest band. Each plane is given a distance
1065
+ (the subjects' plane is 1; others 0.8-3). The shell moves every plane EXACTLY as a pinhole
1066
+ camera sees it at that distance (`planeFrame`: for fronto-parallel planes a homography is
1067
+ a scale and an offset, so it is a CSS transform per plane), enlarged just enough to keep
1068
+ covering the frame (`overscan`), so a push-in or a truck shows real parallax. Focus is
1069
+ racked by cross-fading each plane with a blurred twin (`softAt`; a CSS `filter: blur()`
1070
+ tween cost ~1.5s a frame in the gates' browser). Before slicing, both pictures are
1071
+ upscaled 2x by Real-ESRGAN's `realesr-animevideov3` model through its ncnn/Vulkan build
1072
+ (1.6s and 129 MB a picture, measured), so a push-in stays sharp: the cap `EFF_MIN` is now
1073
+ 0.85 picture px per output px. BOTH MODELS ARE OPTIONAL and never in git:
1074
+ `DECKSMITH_DEPTH_MODEL` names the `.mlpackage` (else
1075
+ `~/.cache/decksmith/models/DepthAnythingV2SmallF16.mlpackage`) and `DECKSMITH_UPSCALER`
1076
+ the `realesrgan-ncnn-vulkan` binary with a `models/` folder beside it (else
1077
+ `~/.cache/decksmith/tools/realesrgan/`). Without them — off macOS, on CI — the backdrop is
1078
+ one plane at distance 2 and the subjects stay at 1 (round 5's parallax, in the new layout),
1079
+ and `bespoke.json` says which and why (`art.depth.info`). A wipe in depth LIGHTS the
1080
+ subjects in turn (they stand dim from the first frame) instead of clipping the picture.
1081
+ A picture whose subjects merged into one is staged as a zoom-out, not a follow, rack or
1082
+ cutaway.
1083
+
1084
+ **Data beats** without a picture (line-chart, bar-compare, data-table) get their scene asked
1085
+ for the chart that builds with the voice — axes draw, the marks come on at their size, the
986
1086
  line traces, the named value lights — from the beat's own numbers, never a table painted
987
- over an illustration (`data_over_picture`).
988
-
989
- **The camera** of an illustrated scene is the shell's, staged in one grammar
990
- (`src/bespoke/shots.ts`). The scene names its `shots` — on which cue, how far into it, which
991
- subject — and the shell compiles them: an ESTABLISHING shot of the whole picture for at
992
- least 1.6s, a PUSH IN (1.1s, power3.inOut) that frames the named subject and its label at
993
- 1.6-2.2x, a move straight to the next subject, a 3% creep while a shot is held, and a
994
- REVEAL back to the whole picture at the last cue; shots closer than 1.6s are dropped. Every
1087
+ over an illustration (`data_over_picture`). Since round 5 each data beat of a deck builds
1088
+ differently (`src/bespoke/databuild.ts`): a line traced with a callout, the delta
1089
+ highlighted and counted, or small multiples lit in turn (round 6 dropped the bar race: bars
1090
+ growing from zero and sliding to their rank are what `ui_motion` refuses) — declared on the
1091
+ chart (`data-build`) and checked statically with its marks; the camera may push in on the
1092
+ value the voice names (required until round 6, when a smoke deck lost a data beat to that rule
1093
+ alone). `verify` refuses two data scenes of a deck that build alike
1094
+ (`build_repeat`).
1095
+
1096
+ **The camera** of an illustrated scene is the shell's (`src/bespoke/shots.ts`), in one of
1097
+ seven GRAMMARS (`src/bespoke/grammar.ts`) picked per beat in the device pass from the
1098
+ narration's rhetorical role (compare, cause, process, reveal, quantify, define — read from
1099
+ the archetype and the narration's own words in en/ko/zh/ja), never the grammar of the
1100
+ illustrated beat before it: `tour` (establish, push in on each named subject, reveal),
1101
+ `follow` (push in, then track along the subjects at one scale), `rack` (A, whip to B, back to
1102
+ A, a two-shot), `zoom-out` (open close on the detail, pull back to the whole), `wipe` (the
1103
+ picture wiped on subject by subject, the camera wide), `cutaway` (hard cuts to close inserts
1104
+ and back) and `parallax` (a slow lateral truck at a medium scale). The scene names its
1105
+ `shots` — on which cue, how far into it, which subject — and the shell compiles them in the
1106
+ grammar; shots closer than 1.6s are dropped, and the end frame is always the whole picture.
1107
+ No push-in goes past the scale where a picture pixel spans more than 1/`EFF_MIN` output
1108
+ pixels (`sharpMax`): the image tool draws ~1.57 megapixels whatever it is asked.
1109
+ `shot_variety` reads the grammar back off the camera's samples (a follow must track, a rack
1110
+ come back or hold a two-shot, a cutaway cut, a zoom-out only pull back, a wipe wipe, a truck
1111
+ travel), and that the backdrop moves less than the subjects; `verify` refuses two
1112
+ consecutive illustrated scenes in one grammar (`grammar_repeat`). Every
995
1113
  camera tween is a `fromTo` with explicit from-values, and no two touch. The scene's own
996
1114
  script may not move `#sN-cam` (`script_camera`). A scene without a picture still moves the
997
- wrapper itself (`scale`/`x`/`y`, arithmetic in the prompt). A scene with a moving camera is
1115
+ wrapper itself (`scale`/`x`/`y`, arithmetic in the prompt), and since round 6 its words keep
1116
+ their declared size under it: for every camera tween (the wrapper's `scale`, or the svg's
1117
+ `viewBox`) the shell adds the inverse scale on each word about its own centre over the same
1118
+ span (`quietWords` in `src/bespoke/scene.ts`; r1's final review measured labels at 75-80px
1119
+ under zoom). A scene with a moving camera is
998
1120
  clipped to its box and marked `data-ds-clip`, so `verify` does not count what a push-in
999
1121
  carries past the canvas edge as off-canvas.
1000
1122
 
1001
- **The labels** of an illustrated scene are the shell's too (`src/bespoke/callouts.ts`). The
1123
+ **The labels** (rounds 4-5; gone in round 6). An illustrated scene's names were the shell's (`src/bespoke/callouts.ts`, deleted). The
1002
1124
  scene says what each subject is called (`labels`); the shell gives each subject a zone just
1003
1125
  above it (or across its top when there is no room), as wide as half the gap to each
1004
- neighbour, and sets the name there on a plate — 56px, shrinking to 44, then two lines —
1126
+ neighbour, and sets the name there on a plate — 64px, shrinking to 52, then two lines —
1005
1127
  with a leader line to a dot on the subject, entering as the camera first arrives on it (or
1006
- at the reveal). The zones are in the draft prompt, which tells the scene to keep its own
1007
- drawing out of them; a name that does not fit even on two lines at 44px is refused
1008
- statically (`labels`). Round 4's first run, with the scene placing its own labels on the
1009
- subjects, put them there and then failed four scenes in five on their own plates and
1010
- leader lines crossing their text.
1128
+ at the reveal). Since round 5 the top 120px of the box is the CAPTION BAND, where the scene
1129
+ draws its own words; a label goes above its subject only under the band, else below it,
1130
+ else across its top; and a label is held at 1/sqrt(s) of the camera's zoom, so on screen it
1131
+ grows by sqrt(s) in a push-in and is its own size in the whole view. `label_size` holds
1132
+ every label to 52px as rendered at every graded frame; `label_band` refuses a label in the
1133
+ band or the scene's own words within 24px of one. Round 6 removed them: plates
1134
+ and leader lines entering as the camera arrived were exactly the "animated UI elements"
1135
+ the founder called old-fashioned, and every name was one the narration already says. A
1136
+ round-6 scene's own layer is FIXED to the frame (`#sid-fx`), holds at most two short
1137
+ phrases or one number, 40-56px, fading in place, and nothing aimed at a subject.
1011
1138
 
1012
1139
  **Repair** (`src/bespoke/repair.ts`), no model call. On `text_overlap`,
1013
1140
  `graphic_crosses_text` or `svg_text_overprint`, each colliding label's unit (the text, or
@@ -1017,6 +1144,8 @@ every visible stroke sample by 10px and every shape painted over it, inside the
1017
1144
  every frame the probe measured, camera scale included; it is written as a wrapping
1018
1145
  `<g transform="translate()">`. A label never moves farther than 0.8 of its own height
1019
1146
  (at least 32px): a collision that needs more is left to the critique round. On
1147
+ `label_band` the same nudge keeps the scene's own words 26px (camera-free) clear of the
1148
+ shell's labels, which never move. On
1020
1149
  `end_dimmed`, the elements dimming parts the scene had lit are tweened back to full
1021
1150
  strength just before the end; on `camera_end`, the camera is tweened home. On `seek_order`,
1022
1151
  two tweens on one target animating one property over overlapping time — which a timeline
@@ -1026,15 +1155,18 @@ property when both start together; `src/bespoke/untangle.ts`). A repaired scene
1026
1155
  through the static contract and is gated again (at most two repair rounds).
1027
1156
 
1028
1157
  **What the model is shown** (`src/bespoke/prompt.ts`): the beat, its cues, the paper's
1029
- excerpts (fenced, untrusted), the contract, motion-design rules with numbers (one focal
1030
- element per cue at 88-120px, the rest dimmed but kept; fill the box; paint it with filled
1031
- shapes; at least three kinds of motion, one of flow, camera, counter or morph; the end
1032
- frame a summary), text widths measured in the pack's own font, and two of four hand-made
1158
+ excerpts (fenced, untrusted), the contract, motion-design rules with numbers (quiet type:
1159
+ words 40-44px, one key number or word up to 56px, nothing bigger; NO UI MOTION — no word,
1160
+ plate, chip, card or bar moved, scaled or grown into place; one focus per cue; fill the
1161
+ box; at least three kinds of motion, one of flow, camera, counter or morph; the end frame a
1162
+ summary — for a scene on a depth picture, a shorter direction: the shot list is the scene), text widths measured in the pack's own font, and two of four hand-made
1033
1163
  reference scenes (`src/bespoke/references.ts`: a routing mechanism with particle flow and
1034
1164
  a counter, a camera zoom into one block, a traced curve whose gap morphs into the headline
1035
- number, a scatter whose dots travel into a bell, and — first, for a beat with an
1036
- illustration — a scene built on its picture), picked for the beat's archetype and painted
1037
- in the pack's colours. `test/bespoke-references.test.ts` runs each reference
1165
+ number, a scatter whose dots travel into a bell — round 6 rewrote every one of them to
1166
+ the quiet type scale with nothing sliding, popping or growing into place), picked for the
1167
+ beat's archetype and painted in the pack's colours; a scene on a depth picture is shown
1168
+ none (round 4's "illustrated" reference, labels and a spotlight over the picture, is
1169
+ deleted). `test/bespoke-references.test.ts` runs each reference, and a depth-staged scene,
1038
1170
  through every gate and the rubric probe in a real deck, so a reference that stops passing
1039
1171
  stops being one. Parts a cue introduces are `<g data-cue="N">` groups (semantic grouping,
1040
1172
  after Vector Prism, arXiv 2512.14336). MorphSVG is registered for a deck only when one of
@@ -1043,19 +1175,21 @@ its scenes morphs.
1043
1175
  **The rubric probe** (`rubricProbe`, free: it reads what the probe measured) sends a
1044
1176
  passing draft to the critique round when any of these is off: the drawing paints under 12%
1045
1177
  of its box at the end (round 1's scenes painted 4-16%), more than half its parts are still
1046
- dimmed at the end, no label reaches 88px (not asked of an illustrated scene, whose picture
1047
- is its focus and whose names are the shell's), fewer than three kinds of motion or none of
1048
- flow/camera/counter/morph, an illustrated scene that holds fewer than two push-ins, does not
1049
- open wide, or names fewer than two subjects on them, a cue whose picture changes by under
1050
- 0.5% of the frame, or a warning about the scene. The critique round scores the frames against a six-line rubric
1051
- (fills the stage, one focus per cue, motion explains, legible hierarchy, the pack, sync),
1178
+ dimmed at the end, no label reaches 44px (not asked of an illustrated scene), a label declares over 56px, a word rendered over the 56px headline, any UI animated into place,
1179
+ fewer than three kinds of motion or none of flow/camera/counter/morph (not asked of a scene
1180
+ on a picture: the picture's camera, focus and light are its motion), an illustrated scene
1181
+ that holds fewer than two push-ins or does not open wide, a cue whose picture changes by
1182
+ under 0.5% of the frame, or a warning about the scene. The critique round scores the frames against a six-line rubric
1183
+ (fills the stage, one focus per cue, motion explains with no UI motion, quiet type, the pack, sync),
1052
1184
  is told the measures and every finding with the colliding labels' coordinates, and returns
1053
1185
  the fixed scene.
1054
1186
 
1055
- **Caps**, checked before every call: `--bespoke-calls` (default 12; two per beat) and
1056
- `--bespoke-seconds` (default 1800). A quota or rate-limit answer stops every further call.
1057
- Codex runs only through the account's own CLI (`--bespoke-cli`, `--bespoke-model`), never
1058
- an API key, five calls in flight (`bespoke.concurrency` in the config file), at
1187
+ **Caps**, checked before every call: `--bespoke-calls` (default 40 scene calls; two per
1188
+ beat), `--bespoke-seconds` (default 3600 for the pass) and `bespoke.callSeconds` (default
1189
+ 600: one call's ceiling, the per-scene timeout). A quota or rate-limit answer stops every
1190
+ further call. Codex runs only through the account's own CLI (`--bespoke-cli`,
1191
+ `--bespoke-model`), never an API key, two calls in flight across pictures, the device call
1192
+ and scenes alike (`bespoke.concurrency` in the config file), at
1059
1193
  `bespoke.effort` reasoning (default `medium`; `default` keeps the account's), from the
1060
1194
  scratch directory, and with its tools off: shell, apps, plugins, browser, image
1061
1195
  generation (the illustration call alone keeps it), web search and every MCP server the
@@ -1074,7 +1208,10 @@ a fallback the beat did not earn (cap, quota, timeout) is not.
1074
1208
 
1075
1209
  **What a generated scene may run.** Three layers. (1) A static walk before anything is
1076
1210
  built or opened (`src/bespoke/contract.ts`, acorn): GSAP timeline calls with literal vars
1077
- at explicit seconds, `gsap.set`, scoped `root.querySelector`, `Math` minus `random`, local
1211
+ at explicit seconds — `tl.fromTo` whose from state names every property it moves, and
1212
+ `tl.set`; `tl.to`, `tl.from` and keyframes are refused (`script_fromto`, AGENTS.md invariant
1213
+ 2: a `to` starts from whatever the page holds when it first renders) — `gsap.set`, scoped
1214
+ `root.querySelector`, `Math` minus `random`, local
1078
1215
  code; `window`, `document`, `fetch`, `eval`, timers, storage, navigation, callbacks and
1079
1216
  function-valued tween vars are refused by name; CSS must be scoped to the scene with no
1080
1217
  at-rules, `url()` or animation; markup is SVG and inline HTML with no handlers, SMIL or
@@ -1097,16 +1234,42 @@ its plate — or a shape painted over one), `seek_order` (the frame must not dep
1097
1234
  was seeked before it, beyond 1500px of rasterising noise — counted on changed AREAS: a pixel
1098
1235
  counts when it and its 8 neighbours changed, so a picture re-rasterised a hair differently
1099
1236
  on its first paint, an outline round every subject, 12,698px raw, counts 309), `stage_fill` (the settled
1100
- drawing's bounding box covers 80% of its box), `type_hierarchy` (one label of 64px or
1101
- more at the end; not asked of an illustrated scene), `stray_marker` (an SVG marker painted where its line is not drawn),
1102
- `early_reveal` (a `data-cue="N"` group showing more than 0.5s before cue N),
1103
- `shot_variety` (an illustrated scene opens on the whole picture and the camera — sampled
1237
+ drawing's bounding box covers 80% of its box), `type_hierarchy` (one label of 44px or
1238
+ more at the end; not asked of an illustrated scene), `type_ceiling` (no label DECLARES more
1239
+ than 56px at the end; KaTeX is exempt), `type_scale` (round 6: no word the scene
1240
+ draws RENDERS above the 56px headline at any graded frame, its camera's zoom included;
1241
+ round 2's 64px key-label floor is gone), `ui_motion` (round 6: the scene's own GSAP
1242
+ timeline animates no label, plate, chip, card or bar into place — read off the tweens'
1243
+ targets and properties, so an opacity fade passes and a particle or a traced path is not
1244
+ UI; a bar is a rect resized, or a path or polygon grown from under 70% of its size; the
1245
+ shell's own counter-scale of words under a camera is not counted), `seam_blank` (round 6:
1246
+ the stage — everything but the headlines — painted under 0.2% of the frame for more than
1247
+ 0.15s across the seam into a generated scene; r1's final deck showed nothing for ~0.6s at 12
1248
+ of 15 seams, so cue 1's group now stands from the scene's first frame), `hollow_hold` (while
1249
+ cue 2 onward holds, and at the end, the drawing spans under 70% of the box's height),
1250
+ `marks_overlap` (at those frames, alike filled dots drawn into each other, deeper than half
1251
+ the smaller's radius), `stray_marker` (an SVG marker painted where its line is not drawn),
1252
+ `early_reveal` (a `data-cue="N"` group, N of 2 or more, showing more than 0.5s before cue N),
1253
+ `text_clipped` (a word cut by the frame's edge while held: sampled every 0.5s, three samples
1254
+ in a row, under 96% of its width or 75% of its line box inside the clip), `morph_glitch` (a
1255
+ path tween — `attr: { d }` or morphSVG — whose shape, at any in-between sample, reaches more
1256
+ than 35% of its ends' size past the union of its two ends: r1 s4's wedge, an x tweened into
1257
+ a y between paths of different commands), `dim_text` (a word held under 3:1 contrast with
1258
+ what is painted behind it at a cue's end or the end; repaired without a call by lifting a
1259
+ literal dimming opacity on its targets to 0.6),
1260
+ `shot_variety` (an illustrated scene opens on the whole picture — unless its grammar is
1261
+ `close-open` — and the camera — sampled
1104
1262
  every 0.5s, no screenshot — HOLDS push-ins at 1.5x or closer on at least two different
1105
- subjects; round 3's 1.1-1.4x pans are not push-ins), `label_anchor` (a label that names a
1263
+ subjects; round 3's 1.1-1.4x pans are not push-ins), `label_anchor` (a label of the scene's that names a
1106
1264
  subject sits within 96px of its box and covers no other subject by more than a quarter of
1107
- itself, and at the end at least two subjects are named that way), `data_over_picture`
1265
+ itself), `data_over_picture`
1108
1266
  (five or more numbers on the illustration),
1109
- `cue_groups` (no such groups, or one naming a cue the scene does not have), `end_dimmed`
1267
+ `cue_groups` (no such groups — not asked of a scene on a picture, which may add nothing —
1268
+ or one naming a cue the scene does not have), `card_row`
1269
+ (`src/bespoke/cards.ts`, read off the markup: three or more alike rectangles — rects, or
1270
+ divs placed in px — in a row or a column, or four in a grid, covering 12% of the box —
1271
+ cards, panels or tiles as the main visual; never asked of a data beat, whose bars are alike
1272
+ by design; such a scene goes to its critique and falls back if it is still cards), `end_dimmed`
1110
1273
  (at the settled frame, more than 10% of the parts the scene had shown lit are left under
1111
1274
  0.6 opacity — a part drawn translucent from the start is not counted) and `camera_end` (the
1112
1275
  camera, or a viewBox, not home at the settled frame). Content
@@ -1127,15 +1290,25 @@ skipped by `build` when the pass already ran it on the same bytes at the same sc
1127
1290
  always runs it. Set
1128
1291
  `DECKSMITH_BESPOKE_WORK=<dir>` to keep the prompts, replies and contact sheets.
1129
1292
 
1130
- **Costs** (measured on four HypePaper decks, twice each, see
1131
- [`.planning/2026-10-09-v2-bespoke-round4.md`](.planning/2026-10-09-v2-bespoke-round4.md)):
1293
+ **Costs, round 6** (four HypePaper decks, twice each, see
1294
+ [`.planning/2026-10-10-v2-bespoke-round6.md`](.planning/2026-10-10-v2-bespoke-round6.md)):
1295
+ a picture takes 90-200s (two layers, redraws included) and its depth cut 5-26s; a draft on a
1296
+ picture ~25s, and critique calls are rare; a deck's `build` took 431-696s for 5-7 scene calls
1297
+ plus 5-6 pictures and 157-237k tokens. A deck carries 1.3-2.9 MB of planes (5.4-7.2 MB in
1298
+ all, against round 5's 4.0-5.3). Round 4's numbers, for the record
1299
+ ([`.planning/2026-10-09-v2-bespoke-round4.md`](.planning/2026-10-09-v2-bespoke-round4.md)):
1132
1300
  an illustration takes 60-120s, a draft 90-220s and a critique 70-120s; a deck's pass took
1133
1301
  380-730s for 5-8 scene calls plus 3-7 pictures (redraws included) and 153-227k tokens, and
1134
1302
  the whole `build` 466-812s on a Mac with 9-23% memory free — three builds of eight under ten
1135
1303
  minutes. The pictures add 128-348 KB of WebP to a deck (round 3: 6-7 MB of PNG). Round 3
1136
1304
  took 593-742s and 214-325k tokens; round 1 13-31 minutes and 540-705k. A rebuild from the
1137
- cache makes 0 calls. None of that fits a 300-second build timeout: a host running this has
1138
- to give `build` that long, or run it off the request path.
1305
+ cache makes 0 calls. Those are 4-6-beat passes at five calls in flight; drawing every beat
1306
+ of a deck at two in flight costs several times that: MEASURED 2026-10-10 on one 14-beat ko
1307
+ deck, 11 of 14 drawn bespoke, 21 scene calls + 1 device call + 3 pictures, 574k tokens, a
1308
+ 1508s pass and a 1567s `build`.
1309
+ None of it fits a 300-second build timeout: a host that builds v2 decks on its request path
1310
+ — HypePaper's build step — must pass `--no-bespoke`, or give `build` that long off the
1311
+ request path.
1139
1312
 
1140
1313
  ## Themes
1141
1314
 
@@ -1262,13 +1435,28 @@ decksmith illustrate storyboard.json --source source.json
1262
1435
  decksmith build storyboard.json --source source.json -o out/
1263
1436
  ```
1264
1437
 
1265
- With `--images`, a beat that has nothing in the inventory to show — a `claim-figure`, or
1266
- either side of a `split-compare` — may carry an `illustration: { prompt, caption }` in
1267
- place of a `figureId`. The prompt describes a scene, never text, labels, numbers or charts:
1438
+ With `--images`, a beat that has nothing in the inventory to show — a `claim-figure`, a
1439
+ `stage`, or either side of a `split-compare` — may carry an `illustration: { prompt, caption }` in
1440
+ place of a `figureId`. A `stage` is how a paper whose figures are all plots gets a
1441
+ full-screen picture; its brief is drawn landscape, since the format is not known until
1442
+ `build`, and a portrait build crops it (and warns). The prompt describes a scene, never text, labels, numbers or charts:
1268
1443
  nothing inside a picture can be read or checked, so the picture illustrates and the beat's
1269
1444
  `evidence` still points at the section. `plan` says how many pictures the storyboard asks
1270
1445
  for and the command to run; `build` and `pack` refuse the file until they exist.
1271
1446
 
1447
+ A `pipeline`, `split-compare`, `callout`, `bar-compare`, `hero-number` or `kinetic` may
1448
+ also carry a `backdrop: { illustration }` — a scene the diagram is drawn over. The picture covers the
1449
+ frame and drifts slowly, a uniform black scrim (0.62) darkens it, and the archetype is
1450
+ drawn unchanged in a glass palette: dark translucent panels, light ink, tones brightened
1451
+ (then whitened only if they must be) just far enough to clear 4.5:1 over a pure-white
1452
+ pixel, and the step-back ink `dim` held below every tone at 3:1, the floor for large
1453
+ text. Geometry and holds are the
1454
+ archetype's own, so a backdrop changes colours and nothing else. With `--images` the
1455
+ planner is held to making most of a deck scenes (60% of beats, within `images.max`) — a
1456
+ stage, a backdrop, a pictured split-compare side, a hero-number or a kinetic claim; a
1457
+ `claim-figure` or `annotated-figure` sits on the pale ground and does not count — so
1458
+ a figure-less paper reads as a sequence of pictures rather than cards on one ground.
1459
+
1272
1460
  `illustrate` turns each brief into a file under `assets/` beside `source.json`, registers
1273
1461
  it as an ordinary figure, and sets the beat's `figureId`; the brief stays on the beat as
1274
1462
  provenance. From there nothing downstream knows the picture was generated — `build`,
@@ -1326,6 +1514,124 @@ carry SMIL or CSS animation, which runs on wall-clock time under capture — a
1326
1514
  nondeterministic render that every gate passes. The tool's own SVG has no text, no
1327
1515
  animation and no external references, and the same brief always draws the same bytes.
1328
1516
 
1517
+ ## Animated pieces
1518
+
1519
+ A **piece** is a figure that moves: a cut-paper scene written for
1520
+ [animate](https://github.com/cth9191/animate), drawn on a `<canvas>` by `claim-figure` or `stage`
1521
+ and played by one `dsAnimate` tween. It is a third figure `kind`, next to `image` and `clip`.
1522
+ You write it by hand. No ingest, `plan`, server or MCP path produces one.
1523
+
1524
+ **Authoring one** takes three edits. Put the scene file under `assets/` beside
1525
+ `source.json`, give it a figure entry, and point a `claim-figure` beat (or a `stage` beat, for the piece alone on the whole frame)
1526
+ at that entry:
1527
+
1528
+ ```jsonc
1529
+ // source.json, under "figures"
1530
+ { "id": "fig-garden", "kind": "piece", "src": "pieces/garden.js",
1531
+ "width": 1920, "height": 1080, "seconds": 6, "caption": "The moon becomes the sun" }
1532
+ // storyboard.json, a beat
1533
+ { "id": "b2", "archetype": "claim-figure", "seconds": 9, "intent": "…",
1534
+ "params": { "headline": "The moon becomes the sun", "claim": "…", "figureId": "fig-garden" } }
1535
+ ```
1536
+
1537
+ The file is an animate `scenes.js` plus `bridges.js`. It defines `ERA_LIST`, `SHOTS`,
1538
+ `ERA_BG`, `BRIDGES`, `pieceCam` and its scene functions, and it uses the kit's names
1539
+ (`cut`, `rect`, `ellipsePts`, `PAL`, `pat`, `spark`, `TT`, `EZ`, `LX`/`LY`, `UNIT` and the
1540
+ rest):
1541
+
1542
+ ```js
1543
+ const ERA_BG = ['#cfe2ee'];
1544
+ function pieceCam(era, t) { return null; }
1545
+ function sceneSky() {
1546
+ cut(rect(-30, -30, W + 60, H + 60, 0), PAL.sky, { key: 'bg', shadow: false, tear: 0, shade: false });
1547
+ const k = EZ.io(seg(TT, 0, DURATION)); // a sun that crosses the sky over the whole piece
1548
+ cut(ellipsePts(LX(0.12 + 0.76 * k), LY(0.55 - 0.38 * Math.sin(Math.PI * k)), 110 * UNIT, 110 * UNIT, 0, 30), PAL.yellow, { key: 'sun' });
1549
+ }
1550
+ const BRIDGES = []; // or [{ tc, A: () => shape, B: () => shape }] between eras
1551
+ const ERA_LIST = [[0, DURATION, () => sceneSky()]];
1552
+ const SHOTS = [['sky', 0, 0.0, DURATION, 'sky']];
1553
+ ```
1554
+
1555
+ Do not define `W`, `H`, `FPS`, `DURATION`, `NFRAMES`, `LOOP_T`, `SAFE`, `HAND`, `CX` or
1556
+ `TIMELINE`. DeckSmith sets them from the figure: its `width`, `height` and `seconds`, 30
1557
+ fps, and the deck's font stack. On a `stage` beat `W` and `H` are the frame's pixels
1558
+ instead (less any caption reserve), so lay the piece out with `LX`, `LY` and `UNIT` rather than
1559
+ fixed coordinates. A file that declares one of them fails to load, and
1560
+ `verify` reports that as a page error. `TIMELINE` carries `shots` only. animate's own demos
1561
+ read `TIMELINE.cues`, so write those times as numbers.
1562
+
1563
+ `build` wraps the file and the kit into one script, `assets/<src>`, inside a function, so
1564
+ none of the kit's names reach the page. The piece starts 1.0s into its beat and runs for
1565
+ its `seconds`, with a 0.3s hold after its last frame. Before the tween the canvas shows
1566
+ frame 0. After it, the canvas holds the last frame; it does not wrap back to frame 0. The
1567
+ beat must be at least `1 + seconds + 0.3` long. A shorter beat is refused by name rather
1568
+ than clamped. The hold is never before 2.4s, when the claim, plate and caption have all
1569
+ entered, so a piece shorter than 1.1s holds there instead. On a `stage` the floor is 2.1s
1570
+ when there are words over it and 1.1s when there are none.
1571
+
1572
+ **What is refused**, and where:
1573
+
1574
+ - **Text in a piece.** `fillText` and `strokeText` throw on every 2D canvas while the
1575
+ piece runs, the kit's offscreen layers included. That is a page error, so `verify` and
1576
+ `build` fail. `handText`, `tag`, `yearTag`, `capStrip`, `monoText` and `cat` all draw
1577
+ text, so none of them can be used. Labels belong in the claim, which is DOM text and
1578
+ is held to the 40px floor. `handwrite` draws letters as ink strokes, which no runtime
1579
+ trap can tell from lines, so `build` refuses a file that calls it by name
1580
+ (`pieces/garden.js:12 calls handwrite — …`). An alias of it is not caught.
1581
+ - **A last era or last shot that ends before the piece does.** morph.js draws era 0 for
1582
+ any time past the last era, so `[[0, 3, …]]` in a 6s piece would replay the opening
1583
+ scene for 3s and the whole hold. The piece throws when it mounts (`piece "fig-garden":
1584
+ ERA_LIST ends at 3s and the figure plays 6s — end its last entry at DURATION`), which
1585
+ fails `verify`. End the last entry of `ERA_LIST` and of `SHOTS` at `DURATION`.
1586
+ - **More than one piece per deck.** `build` stops with ``claim-figure b3: figure
1587
+ "fig-garden-2" is a second animate piece in this deck — b2 already draws "fig-garden"``.
1588
+ The vendored `morph.js` is unpatched and writes `window.renderFrame`, so two pieces
1589
+ would share that global.
1590
+ - **Styles other than cut-paper.** Only the cut-paper kit is vendored. Another style's
1591
+ primitive is simply undefined: a piece that calls riso's `plates()` fails `verify` with
1592
+ `page_error plates is not defined`. Riso and pixel read pixels back from the canvas,
1593
+ and that has not been measured under capture.
1594
+ - A piece in any archetype but `claim-figure` and `stage`, a piece with no `seconds`, a `src` that
1595
+ does not end in `.js` (the determinism scan reads only those), and an `id` containing
1596
+ anything other than letters, digits, `.`, `_` and `-`.
1597
+
1598
+ **What the gates see.** A piece that throws while it draws fails `verify`, because the
1599
+ runtime reports the error before rethrowing it. Before that fix, hyperframes swallowed
1600
+ the error and every gate passed a piece frozen on a stale frame. That is the eleventh
1601
+ case under "What the gates do not check". `render` still exits 0 when the page has
1602
+ errors, so `verify` is the gate here. `hyperframes check` sees a throw only at the times
1603
+ it samples, so the fidelity gate also draws every frame of each piece once, off-screen,
1604
+ and fails with `piece_error` on any that throws: a throw confined to one frame is caught
1605
+ too. `frames` refuses to write a PNG once the page has raised an error, rather than
1606
+ saving a stale or half-painted canvas. `hyperframes check` cannot see inside a canvas:
1607
+ layout and contrast cover only the DOM around it. The determinism scan reads
1608
+ `assets/**/*.js`, so a `Math.random`, a `setTimeout`/`setInterval`, a
1609
+ `requestAnimationFrame` loop or a `new Image()` in a piece is caught. A checker without WebGL
1610
+ measures a piece instead of refusing it, because the piece's canvas carries
1611
+ `data-ds-piece`.
1612
+
1613
+ Measured on 2026-10-09 (macOS, Metal, hyperframes' headless shell). The deck had a 6s
1614
+ two-era piece with a moon-to-sun bridge in a 9s beat:
1615
+
1616
+ - `build` and `verify` both passed.
1617
+ - `render -w 1` took 13.1s for 360 frames. Stills at ten instants showed the night scene,
1618
+ the bridge, the morning scene and then the last frame held.
1619
+ - `drift --workers 1` found 360/360 frames byte-identical, and a second render produced
1620
+ an mp4 with the same md5.
1621
+ - A `pack`/`unpack` round trip rebuilt the same files.
1622
+
1623
+ **Limits.** A piece is baked at 30 fps, so `render --fps 24` judders, and `--speed` plays
1624
+ the whole piece faster or slower than written. Each piece inlines about 60 KB of kit.
1625
+ Canvas raster differs between Metal and SwiftShader, so compare `drift` runs on one
1626
+ backend only. See `.planning/2026-10-09-animate-piece-design.md` and
1627
+ `.planning/2026-10-09-animate-piece-spike.md`.
1628
+
1629
+ **Attribution.** The kit (`core.js`, `morph.js`, `cut-paper.js`) is animate @7e5eb56,
1630
+ copyright (c) 2026 cth9191, MIT. It is vendored unmodified in `src/build/animate/`, with
1631
+ upstream's `LICENSE` and a `NOTICE` that names the commit and each file's sha256.
1632
+ `npm run build` copies all of it to `dist/animate/`. Every assembled piece script carries
1633
+ the MIT text in its header, so a built deck carries the attribution too.
1634
+
1329
1635
  ## The `.deck` container
1330
1636
 
1331
1637
  One file holding the whole deck: the source, the storyboard, the preferences it was made
@@ -1386,7 +1692,7 @@ names the figure or equation it rests on, a later pass can ask whether the anima
1386
1692
  actually asserts what the source asserts. Prior art verifies that slides *look* fine;
1387
1693
  nothing verifies that they are *true*.
1388
1694
 
1389
- ## The thirteen archetypes
1695
+ ## The sixteen archetypes
1390
1696
 
1391
1697
  The explanatory vocabulary. These came out of hand-building a real deck
1392
1698
  (`.planning/EXPERIMENT-002-thinksr-korean.md`), not from guessing at what might be useful.
@@ -1395,21 +1701,71 @@ The explanatory vocabulary. These came out of hand-building a real deck
1395
1701
  |---|---|---|
1396
1702
  | `title` | opening or section break | `headline`, `eyebrow?`, `sub?` |
1397
1703
  | `claim-figure` | one assertion beside the figure that supports it | `claim`, `figureId` |
1704
+ | `stage` | one picture, UI, clip or piece filling the whole frame, words optional over it | `figureId` or `illustration`, `placement`, `headline`, `line?` |
1398
1705
  | `equation-walk` | an equation explained symbol by symbol | `equationId`, `terms` (1–4) |
1399
1706
  | `equation-morph` | one equation becoming the next, the shared terms carried across | `fromId`, `toId`, `terms` (1–4) |
1400
1707
  | `data-table` | a results table with rows revealed in argument order | `tableId`, `highlight` |
1401
1708
  | `line-chart` | a trend, with per-step deltas, optionally against a baseline | `points`, `deltas?`, `readout?`, `compare?` |
1402
- | `callout` | 1–3 panels of prose: definitions, contrasts, takeaways | `panels`, `note?` |
1403
- | `pipeline` | stages in a flow, arrowed, with an optional feedback loop | `stages` (2–6), `loop?` |
1709
+ | `callout` | 1–3 panels of prose: definitions, contrasts, takeaways | `panels`, `note?`, `backdrop?` |
1710
+ | `pipeline` | stages in a flow, arrowed, with an optional feedback loop | `stages` (2–6), `loop?`, `backdrop?` |
1404
1711
  | `annotated-figure` | a figure cropped to the panel under discussion, with leader lines | `figureId`, `crop?`, `notes` |
1405
1712
  | `grid` | regions of a field lit in turn: windows, patches, receptive fields | `cols`, `rows`, `regions` |
1406
- | `bar-compare` | magnitudes that share a unit, grown from zero | `bars` (2–8), `unit?` |
1713
+ | `bar-compare` | magnitudes that share a unit, grown from zero | `bars` (2–8), `unit?`, `backdrop?` |
1407
1714
  | `stack` | layers drawn bottom-up as offset planes | `layers` (2–7) |
1408
- | `split-compare` | two things side by side, each figure or lines | `left`, `right` |
1409
-
1410
- The last six draw the mechanism rather than describe it, and `verify` warns when a deck
1411
- leans on the others: a deck of headlines and bullet panels is what every other slide
1412
- generator already makes.
1715
+ | `split-compare` | two things side by side, each figure or lines | `left`, `right`, `backdrop?` |
1716
+ | `hero-number` | one number as the slide's statement (v2: 56px, faded in; classic: filling the frame, rolled in like an odometer) | `value`, `unit?`, `label`, `compare?`, `headline`, `backdrop?` |
1717
+ | `kinetic` | a claim as type: 2–4 phrases (v2: 44-56px, faded in, the key word turns the accent; classic: each its own move, a key word struck) | `phrases` (`text`, `key?`), `headline`, `backdrop?` |
1718
+
1719
+ All but `title`, `data-table`, `callout` and `kinetic` draw rather than describe
1720
+ (`DIAGRAMMATIC` in `src/types.ts`), and `verify` warns when a deck leans on those four: a
1721
+ deck of headlines and bullet panels is what every other slide generator already makes.
1722
+
1723
+ `stage` is the slide that is not a slide: the figure covers the frame edge to edge
1724
+ (cover-fit, so it is cropped to the frame's shape), with no plate, border or column. Words
1725
+ are optional. `placement` is one of `none`, `bottom-left`, `top-left`, `center` or
1726
+ `right`; under anything but `none` the `headline` (72px) and an optional `line` (44px)
1727
+ are set in white over a black scrim. The scrim's solid part is sized from the text's
1728
+ measured height and width, bounded on both axes so the rest of the picture is left
1729
+ alone, and is 55% black, which white text clears at about 4.8:1 even over a pure white
1730
+ picture, so the contrast gate passes on whatever the picture is. Text is never shrunk: words
1731
+ that need more than three headline lines or two of `line` in their placement's column take
1732
+ a wider column (and `build` warns), and only words that will not fit across the whole
1733
+ content width are refused by name. `right` sets the words flush right; in a portrait
1734
+ frame every placement is full width. The picture fades and settles in first, the words after; one stop lands
1735
+ once both are in, or after a piece's last frame. A still or clip drifts 4% over the beat.
1736
+ `build` warns (it does not refuse) when a raster figure is upscaled more than 1.5x or
1737
+ cropped by more than 30% to fill the frame. With `--reserve-captions` the picture stops
1738
+ above the caption strip. The planner is told to reach for it when a visual should own the
1739
+ screen, and, under RULE 1's variety rule, to give consecutive stages different placements.
1740
+
1741
+ `hero-number` and `kinetic` are full-bleed too, and need no picture (in `--design v2` so
1742
+ is a `bar-compare`, which classic still draws on its ground). Without a
1743
+ `backdrop` they stand on a field of one of the pack's colours — the accent and each tone in
1744
+ turn, by the beat's place in the deck, so field beats near each other differ — darkened
1745
+ until it is no lighter
1746
+ than half the backdrop scrim's worst-case ground, so the same glass inks clear 4.5:1 on
1747
+ it; with one they go over the picture as the diagrams do. Under `--design v2` both are
1748
+ quiet: `hero-number` sets `value` at the headline's 56px in the accent, its label and
1749
+ comparison as body lines, every part faded in where it stands; `kinetic` sets its phrases
1750
+ at 56px down to 44px, each word fading in, and turns the `key` the accent instead of
1751
+ sweeping a chip behind it — the motion is the picture under them. In classic,
1752
+ `hero-number` sets `value` as
1753
+ large as the frame holds (160–560px) and rolls each digit in on a reel — a strip of
1754
+ 0-9 clipped to one cell with `clip-path`, its `y` tweened a whole number of cells, the
1755
+ right-hand reels turning most and every reel landing left to right. No counter is
1756
+ written from a callback (invariant 11). A `compare` whose value and `value` are both
1757
+ plain numbers is drawn as two bars to one scale, the baseline first — unless the
1758
+ shorter would be over 85% of the longer, when bars read as "equal" and the baseline
1759
+ is a second figure followed by the signed difference ("+0.83 dB"); otherwise as a
1760
+ second figure. One stop, after the `headline` under it. `kinetic` sets two to four
1761
+ `phrases` as large as two lines each allow (64–120px), down a slight stair; each
1762
+ arrives word by word with its own move (rise, slide, drop or zoom, the first seeded by
1763
+ the beat id), then its `key` — verbatim in the phrase — is struck by a chip swept in
1764
+ behind it. One stop per phrase. Its moves are the content, so the scene sets
1765
+ `ownEntrances` and the v2 motion grammar leaves them alone. Its `headline` labels the
1766
+ slide and is not drawn. With `--images` both count as scenes, and the planner is held
1767
+ to at most 36% panel beats (pipeline, split-compare, callout, stack, data-table,
1768
+ backdrop or not): five of fourteen.
1413
1769
 
1414
1770
  `line-chart`'s `compare` is the one parameter that changes what its archetype *is*. Given
1415
1771
  `{ label, points }` the chart draws the baseline first, holds it, then reshapes the curve
@@ -1606,6 +1962,8 @@ src/prefs.ts the three-layer preference resolver
1606
1962
  src/emit/kit.ts the seam between the deck shell and the archetype emitters
1607
1963
  src/emit/archetypes/ one emitter per archetype
1608
1964
  src/emit/themes/ one palette per file; the registry is the extension point
1965
+ src/emit/animate-runtime.ts the dsAnimate plugin that draws an animated piece
1966
+ src/build/animate/ animate's cut-paper kit, vendored unmodified (MIT; LICENSE, NOTICE)
1609
1967
  src/emit/type.ts type specs: the v2 packs' faces and chrome scale, read by chromeHeight and chromeCss
1610
1968
  src/emit/faces.ts measured width tables for the packs' Latin faces (generated)
1611
1969
  src/images/ the three rungs a brief is drawn through, and the illustrate step
@@ -1636,4 +1994,5 @@ where a real fix would arrive.
1636
1994
 
1637
1995
  ## Licence
1638
1996
 
1639
- MIT.
1997
+ MIT. The animate kit vendored in `src/build/animate/` is also MIT, copyright (c) 2026
1998
+ cth9191. Its `LICENSE` and `NOTICE` sit beside it; see "Animated pieces".