luckiest-co 1.0.13 → 1.0.14

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 (30) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/skills/luckiest-design-website/ATTRIBUTION.md +38 -0
  5. package/skills/luckiest-design-website/CHANGELOG.md +104 -0
  6. package/skills/luckiest-design-website/LICENSE +21 -0
  7. package/skills/luckiest-design-website/SKILL.md +372 -0
  8. package/skills/luckiest-design-website/UPSTREAM-CHANGELOG.md +526 -0
  9. package/skills/luckiest-design-website/engine/scrollcraft.css +432 -0
  10. package/skills/luckiest-design-website/engine/scrollcraft.js +1167 -0
  11. package/skills/luckiest-design-website/evals/evals.json +33 -0
  12. package/skills/luckiest-design-website/references/assets.md +286 -0
  13. package/skills/luckiest-design-website/references/device-diag.html +214 -0
  14. package/skills/luckiest-design-website/references/devices.md +466 -0
  15. package/skills/luckiest-design-website/references/feel.md +277 -0
  16. package/skills/luckiest-design-website/references/registry-examples.md +305 -0
  17. package/skills/luckiest-design-website/references/taste.md +304 -0
  18. package/skills/luckiest-design-website/references/template.html +138 -0
  19. package/skills/luckiest-design-website/references/uniqueness.md +480 -0
  20. package/skills/luckiest-design-website/references/verify.md +381 -0
  21. package/skills/luckiest-design-website/references/worldflight.md +349 -0
  22. package/skills/luckiest-design-website/references/worlds.md +178 -0
  23. package/skills/luckiest-design-website/scripts/doctor.mjs +177 -0
  24. package/skills/luckiest-design-website/scripts/encode.sh +80 -0
  25. package/skills/luckiest-design-website/scripts/kie.mjs +202 -0
  26. package/skills/luckiest-design-website/scripts/serve.mjs +52 -0
  27. package/skills/luckiest-design-website/scripts/shoot.mjs +644 -0
  28. package/skills/luckiest-design-website/scripts/workspace.mjs +106 -0
  29. package/skills/luckiest-design-website/scripts/worldflight-assert.mjs +273 -0
  30. package/skills/luckiest-design-website/templates/FINGERPRINTS.md +65 -0
@@ -0,0 +1,526 @@
1
+ # scrollcraft changelog
2
+
3
+ Dated notes on what changed in the skill and which build's finding drove it.
4
+ Builds live in `OtherWorlds/Ultimate Websites/builds/`; each carries a
5
+ `BUILD-REPORT.md`.
6
+
7
+ ## 2026-08-23: iOS clip priming hardened; real-device diagnostic added
8
+
9
+ Driven by a build whose hero clip sat frozen on the owner's actual iPhone
10
+ through four rounds of fixes while every headless probe reported it scrubbing.
11
+ The working clip further down the same page was the clue; the only difference
12
+ between them was being first.
13
+
14
+ **Engine, `engine/scrollcraft.js`**
15
+
16
+ - Each clip is primed at `loadedmetadata`, not only on a later gesture. A
17
+ muted inline `play()` needs no activation outside Low Power Mode; where it
18
+ is rejected, gesture listeners retry per clip until every clip is primed.
19
+ (The previous one-shot first-touch prime lost a race: the reader touches to
20
+ scroll while the hero is still downloading, and the shot was spent on a
21
+ sourceless element.)
22
+ - `touchend` and `click` joined the prime listeners. The HTML spec's
23
+ activation-triggering events include `touchend` but not `touchstart`, so a
24
+ Low Power Mode phone gets a valid attempt when the finger lifts.
25
+ - The priming flag releases on a 2s timer. iOS may leave a `play()` promise
26
+ pending forever, which jammed the retry permanently.
27
+ - A seek stuck past 700ms is re-issued. `tick()` skips a clip while
28
+ `el.seeking` is true, so one hung seek froze that clip for the life of the
29
+ page through the guard meant to protect it.
30
+ - The poster reveal fires on a 2.5s timeout as well as on `seeked`, so a clip
31
+ whose `seeked` never arrives cannot stay invisible forever.
32
+ - `muted` and `playsInline` are set as properties at load time, not only as
33
+ attributes; iOS treats the two differently.
34
+
35
+ **Docs and references**
36
+
37
+ - `references/verify.md`: new sections "The phone is a different machine"
38
+ (what headless cannot see, what the engine now handles on iOS), "Ship the
39
+ diagnostic with the site", and "Ask what differs before asking what's
40
+ broken", plus three failure-table rows. Mobile named a first-class target.
41
+ - `references/device-diag.html`: standalone real-device scrub diagnostic.
42
+ Suspect clip loaded two ways beside a known-good clip, MOVING / FROZEN
43
+ verdict per pane, prime results and distinct-frame counts on screen. Edit
44
+ its TESTS array and deploy beside the site on the first mobile report.
45
+ - `SKILL.md` Step 5 now states the real-device gap explicitly.
46
+
47
+ ## 2026-08-22: bespoke fixed-stage verification state
48
+
49
+ Driven by the owner's cold-scroll finding on PHASE: "Nothing happens when I
50
+ start scrolling. Like literally nothing's happening in the first couple of
51
+ scenes." The first pass had passed mechanically because the experience used
52
+ `flow` sections as invisible scroll markers around a page-local fixed stage.
53
+ Ordinary flow is deliberately excluded from dead-scroll detection, so the
54
+ harness never inspected the stage's visible timeline.
55
+
56
+ **Harness: `scripts/shoot.mjs`**
57
+
58
+ - Reads optional `data-sc-verify-state` signatures from bespoke fixed stages
59
+ and includes them in the dead-scroll signature.
60
+ - Flow spans carrying a custom state are now checked like pinned spans.
61
+ - Reads `data-sc-verify-hold="true"` as an explicit authored hold, including
62
+ resolved closes and deliberately stable reduced-motion frames.
63
+ - The contract requires rendered values, not raw scroll progress, so a page
64
+ cannot make a static composition pass by publishing a changing percentage.
65
+
66
+ **Docs**
67
+
68
+ - `references/verify.md` documents the custom-state contract, the authored-hold
69
+ escape hatch, the reduced-motion rule, and the failure mode.
70
+
71
+ **PHASE correction**
72
+
73
+ - Opening act now moves the split, product scale and position, headline, and
74
+ metadata from the first wheel gestures.
75
+ - The frustration act now transfers dominance from the burn state to the
76
+ forgotten state while the seam crosses the cup.
77
+ - Act progress no longer clamps early; the close reaches a true 0% divider.
78
+ - Dense six-samples-per-act sheets replaced the earlier coarse pass.
79
+
80
+ ## 2026-08-21: worldflight mode and the global smoothed playhead
81
+
82
+ Driven by the owner's verdict on the act-based continuous world: "awful... you're
83
+ literally going from scrolling down to static page and then you start scrolling
84
+ down again... weird clear page lines scrolling up... very cheap looking."
85
+ Mechanics ported from oso95/scroll-world.
86
+
87
+ **Engine: `engine/scrollcraft.js`**
88
+
89
+ - New page mode `data-sc-mode="worldflight"`: one `position: fixed` stage, an
90
+ empty spacer as the only element in document flow, legs mounted for the life
91
+ of the page, and scroll driving only the film timeline and overlay opacity.
92
+ - Spacer height is (sum of `data-sc-w` + 1) viewport-heights, set in pixels so
93
+ the track and the `svh`-sized stage share a ruler.
94
+ - One-sided crossfade over a `data-sc-seam` band (default 0.12vh): the incoming
95
+ leg fades up while the outgoing one holds opaque underneath, so the page
96
+ ground never shows through a seam. z-index 120 for the current leg, 100 +
97
+ opacity × 10 for the rest. No `src` swapping, ever.
98
+ - Per-leg `data-sc-linger`, a monotone dwell remap capped at 0.6 with fixed
99
+ endpoints, so seam frames are never touched.
100
+ - Copy windows against the whole track: `hero`, `finale`, or a `from to` pair
101
+ with a plateau. Only transform is `translateY`, capped at 4vh across a window.
102
+ Pointer events gated above opacity 0.5.
103
+ - Legs prefetch within ±1.6vh; until a real frame paints, the poster carries a
104
+ push-in (`scale(1.03 + local × 0.14)`).
105
+ - Waypoints published as `--sc-seg` / `--sc-segp` plus a bubbling `sc:waypoint`
106
+ event. The engine renders no rail UI.
107
+ - Reduced motion: no clip is ever fetched, posters cross-dissolve through the
108
+ same seams and windows, all transforms dropped.
109
+ - **Playhead retrofit, all scrub clips everywhere.** Every clip now lives in one
110
+ `playheads` list walked by one rAF loop: lerp 0.18 per frame (`data-sc-lerp`,
111
+ clamped 0.02 to 1, 1.0 under reduced motion), 8ms/20ms deadband, seek coalescing,
112
+ offscreen clips skipped within 0.002. Replaces the act-only 0.16 loop. Act
113
+ behaviour is otherwise untouched.
114
+ - iOS priming: one muted play-pause across every clip on the first touch or
115
+ pointer down, so a seeked-but-never-played clip is not left blank.
116
+ - `ScrollCraft.instances` collects mount handles, so the harness can ask whether
117
+ the playhead has arrived instead of guessing with a timeout.
118
+ - Warns when a worldflight stage does not compute `position: fixed`, the same
119
+ silent failure as an unpinned act one level worse.
120
+ - Synced byte-identical to all eight build folders and `cmp`-verified.
121
+
122
+ **Engine: `engine/scrollcraft.css`**
123
+
124
+ - `.sc-world`, `.sc-world__seg`, `.sc-world__poster`, `.sc-world__copy`,
125
+ `.sc-world__scrim`, `.sc-world__spacer`, and `[data-sc-copy]` pre-paint state.
126
+ - `video[data-sc-scrub].sc-has-clip` lights a leg's clip, since a worldflight
127
+ clip has no ancestor act to light it through.
128
+ - Reduced motion drops the poster push-in and the copy drift.
129
+
130
+ **Harness: `scripts/shoot.mjs`**
131
+
132
+ - Detects `[data-sc-mode="worldflight"]` and switches sampling to the track:
133
+ per-leg fractions at the same density as per-act, plus four positions across
134
+ every seam.
135
+ - `settle()` now waits for the playhead to ARRIVE (|cur - target| < 0.002 and not
136
+ seeking) via `ScrollCraft.instances`, falling back to the old currentTime-goes-
137
+ quiet method. Without this every worldflight frame is shot mid-lerp and no run
138
+ is repeatable.
139
+ - Dead scroll for worldflight is no leg advancing `currentTime`, no crossfade
140
+ progress, and no copy-window opacity change across a gap of 0.12vh. Skipped
141
+ under reduced motion, where each leg holds one still frame by design.
142
+ - New findings: legs that never reach full opacity, and legs stuck on poster
143
+ (suppressed under reduced motion).
144
+ - Cue and contrast passes now cover `[data-sc-copy]` alongside `[data-sc-cue]`.
145
+ - The fixed-chrome hide before the contrast shot skips anything inside
146
+ `[data-sc-world]` or `[data-sc-world-copy]`: that stage IS the background
147
+ behind every line and that layer carries the scrim, so hiding them graded the
148
+ copy against the page ground and failed a page that looks fine.
149
+
150
+ **Docs**
151
+
152
+ - New `references/worldflight.md`: the mode, its attributes, the track maths,
153
+ the one-sided seam, the copy-window contract, the waypoint contract, the seam
154
+ law for assets (chain on start images only; connector frames via
155
+ `ffmpeg -sseof -0.15` from the ENCODED mp4), GOP 8 / GOP 4, the ±1.6vh
156
+ prefetch window, and a hard-rules table.
157
+ - `references/uniqueness.md` §2.4: the continuous-world grammar now REQUIRES
158
+ worldflight mode, and says why, quoting the verdict on the act-based attempt.
159
+ - `references/devices.md` §1: new "The playhead is lerped" section covering the
160
+ 0.18 rate, the deadband, seek coalescing and `data-sc-lerp`.
161
+
162
+ **Verification**
163
+
164
+ - New rig at `OtherWorlds/Ultimate Websites/lab/worldflight-rig/` (3 descent legs,
165
+ hero/mid/finale copy, page-drawn route rail) on port 4520, with
166
+ `lab/worldflight-assert.mjs`: 24/24 assertions pass.
167
+ - `shoot.mjs` green on the rig desktop and reduced motion; no dead scroll, all
168
+ three legs reach full opacity and paint a real frame, all copy clears 4.5:1.
169
+ - Regression on four act builds (perkform 4500, nateherk 4501, vesper-v2 4504,
170
+ maison 4507): all green, frame counts and poster counts identical to the
171
+ pre-change baselines, zero console errors, zero weak cues.
172
+
173
+ ## 2026-08-21: consolidation after the showcase trio (descent, airfield, maison)
174
+
175
+ **Engine: `engine/scrollcraft.js`**
176
+
177
+ - `focusin` now centres a focused control that sits inside a `[data-sc-act]` when
178
+ its own cue computes under 0.85, with `behavior: 'instant'` because
179
+ `scrollcraft.css` sets `scroll-behavior: smooth` and the default animated a
180
+ multi-screen glide with focus off screen throughout. Promoted from airfield's
181
+ page-local handler and guarded to the act stack. Verified: fixes the `flow`
182
+ case; does **not** fix a pinned act, where the sticky stage means centring
183
+ scrolls backwards out of the act and leaves the cue dark. That residue is
184
+ documented in `verify.md` rather than fixed, because the correct scroll target
185
+ needs an inverse of `dwell()`. (airfield, with the limit measured on descent's
186
+ reported case)
187
+ - Synced byte-identical to all eight build folders.
188
+
189
+ **Harness: `scripts/shoot.mjs`**
190
+
191
+ - Null guard on the clip's owning act: `(v.closest("[data-sc-act]") ?? v)`. A
192
+ `video[data-sc-scrub]` outside the act stack, which is the legitimate shape for
193
+ a continuous world, threw a `TypeError` and took the whole run down before it
194
+ wrote anything. Confirmed the old expression throws and the new one returns a
195
+ boolean. (descent)
196
+
197
+ **Encoding: `scripts/encode.sh`**
198
+
199
+ - CRF is now overridable: fourth positional argument, or `SCROLLCRAFT_CRF`,
200
+ positional winning. Defaults unchanged at 20 desktop / 24 mobile, and the
201
+ echoed summary reports the value used. `assets.md` recommends 22-23 for
202
+ grain-heavy worlds, which previously meant not using the script. (descent)
203
+
204
+ **Docs: `references/devices.md`**
205
+
206
+ - §2 `pin`: minimum useful span is ~1.2. At span ≤ 1 a pinned act has one pixel
207
+ of travel, so progress jumps 0 to 1 and every cue and reveal inside it snaps.
208
+ (maison)
209
+ - §4 `reveal`: `clip-path` is relative to the border box, so a reveal on type
210
+ with `line-height` below 1 eats the ascenders and descenders and renders a
211
+ figure as a plain bar. Give it room or wrap it. (maison)
212
+ - §10 `drift`: documented the scoping rule. Drift belongs to the first act whose
213
+ progress is strictly between 0 and 1, so on a page of short acts several
214
+ qualify at once and the ground lags a full section. Paint grounds per section
215
+ instead, which is what a cutlist or chaptered page wants anyway. (airfield,
216
+ with maison's §2.2 conflict resolved the same way)
217
+
218
+ **Docs: `references/taste.md`**
219
+
220
+ - Colour: redefining `--sc-ink` on a subtree does not re-ink text whose `color`
221
+ already computed on `<body>`. Restate `color` on the subtree. (airfield)
222
+ - Colour: the "lock the accent" rule gains its exception. A page that hard-cuts
223
+ between light and dark grounds may carry a two-stop accent, one hue at two
224
+ lightnesses keyed to the ground. Still one accent per ground. (maison)
225
+ - Text over media: `width`/`height` HTML attributes are presentational hints and
226
+ come in pairs. Overriding only one in CSS lets the other resolve to the
227
+ attribute's pixel value. The reference template ships both, so this is a trap
228
+ the template hands to every build. (maison)
229
+
230
+ **Docs: `references/uniqueness.md`**
231
+
232
+ - §2.8 rhythmic cutlist: named the peak collision. The grammar bans `pin` and
233
+ `dwell` while feel.md demands the peak hold. Resolution is to hold in the fixed
234
+ chrome layer and keep every act short and unpinned, generalised to "move the
235
+ peak out of the act stack rather than breaking the grammar". (airfield)
236
+
237
+ **Docs: `references/verify.md`**
238
+
239
+ - Cue fade-outs should land between harness sample positions, or the sheet shows
240
+ half-faded type that nothing grades. Fix `rampOut`, not the sampling. (descent)
241
+ - Keyboard section rewritten to say exactly what the engine's `focusin` handler
242
+ covers and what it does not, with the pinned-act measurement.
243
+ - Six new rows in the failure table for the findings above.
244
+ - Credit accounting: per-call sums overstate real spend, same reason the probe
245
+ delta overstates in the other direction.
246
+
247
+ **Docs: `references/assets.md`**
248
+
249
+ - seedream's aspect ratios: `16:9`, `9:16` and `3:4` verified working, `4:5`
250
+ rejected with an error that does not list the alternatives. Returned pixel
251
+ dimensions are near the ratio, not exact. (maison)
252
+ - Unit costs reframed as published planning rates. Two ledger reconciliations put
253
+ actual debits at roughly 0.4x the per-call sum (1447 vs 530, 2252 vs 856), so
254
+ every budget cap in the skill is conservative and a per-call sum must not be
255
+ reported as measured spend. (orchestrator's ledger reconciliation)
256
+
257
+ **Verification**
258
+
259
+ Desktop and reduced-motion harness passes re-run on descent (4505), airfield
260
+ (4506) and maison (4507) after the engine sync. All six green: no dead scroll,
261
+ all cues clear 4.5:1 at their worst frame, no console errors, no failed
262
+ requests, and every clip reports `poster` under reduced motion.
263
+
264
+ ## 2026-08-21: triage of three parallel builds (nateherk, agency, saas)
265
+
266
+ **Engine: `engine/scrollcraft.js`**
267
+
268
+ - `data-sc-count` strips thousands separators before parsing, so a target can be
269
+ written the way it should render (`"0 3,500"`). Previously any real figure
270
+ between 1,000 and 9,999 was unrenderable. (nateherk, merged from its local patch)
271
+
272
+ **Engine: `engine/scrollcraft.css`**
273
+
274
+ - Reduced motion no longer zeroes a `pan` rail into missing content. The stage
275
+ becomes a native `overflow-x: auto` scroll region with proximity snapping, so
276
+ every item stays reachable without motion. (agency and nateherk both hit it;
277
+ perkform and saas both had the hole)
278
+ - `.sc-scrim--trail` switches to a bottom band below 860px, where `.sc-copy--trail`
279
+ re-anchors left and spans the full width. It was darkening the corner the copy
280
+ had just left, which guaranteed a mobile contrast failure over any bright clip.
281
+ (saas)
282
+ - New `.sc-scrim--band` utility: a bottom band, transparent above 58%, for copy
283
+ that spans the frame. (saas)
284
+
285
+ **Harness: `scripts/shoot.mjs`**
286
+
287
+ - Cue opacity reads a kinetic heading through its `.sc-split__i` line units. The
288
+ engine forces the element to 1, so kinetic headlines were reported as peaked on
289
+ frames where every line was at 0. (agency)
290
+ - Contrast is direction-aware: the ink is compared to the mean background and
291
+ graded against the darkest patch when it is dark type, the brightest when it is
292
+ light type. A high-key page was being graded against its most lenient possible
293
+ reading. Ported from the working checker nateherk shipped at
294
+ `lab/min-contrast.mjs`, along with its two corrections: the sampled rect is
295
+ clamped to the viewport, and `position: fixed` chrome is hidden with the text
296
+ because it paints in front of what scrolls under it. (nateherk)
297
+
298
+ **Docs**
299
+
300
+ - `devices.md` §2: only the last act may use a one-value hold cue; a middle act's
301
+ hold stays lit through the un-pin slide and crosses the header. (saas) Plus the
302
+ "ground or greet" rule for any pinned act (agency) and the `"0 1 0 0"` greet-and-hold
303
+ form (agency).
304
+ - `devices.md` §3: reduced-motion behaviour of `pan`, and the rail's copy
305
+ constraint (headings are read cropped). (agency, saas)
306
+ - `devices.md` §6: `data-sc-parallax` documented in its real unit, hundreds of
307
+ pixels rather than viewport fractions, with usable ranges. (agency)
308
+ - `devices.md` §7: `count` is unavailable to concept, fictional and pre-launch
309
+ brands, because every number would be invented. (saas)
310
+ - `devices.md` §8: a flow section after a pinned act takes reduced padding. (saas)
311
+ - `devices.md` §9 and `references/template.html`: `magnet`, `parallax` and `cue`
312
+ all write `transform` and cannot share an element; `data-sc-rise="0"` is the
313
+ guard. The template taught the collision by example. (nateherk)
314
+ - `assets.md`: a section on real supplied footage (measure with `signalstats`,
315
+ expand levels in the pre-encode intermediate, trim before anyone walks into
316
+ frame, target 24-30fps, never stream-copy). (nateherk)
317
+ - `assets.md`: portrait `<picture>` poster-swap pattern and the hand-written
318
+ ffmpeg portrait/crop recipe, since `encode.sh` has no portrait mode. (agency)
319
+ - `assets.md`: grain-heavy worlds run about double the size estimate; measured
320
+ kie.ai unit costs (seedream 5-pro still 28, kling v2-1-pro 5s 160). (agency, saas)
321
+ - `worlds.md`: what inverts when the canvas is light (scrim direction, ink over
322
+ media, `--sc-edge`, shadow alphas). (nateherk) Naming the empty space is now
323
+ stated as a per-shot requirement. (saas)
324
+ - `taste.md`: the positive case behind "no full-frame overlay" (corner, band,
325
+ column, and masking the image away from the text), and stepping the hero
326
+ display size down below ~700px. (agency, nateherk, saas)
327
+ - `verify.md`: how the contrast pass now works, its four known limitations,
328
+ checking that reduced motion did not delete content, and the fact that probe
329
+ deltas cannot attribute credit spend when builds run in parallel.
330
+
331
+ **Deferred, deliberately**
332
+
333
+ - `kie.mjs probe --since <baseline>` and per-call cost logging. Documented in
334
+ `assets.md` and `verify.md` instead.
335
+ - A `portrait` mode for `encode.sh`. The ffmpeg recipe is in `assets.md`.
336
+ - Keying harness cue rows by act and element rather than by text, and the 0.85
337
+ opacity gate on the contrast pass. Both are real, neither blocked a build;
338
+ written down in `verify.md` as known limitations.
339
+ - Lowering the `--sc-t-4xl` floor. It is a token every build inherits, and the
340
+ fix belongs in a page's own phone media query.
341
+
342
+ ## 2026-08-21, later the same day
343
+
344
+ - Reduced motion now settles `data-sc-reveal` wipes (`clip-path: none`), the
345
+ same floor the block already applied to parallax, pan and kinetic type. Found
346
+ by the nateherk build; re-verified green on the three reveal-using builds
347
+ (agency, nateherk, vesper-v2).
348
+ - `references/uniqueness.md` added: the structure axis. Eight page grammars, a
349
+ mandatory Step 0 interview, a required per-build signature move, and the
350
+ fingerprint gate against `Ultimate Websites/FINGERPRINTS.md`. Driven by the
351
+ owner's verdict that four builds shared one skeleton.
352
+ - Live-surface honesty rule: the labelled-sample-data escape is spelled out
353
+ (vesper-v2's route to the grammar for a concept product).
354
+ - Ground-or-greet extended to any progress-gated content on a pinned act, not
355
+ just engine cues.
356
+ - verify.md: two new measured failures (reduced-motion rail snap-centring a
357
+ single wide track; keyboard focus landing off-screen on pinned acts) and the
358
+ note that a shoot.mjs run can take the background server down with it.
359
+
360
+ ## 2026-08-22: the `orrery` build (travel, continuous world)
361
+
362
+ First run of the skill start-to-finish as a new user would meet it: interview
363
+ first, nothing generated until the eight answers existed. Three findings, all
364
+ of them things that cost a full iteration to discover.
365
+
366
+ - **A scrim parented to the copy block is invisible to the contrast pass.**
367
+ `shoot.mjs` hides `[data-sc-copy],[data-sc-copy] *` to photograph the film
368
+ underneath a line, and `visibility: hidden` takes an element's `::before` with
369
+ it. A per-block scrim written as `.copy::before` is therefore never composited
370
+ into the measurement: six blocks reported 1.2-1.9:1 against means of 7-13:1,
371
+ and **strengthening the scrim changed the reported numbers by exactly zero**,
372
+ which is the tell. The identical-numbers-after-a-real-change signature is
373
+ worth knowing on its own. Fix: the scrim must be a **sibling** of the copy
374
+ block. `orrery` mounts one soft plate per block into the copy layer and drives
375
+ its opacity from the block's own inline opacity, so it tracks the engine's
376
+ window for free. Written up in taste.md and verify.md.
377
+ - **The worldflight spacer is sized once, inside mount, and a viewport that
378
+ reports height 0 at that moment leaves the page with no scroll track at all.**
379
+ The engine writes `height: 0px`, every mechanical check still passes, and the
380
+ page looks like a frozen still. Seen in an embedded preview pane; a single
381
+ dispatched `resize` corrected it to the exact expected 10.5vh. Page-level fix
382
+ (the engine is not edited): dispatch a resize on `load` and on
383
+ `document.fonts.ready`. That second one also absorbs the reflow when a webfont
384
+ swaps in, which changes every copy block's measured height.
385
+ - **`encode.sh` finds a full ffmpeg, but the poster step in `assets.md` calls
386
+ bare `ffmpeg`.** On a machine whose PATH ffmpeg is a stripped build, the
387
+ documented `-frames:v 1 ... poster.webp` fails with "Unable to choose an
388
+ output format", which reads like a filename problem rather than a missing
389
+ muxer. Use the same resolved binary `encode.sh` uses. Also: PSNR seam checks
390
+ need that binary, since the stripped build has no `psnr` filter.
391
+
392
+ Build notes worth carrying: the owner asked for a "tiny world" that does not
393
+ look cheap, which is one word away from the banned clay diorama. Resolving it as
394
+ a **handmade physical scale model** shot macro (brass, painted plaster, real
395
+ moss, poured resin, sifted sand, visible glue seams) satisfied both, and the
396
+ negative list in the preamble did the work. Ten legs chained head-to-tail on
397
+ pre-generated anchors rather than sequentially off encoded tails: every joint
398
+ came back 28.5-39.8 dB, inside the band `descent` shipped at, and all ten clips
399
+ generated **in parallel** instead of in a 45-minute serial chain.
400
+
401
+ Also confirmed the harness will happily photograph a **different site** if the
402
+ port is already occupied by another build's server. `serve.mjs` exits
403
+ EADDRINUSE in the background while `shoot.mjs` gets a clean 200 from the
404
+ squatter and produces a full, plausible, entirely wrong contact sheet. Check the
405
+ served `<title>` before trusting a run.
406
+
407
+ ### Pacing pass, same day (owner note on `orrery`)
408
+
409
+ > "when you're building fly-through worlds like this, and most of the times when
410
+ > you're doing things with scroll, you need to slow it down a bit... it needs to
411
+ > feel smoother and consistent."
412
+
413
+ Two faults hide under "not smooth", and the skill only had guidance for one.
414
+
415
+ - **Inconsistent pace.** `orrery` shipped its first cut with leg weights from
416
+ 0.70 to 0.95vh for identical 5-second clips, a 36% spread in how fast the
417
+ world moved per pixel. Nothing in the skill said to hold `weight / clip
418
+ seconds` constant, so nothing caught it. Now a hard rule in worldflight.md,
419
+ with `orrery` normalised to a 6% spread.
420
+ - **Too fast.** The `~1.5vh per 8s` line was being read as a target when it is a
421
+ dead-scroll guardrail. It yields 0.14-0.19vh per second of film, which is
422
+ brisk for a page the reader is steering. worldflight.md §7c now names
423
+ **0.21-0.22vh/s as the floor for a fly-through** and says plainly that
424
+ exceeding the guardrail is fine as long as the harness confirms no dead
425
+ scroll. It did, at both viewport sizes.
426
+ - **`data-sc-lerp` 0.18 is the wrong default for this mode.** 0.12 is now the
427
+ documented worldflight value, alongside a wider `data-sc-seam` (~0.16). The
428
+ damping is what removes wheel judder; the weights are what remove the surging.
429
+
430
+ feel.md §5 gained the exception it was missing: pacing variety is how an act
431
+ page carries feeling, and a continuous world is the one grammar where it is
432
+ wrong. There the peak carries the shape by being the single long leg.
433
+
434
+ Also recorded: changing any weight moves every leg boundary, so every
435
+ `data-sc-window` must be recomputed and then re-checked against the frame it was
436
+ tuned to. `orrery`'s labels were re-verified on screen at Kyoto and Erg Chebbi
437
+ after the repace.
438
+
439
+ ---
440
+
441
+ ## Clip time is not cue time (the frozen-clip class)
442
+
443
+ Reported from a real read of `agency` / Fallowbank: "scenes are moving with a
444
+ scroll, but then they stop and then the whole site starts to move, so now we're
445
+ seeing an image that's still."
446
+
447
+ A pinned stage is on screen for one viewport **before** its pinned travel starts
448
+ and one viewport **after** it ends. Act progress `p` is 0 across the whole entry
449
+ slide and 1 across the whole exit slide, so a clip driven by `p` is parked on its
450
+ first frame while the stage slides in and on its last frame while it slides out.
451
+ The reader scrubs a film, the film stops, and the page then slides a still
452
+ photograph past them.
453
+
454
+ **What changed**
455
+
456
+ - **The engine now maps clip time across the stage's entire visible life by
457
+ default**, with both ends clamped to scroll that actually exists so a hero at
458
+ document top still starts on frame one and an act near the bottom still reaches
459
+ its last frame. Cues still use `p`, because cues belong to the pin.
460
+ `data-sc-clip-map="travel"` is the opt-out. The old `data-sc-runout` opt-in is
461
+ accepted and ignored: it addressed only the exit half, and it was opt-in, which
462
+ is why three of four builds carrying a scrub act shipped frozen anyway.
463
+ - **`shoot.mjs` samples each scrub act's entry and exit slides.** It previously
464
+ sampled pinned acts only at `top + (h - vh) * p`, which never visits either
465
+ slide. That sampling gap is the whole reason this survived four builds of
466
+ verification: the defect lived precisely where the harness never looked.
467
+ - **New FROZEN CLIP finding**, graded against stage visibility rather than act
468
+ progress. First/last-frame holds always report; mid-clip holds only report once
469
+ they outlast a plausible `dwell` settle. Skipped under reduced motion.
470
+
471
+ **Validated, not assumed.** The detector was proven to discriminate before being
472
+ trusted: it fires on a clip deliberately opted out with `data-sc-clip-map="travel"`
473
+ and stays silent on the clip beside it in the same page.
474
+
475
+ **Do not pair this with a shorter dwell.** Dwell moves fast at the edges and
476
+ settles in the middle, which is exactly the shape the new mapping wants: fast
477
+ motion on the two slides, the settle inside the pin where the copy lands.
478
+
479
+ **Also found, unrelated and pre-existing:** `nateherk`'s hero paragraph fails
480
+ contrast from `y=0` (1.01:1, dark ink over dark foliage in the sky clip). The new
481
+ slide sampling surfaced it; the old engine fails it identically, so it is not a
482
+ regression from this change.
483
+
484
+ ## 2026-08-22, portability pass (v0.2.0)
485
+
486
+ The skill assumed one person's repo layout. Five references pointed at
487
+ `OtherWorlds/Ultimate Websites/`, so the fingerprint gate and the worked
488
+ worldflight rig dead-ended on every other machine. Packaging it as a plugin made
489
+ that a real defect rather than a note.
490
+
491
+ The fix is **not** a second templatised copy of the skill. Two copies drift, and
492
+ this one proved it inside a day: five files diverged between the working skill
493
+ and its published copy within an hour of the first push. There is one skill, and
494
+ it resolves its paths instead of assuming them.
495
+
496
+ - **`scripts/workspace.mjs`.** Resolves the workspace that holds builds and the
497
+ registry: `SCROLLCRAFT_HOME`, then the nearest `.scrollcraft.json` walking up,
498
+ then `<project root>/scrollcraft`. `--ensure` creates it and seeds an empty
499
+ registry. Builds are `<workspace>/builds/<name>/`, the registry is
500
+ `<workspace>/FINGERPRINTS.md`, and neither is hardcoded anywhere any more.
501
+ - **The registry is per-user and starts empty.** The gate exists to stop you
502
+ repeating *yourself*, so a new user's first build has nothing to clear and
503
+ every build after it does. Gating a newcomer against somebody else's twelve
504
+ rows would block them out of grammars they have never used, which is the
505
+ opposite of the point. `templates/FINGERPRINTS.md` is the seed; the author's
506
+ twelve-row table ships separately as `EXAMPLES.md`, explicitly as
507
+ illustration rather than constraint.
508
+ - **`scripts/doctor.mjs`.** Preflight, run before the interview. Checks node, a
509
+ full ffmpeg build, playwright, Chrome, the API key and the resolved workspace,
510
+ and separates required failures from optional ones. It exists because the
511
+ three most common setup faults all surface later as misleading errors: a
512
+ stripped ffmpeg reports a missing filter as a syntax error, a missing webp
513
+ muxer reports as a bad filename, and playwright resolves from the wrong
514
+ directory. On the author's machine it correctly picks the 585-filter build
515
+ over the stripped one first on PATH.
516
+ - **`scripts/worldflight-assert.mjs` now ships with the skill.** It was
517
+ URL-driven and generic all along, sitting in a lab folder nobody else had.
518
+ Run it against any worldflight page before the contact sheet.
519
+ - **The API key is documented as optional in the right place.** Generation costs
520
+ real money; a build from the user's own photos and footage needs no key and no
521
+ spend, and that is now stated as a first-class route in Bootstrap rather than
522
+ implied.
523
+
524
+ Nothing moved on the author's machine: a two-line `.scrollcraft.json` at the
525
+ project root points the workspace at the existing `OtherWorlds/Ultimate
526
+ Websites`, so twelve builds and the live registry resolve exactly as before.