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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/luckiest-design-website/ATTRIBUTION.md +38 -0
- package/skills/luckiest-design-website/CHANGELOG.md +104 -0
- package/skills/luckiest-design-website/LICENSE +21 -0
- package/skills/luckiest-design-website/SKILL.md +372 -0
- package/skills/luckiest-design-website/UPSTREAM-CHANGELOG.md +526 -0
- package/skills/luckiest-design-website/engine/scrollcraft.css +432 -0
- package/skills/luckiest-design-website/engine/scrollcraft.js +1167 -0
- package/skills/luckiest-design-website/evals/evals.json +33 -0
- package/skills/luckiest-design-website/references/assets.md +286 -0
- package/skills/luckiest-design-website/references/device-diag.html +214 -0
- package/skills/luckiest-design-website/references/devices.md +466 -0
- package/skills/luckiest-design-website/references/feel.md +277 -0
- package/skills/luckiest-design-website/references/registry-examples.md +305 -0
- package/skills/luckiest-design-website/references/taste.md +304 -0
- package/skills/luckiest-design-website/references/template.html +138 -0
- package/skills/luckiest-design-website/references/uniqueness.md +480 -0
- package/skills/luckiest-design-website/references/verify.md +381 -0
- package/skills/luckiest-design-website/references/worldflight.md +349 -0
- package/skills/luckiest-design-website/references/worlds.md +178 -0
- package/skills/luckiest-design-website/scripts/doctor.mjs +177 -0
- package/skills/luckiest-design-website/scripts/encode.sh +80 -0
- package/skills/luckiest-design-website/scripts/kie.mjs +202 -0
- package/skills/luckiest-design-website/scripts/serve.mjs +52 -0
- package/skills/luckiest-design-website/scripts/shoot.mjs +644 -0
- package/skills/luckiest-design-website/scripts/workspace.mjs +106 -0
- package/skills/luckiest-design-website/scripts/worldflight-assert.mjs +273 -0
- package/skills/luckiest-design-website/templates/FINGERPRINTS.md +65 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# Verify
|
|
2
|
+
|
|
3
|
+
A scroll page cannot be checked by looking at it. It has no single state: every
|
|
4
|
+
scroll position is a different frame, and the failures live between the two you
|
|
5
|
+
happened to look at. So walk it mechanically.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd <build project>
|
|
9
|
+
npm i playwright-core # once
|
|
10
|
+
|
|
11
|
+
node <skill>/scripts/serve.mjs --root . --port 4500 &
|
|
12
|
+
node <skill>/scripts/shoot.mjs --url http://localhost:4500 --out lab/shots
|
|
13
|
+
node <skill>/scripts/shoot.mjs --url http://localhost:4500 --out lab/mobile --width 390 --height 844
|
|
14
|
+
node <skill>/scripts/shoot.mjs --url http://localhost:4500 --out lab/reduced --reduced-motion
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then **read `sheet.png`**. The whole point of shooting contiguously is looking
|
|
18
|
+
at the frames side by side; a folder of PNGs does not get looked at that way.
|
|
19
|
+
|
|
20
|
+
Two setup facts that will otherwise waste a pass:
|
|
21
|
+
|
|
22
|
+
- **Serve it.** `file://` blocks the Blob fetch the engine uses for clips, so
|
|
23
|
+
the page silently falls back to posters and proves nothing.
|
|
24
|
+
- **Real Chrome, not bundled Chromium.** Chromium ships without an h264
|
|
25
|
+
decoder, so every clip fails to paint and the run "passes" against posters.
|
|
26
|
+
`shoot.mjs` already resolves installed Chrome; override with
|
|
27
|
+
`SCROLLCRAFT_CHROME`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## What the harness reports
|
|
32
|
+
|
|
33
|
+
It samples **within each act** (default 6 positions per act) rather than
|
|
34
|
+
uniformly down the document. Uniform sampling moves every position whenever you
|
|
35
|
+
change any section's height, so findings appear and vanish with unrelated edits.
|
|
36
|
+
|
|
37
|
+
**DEAD SCROLL**: consecutive positions where nothing changed: no cue moved, no
|
|
38
|
+
clip time advanced, no rail travelled, no wipe progressed, no stage shifted.
|
|
39
|
+
Real dead scroll means the reader is turning the wheel and being given nothing.
|
|
40
|
+
Fix by shortening the act's span or adding a cue.
|
|
41
|
+
|
|
42
|
+
**Bespoke fixed stages must report their visible state.** A split stage, live
|
|
43
|
+
canvas, or other page-local system can use ordinary `flow` acts only as scroll
|
|
44
|
+
markers while every visible change happens on a fixed layer outside the engine.
|
|
45
|
+
The harness cannot infer that layer's semantics. Put `data-sc-verify-state` on
|
|
46
|
+
the fixed stage and update its value to a compact signature of the values that
|
|
47
|
+
actually paint: divider position, scene opacity, canvas phase, custom film
|
|
48
|
+
time, or similar. The detector then checks those flow spans too.
|
|
49
|
+
|
|
50
|
+
Do not publish raw scroll progress just to make the check green. If progress is
|
|
51
|
+
changing while the composition is not, that is the exact failure this path is
|
|
52
|
+
meant to catch. Round and publish the rendered values. For an intentional
|
|
53
|
+
resolved hold, set `data-sc-verify-hold="true"` only while the hold is active.
|
|
54
|
+
Reduced-motion fixed stages may use the same attribute for deliberately stable
|
|
55
|
+
frames, which still require manual contact-sheet review.
|
|
56
|
+
|
|
57
|
+
**FROZEN CLIP**: a scrub stage is on screen, the reader is scrolling, and the
|
|
58
|
+
clip's playhead is not moving. Dead scroll cannot see this, because the stage
|
|
59
|
+
itself *is* moving: a still photograph is sliding up the page, which is the
|
|
60
|
+
worst-looking failure this kit can produce and the one that most reliably makes
|
|
61
|
+
a page feel broken.
|
|
62
|
+
|
|
63
|
+
The harness samples each scrub act's **entry and exit slides**, not only its
|
|
64
|
+
pinned travel. That gap is why this went undetected for four builds: a pinned
|
|
65
|
+
act's samples were taken at `top + (h - vh) * p`, which never visits the viewport
|
|
66
|
+
of scroll on either side where the stage is visible and the clip is parked. A
|
|
67
|
+
hold on the first or last frame is always reported. A hold in the middle is only
|
|
68
|
+
reported once it outlasts any plausible `data-sc-dwell` settle, since that settle
|
|
69
|
+
is a deliberate effect. The check is skipped under reduced motion, where no clip
|
|
70
|
+
is ever fetched on purpose.
|
|
71
|
+
|
|
72
|
+
The fix is almost never per-page: the engine maps clip time across the stage's
|
|
73
|
+
whole visible life by default. A page that reports this has usually opted out
|
|
74
|
+
with `data-sc-clip-map="travel"`, or is running an engine copy from before that
|
|
75
|
+
default existed. See [devices.md §1](devices.md).
|
|
76
|
+
|
|
77
|
+
**CUES THAT NEVER PEAK**: an element that never reaches full opacity anywhere.
|
|
78
|
+
Usually a cue window too narrow for its act, or ramps that eat the whole window.
|
|
79
|
+
Widen the window or set explicit ramps. A kinetic heading is read through its
|
|
80
|
+
line units, not through the element: the engine forces the element itself to
|
|
81
|
+
opacity 1 and carries the real value on `.sc-split__i`, so reading the element
|
|
82
|
+
reports every kinetic headline as fully present even on frames where every line
|
|
83
|
+
is at 0.
|
|
84
|
+
|
|
85
|
+
**CONTRAST**: measured on the **composited page**, not on the source video. The
|
|
86
|
+
harness hides the text, re-shoots the same frame, and samples the real
|
|
87
|
+
background under each line, so scrims, gradients and blends are all included.
|
|
88
|
+
Elements with their own opaque background are graded against that fill instead.
|
|
89
|
+
|
|
90
|
+
Three things it gets right that a hand-rolled version usually does not:
|
|
91
|
+
|
|
92
|
+
- **The direction is picked per line.** Light type on a dark page fails on the
|
|
93
|
+
brightest patch under it; dark type on a light page fails on the *darkest*
|
|
94
|
+
one, and grading that against the brightest patch is the most lenient reading
|
|
95
|
+
available, so a high-key page can report clean over text that is failing. The
|
|
96
|
+
harness compares the ink to the mean background and grades against whichever
|
|
97
|
+
extreme is on the ink's own side.
|
|
98
|
+
- **The sampled rect is clamped to the viewport.** The part of a pinned act's
|
|
99
|
+
copy that has scrolled above the fold is not on screen, so what sits in those
|
|
100
|
+
pixels is not behind anything the reader can see.
|
|
101
|
+
- **Fixed chrome is hidden with the text.** A fixed bar paints in *front* of
|
|
102
|
+
what scrolls under it, so its own mark is not the background behind a headline
|
|
103
|
+
passing beneath it.
|
|
104
|
+
|
|
105
|
+
This is the check no static audit can do: the frame under a headline changes as
|
|
106
|
+
the clip scrubs, so text can clear 4.5:1 against the poster and fail badly three
|
|
107
|
+
hundred pixels later.
|
|
108
|
+
|
|
109
|
+
### The scrim has to be a SIBLING of the copy, never a child
|
|
110
|
+
|
|
111
|
+
The pass hides `[data-sc-cue],[data-sc-cue] *,[data-sc-copy],[data-sc-copy] *`
|
|
112
|
+
before photographing the frame underneath a line. `visibility: hidden` hides an
|
|
113
|
+
element's pseudo-elements too, so a scrim written as `.mycopy::before` is hidden
|
|
114
|
+
along with the text it exists to protect, and the pass grades the line against
|
|
115
|
+
the raw film every time.
|
|
116
|
+
|
|
117
|
+
The tell is unmistakable once you know it: **you strengthen the scrim and the
|
|
118
|
+
reported numbers do not move at all.** Not "improve slightly", not "move by a
|
|
119
|
+
tenth": byte-identical, because the thing you changed was never in the
|
|
120
|
+
measurement. If a contrast number is unchanged to two decimals after a real
|
|
121
|
+
change, stop tuning and check what is actually being composited.
|
|
122
|
+
|
|
123
|
+
A very high mean against a very low worst (`1.21:1 (mean 12.83)`) is the same
|
|
124
|
+
finding seen from the other side: the type is fine almost everywhere and there
|
|
125
|
+
is a bright patch under it that nothing is covering.
|
|
126
|
+
|
|
127
|
+
Two shapes that work:
|
|
128
|
+
|
|
129
|
+
- `.sc-world__scrim` in the copy layer, which is what worldflight.md ships and
|
|
130
|
+
which survives the hide because it carries no `data-sc-copy`.
|
|
131
|
+
- One plate per block, mounted as a sibling and driven from the page's own JS.
|
|
132
|
+
`orrery` sizes each plate off its block's untransformed box (set
|
|
133
|
+
`transform:'none'`, read the rect, put it back, so the engine's ±2vh copy
|
|
134
|
+
drift does not skew the measurement) and each frame copies the block's own
|
|
135
|
+
inline opacity onto its plate, so the plate tracks the engine's window with no
|
|
136
|
+
duplicated window maths.
|
|
137
|
+
|
|
138
|
+
### Known limitations of the contrast pass
|
|
139
|
+
|
|
140
|
+
Real, and worth knowing before you trust a green run:
|
|
141
|
+
|
|
142
|
+
- **Cues are keyed by their text.** Two cues that share a string, which
|
|
143
|
+
taste.md's "one label per intent" rule actively encourages, are collapsed into
|
|
144
|
+
one row, and the reported worst frame is the worse of the two.
|
|
145
|
+
- **Lines under 0.85 opacity are skipped.** A headline parked at 0.6 over a
|
|
146
|
+
bright frame is never graded, so "contrast clean" can still hide a legibility
|
|
147
|
+
problem. Look at the sheet for anything that reads washed out.
|
|
148
|
+
|
|
149
|
+
**Author the fade-outs to land between sample positions.** The harness samples
|
|
150
|
+
a fixed number of positions per act, so a ramp-out that happens to straddle one
|
|
151
|
+
puts a half-faded headline on the sheet: graded by nobody, and read by eye as
|
|
152
|
+
ghost type over the frame. **Fix the ramp, not the sampling.** Shorten
|
|
153
|
+
`rampOut` so the cue is at full opacity at one sample and gone by the next,
|
|
154
|
+
rather than sitting at 0.5 on the sample in between. Widening the sample count
|
|
155
|
+
only finds more half-faded frames; it does not make the page look better,
|
|
156
|
+
because a real reader stopping on that pixel sees exactly what the sheet
|
|
157
|
+
shows. A cue caught mid-fade over a bright frame is a real defect, not a
|
|
158
|
+
sampling artefact.
|
|
159
|
+
- **The floor is not size-aware.** It reports below 3:1 as a failure and 3:1 to
|
|
160
|
+
4.5:1 as thin. WCAG allows 3:1 for large text, so a display headline in the
|
|
161
|
+
thin band is usually fine and a 16px caption in it is not.
|
|
162
|
+
- **Acts with no `[data-sc-cue]` elements are not graded at all.** Copy on plain
|
|
163
|
+
canvas is a static case, but it is unmeasured.
|
|
164
|
+
- **Ordinary `flow` acts are excluded from dead-scroll checks.** Static flow is
|
|
165
|
+
normally correct. A bespoke fixed experience built over flow markers must use
|
|
166
|
+
`data-sc-verify-state`, or the harness will skip its visible timeline and can
|
|
167
|
+
report a dead opening as healthy.
|
|
168
|
+
- **A `pan` act whose rail does not overflow is reported as healthy.** The
|
|
169
|
+
`pigment` build ran a rail measuring 1368px inside a 1440px viewport, so it
|
|
170
|
+
travelled zero for its entire 2.1vh span, and every pass printed `no dead
|
|
171
|
+
scroll detected`. Measure `rail.scrollWidth - innerWidth` yourself; a green run
|
|
172
|
+
does not cover it. See devices.md §3.
|
|
173
|
+
|
|
174
|
+
**Console errors and failed requests**: a 404 on a clip degrades to a poster
|
|
175
|
+
silently, which looks fine and is not.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## What the harness cannot tell you
|
|
180
|
+
|
|
181
|
+
Read the sheet for these. They are the ones that matter most.
|
|
182
|
+
|
|
183
|
+
- **Whether the composition is any good.** Copy landing on the busiest part of
|
|
184
|
+
the frame, a subject cropped at an unfortunate point, an act whose end frame
|
|
185
|
+
is a dark empty corner.
|
|
186
|
+
- **Whether the motion is smooth.** Contiguous frames prove the clip advances;
|
|
187
|
+
they do not prove it advances evenly. Watch the contact sheet for a move that
|
|
188
|
+
lurches, reverses, or stalls in the middle.
|
|
189
|
+
- **Whether the page means anything.** Six acts that each work and together say
|
|
190
|
+
nothing is the most expensive failure available here.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## The manual passes
|
|
195
|
+
|
|
196
|
+
**Reduced motion.** Clips are never fetched, posters hold, copy still cues. The
|
|
197
|
+
page must remain comprehensible, not merely not-crash. This doubles as the
|
|
198
|
+
low-bandwidth check.
|
|
199
|
+
|
|
200
|
+
Comprehensible includes **reachable**: check that no content was deleted rather
|
|
201
|
+
than merely stilled. A `pan` rail is the case that bites, because zeroing its
|
|
202
|
+
transform parks it on its first screenful. The engine now hands the stage back
|
|
203
|
+
as a native scroll region, so confirm on the sheet that the rail shows real
|
|
204
|
+
content and that items past the fold can still be got to. Nothing in the harness
|
|
205
|
+
reports this; it reads as a page behaving correctly.
|
|
206
|
+
|
|
207
|
+
**Credit accounting.** `kie.mjs probe` reports a balance, not a delta, so a
|
|
208
|
+
build's spend is a before-and-after subtraction. That subtraction is only valid
|
|
209
|
+
if nothing else is generating against the same key. When builds run in parallel,
|
|
210
|
+
or when a settlement lands late, the deltas overlap and each build will claim
|
|
211
|
+
some of another's spend (three parallel builds each read the same 7597 → 7067 and
|
|
212
|
+
each reported 530). Either serialise generation, or cost the build from the
|
|
213
|
+
per-call model prices in [assets.md](assets.md) against the calls you actually
|
|
214
|
+
made, and treat the probe delta as a ceiling.
|
|
215
|
+
|
|
216
|
+
**And the per-call sum overstates real spend in the other direction.** Two
|
|
217
|
+
reconciliations against the account ledger, each with no other consumer, put
|
|
218
|
+
actual debits at roughly **0.4x** the documented unit rates: a fleet whose
|
|
219
|
+
per-call sums came to ~1447 credits was debited 530, and a three-build run whose
|
|
220
|
+
per-call sums came to 2252 was debited 856. Both land near the same ratio. So a
|
|
221
|
+
build report should say what the per-call sum is *and* that it is a planning
|
|
222
|
+
ceiling rather than the amount billed. Reporting the sum as the cost is the
|
|
223
|
+
honest default, because it never under-claims; reporting it as *measured* spend
|
|
224
|
+
is wrong. Neither number is the other's substitute: the probe delta bounds a
|
|
225
|
+
parallel run from above, the per-call sum bounds a serial one from above, and
|
|
226
|
+
only a ledger read with a single consumer settles it.
|
|
227
|
+
|
|
228
|
+
**Mobile.** Pinned stages use `100svh` so the URL bar does not cause a jump.
|
|
229
|
+
Copy reflows and does not collide with the fixed bar. Confirm the phone encodes
|
|
230
|
+
actually load. Check the portrait crop of every clip: a 16:9 move composed
|
|
231
|
+
around left-hand negative space loses exactly that space at 9:16
|
|
232
|
+
(see [assets.md](assets.md)). Mobile is a first-class target, not a check at
|
|
233
|
+
the end: the phone clips are cut portrait, the lerp is retuned for touch, tap
|
|
234
|
+
targets are grown, and every one of those is authored, not inherited.
|
|
235
|
+
|
|
236
|
+
### The phone is a different machine
|
|
237
|
+
|
|
238
|
+
Headless Chrome on the build box cannot reproduce an iPhone's video decoder,
|
|
239
|
+
its autoplay policy, Low Power Mode, or touch scrolling. On one build every
|
|
240
|
+
probe reported the hero clip scrubbing perfectly for **four consecutive
|
|
241
|
+
rounds while the real phone showed a frozen frame**. A green harness run says
|
|
242
|
+
the page is correct where the harness runs. It says nothing about iOS video.
|
|
243
|
+
|
|
244
|
+
What iOS does to a scrub clip, and what the engine now handles for you:
|
|
245
|
+
|
|
246
|
+
- iOS will not *paint* a muted video that has never been played. Seeks land,
|
|
247
|
+
`seeked` fires, and the picture stays on one frame. The decoder has to be
|
|
248
|
+
primed with one `play()`/`pause()`.
|
|
249
|
+
- The engine primes each clip at `loadedmetadata` (a muted inline `play()`
|
|
250
|
+
needs no gesture outside Low Power Mode) and retries on `touchstart`,
|
|
251
|
+
`touchend`, `pointerdown`, `click` and `scroll`. `touchend` matters: the
|
|
252
|
+
HTML spec's activation-triggering events include `touchend` but **not**
|
|
253
|
+
`touchstart`, so a Low Power Mode phone that rejects the touchstart attempt
|
|
254
|
+
gets a valid one when the finger lifts.
|
|
255
|
+
- A prime must be re-attemptable per clip. A one-shot prime on first touch
|
|
256
|
+
loses a race: the reader touches to scroll within the first second, while
|
|
257
|
+
the hero's megabytes are still downloading, and the shot is spent on a
|
|
258
|
+
sourceless element. The tell is exactly "the first clip is frozen and every
|
|
259
|
+
later one works".
|
|
260
|
+
- iOS may leave a `play()` promise pending forever, and may leave `seeking`
|
|
261
|
+
true forever. Both were permanent silent freezes; the engine now releases
|
|
262
|
+
the priming flag on a timer and re-issues any seek stuck past 700ms. The
|
|
263
|
+
reveal also fires on a 2.5s timeout, never only on `seeked`.
|
|
264
|
+
|
|
265
|
+
Do not re-implement any of that in page JS, and do not strip it when copying
|
|
266
|
+
the engine. If a phone still shows a frozen clip, the cause is past what this
|
|
267
|
+
machine can measure, which is what the next section is for.
|
|
268
|
+
|
|
269
|
+
### Ship the diagnostic with the site
|
|
270
|
+
|
|
271
|
+
You get one question per round with a real device, so make the round count.
|
|
272
|
+
`references/device-diag.html` is a standalone page that scrubs the suspect
|
|
273
|
+
clip two ways (blob URL, exactly as the engine loads it, and direct file src)
|
|
274
|
+
beside a known-good clip, prints a MOVING / FROZEN verdict over each pane,
|
|
275
|
+
and reports prime results, seek counts and distinct painted frames. Edit its
|
|
276
|
+
`TESTS` array to point at the build's own clips, deploy it next to the site,
|
|
277
|
+
and one screenshot from the phone isolates the layer: blob loading, the file,
|
|
278
|
+
the device's decode policy, or the engine's lifecycle. Deploy it **with** the
|
|
279
|
+
first mobile fix, not after the fourth.
|
|
280
|
+
|
|
281
|
+
### Ask what differs before asking what's broken
|
|
282
|
+
|
|
283
|
+
The debugging lesson that cost three wasted rounds: "desktop works, the phone
|
|
284
|
+
does not" reads as a platform difference and invites platform theories
|
|
285
|
+
(codecs, keyframes, resolution). **"One clip works and another does not, on
|
|
286
|
+
the same device"** cannot be a platform difference. Before theorising, write
|
|
287
|
+
down every way the working case differs from the broken one; the bug lives in
|
|
288
|
+
that list. On the build above the list had one entry: the hero is first, so
|
|
289
|
+
it loads while the first touch is being spent.
|
|
290
|
+
|
|
291
|
+
**Keyboard.** Tab through. Focus order matches visual order, the focus ring is
|
|
292
|
+
visible against every ground it crosses, and nothing reachable is parked at
|
|
293
|
+
opacity 0. Cues set `pointer-events: none` when faded, but a focusable element
|
|
294
|
+
inside a faded cue is still a trap.
|
|
295
|
+
|
|
296
|
+
The engine helps here but does not finish the job, and the gap is specific:
|
|
297
|
+
|
|
298
|
+
- **It handles the ordinary case.** On `focusin`, if the focused element is
|
|
299
|
+
inside a `[data-sc-act]` and its own cue computes under 0.85, the engine
|
|
300
|
+
scrolls it to the centre of the viewport with `behavior: 'instant'`
|
|
301
|
+
(`smooth` would animate a multi-screen glide with focus off screen the whole
|
|
302
|
+
way). On a `flow` act, centring the element also opens its cue, because the
|
|
303
|
+
element's viewport position and the act's progress move together.
|
|
304
|
+
- **It does not fix a pinned act, and cannot with this approach.** A pinned
|
|
305
|
+
stage is `position: sticky`, so the control holds *one* viewport position for
|
|
306
|
+
the entire act. Centring it is then only achievable by scrolling backwards out
|
|
307
|
+
of the act, which parks progress at 0 and leaves the cue dark. Measured: a CTA
|
|
308
|
+
cued at 0.75 on a 3vh pinned act sits at viewport y=70 from progress 0 to
|
|
309
|
+
0.875; `scrollIntoView({block:'center'})` from inside the act lands *before*
|
|
310
|
+
the act's top, at progress 0, cue opacity 0. The control is on screen and
|
|
311
|
+
still invisible.
|
|
312
|
+
|
|
313
|
+
**On a pinned act, park the act at the progress where the focused element's own
|
|
314
|
+
cue is open.** That is page-local work, because only the page knows which cue
|
|
315
|
+
belongs to which control, and because act progress runs through `dwell()` when
|
|
316
|
+
the act has any, so the scroll target is not a straight inverse of the cue
|
|
317
|
+
window. The descent build does exactly this. If a pinned act carries a focusable
|
|
318
|
+
control, write that handler and assert it; do not assume the engine covered you.
|
|
319
|
+
|
|
320
|
+
**Fresh eyes.** Look again later. Timing you tuned for twenty minutes reads
|
|
321
|
+
differently when you have forgotten what it is supposed to do.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Failures worth knowing about
|
|
326
|
+
|
|
327
|
+
Each of these shipped once during this skill's own build, and each looked fine
|
|
328
|
+
until it was measured.
|
|
329
|
+
|
|
330
|
+
| Symptom | Cause |
|
|
331
|
+
|---|---|
|
|
332
|
+
| A hero headline wrapped to six lines | `max-width` in `ch` on a **container**: `ch` resolves against the container's font-size, not the display size of the heading inside it |
|
|
333
|
+
| Centred copy hanging off the left edge | `inset-inline` declared **after** `left: 50%`; the shorthand resets `left` to auto |
|
|
334
|
+
| An act that never pins, silently | An author rule setting `position` on the stage. The engine now warns in the console |
|
|
335
|
+
| A stray headline painted over a later section | Cues frozen at their last value when their act scrolled out of range |
|
|
336
|
+
| A clip stuck on its poster at the top of its act | The reveal waits for a `seeked` event, and a clip already at time 0 never seeks |
|
|
337
|
+
| A closing CTA that fades out before the page ends | A two-value cue on the last act, plus a tall section after it |
|
|
338
|
+
| Copied headings reading "even whenbreakfast" | Line-split spans abutting with no whitespace between them |
|
|
339
|
+
| A headline from act 2 overlapping act 3, failing contrast on the way | A one-value hold cue on a middle act. Only the last act may hold |
|
|
340
|
+
| A phone-only contrast failure on a trail-anchored act | The trail scrim aimed at the corner the copy leaves below 860px. The engine now switches it to a band |
|
|
341
|
+
| A rail act that shows one frozen screenful under reduced motion | `[data-sc-pan] { transform: none }` deleting the navigation. The engine now falls back to a scroll region |
|
|
342
|
+
| Two washed-out video acts no scrim tuning could rescue | Flat supplied footage with no white point. Grade the intermediate, not the CSS |
|
|
343
|
+
| A blank stage for the first viewport of a pinned act | A two-value first cue with no ground. Ground or greet |
|
|
344
|
+
| A rail heading dragged off-screen under reduced motion | The scroll-region fallback snap-centres a single wide track; keep the act heading outside the region, or give the rail multiple snap stops |
|
|
345
|
+
| Keyboard focus landing on a control nobody can see | The browser's scroll-into-view parks the element barely on screen, which is where its cue has not opened, and the opacity check still passes. **The engine now centres it on `focusin`** when the element is inside a `[data-sc-act]` and its cue is under 0.85. That fixes the off-screen half. See the note below for what it does not fix |
|
|
346
|
+
| A figure or drop numeral rendering as a plain bar | `data-sc-reveal` on type with `line-height` below 1. `clip-path` is relative to the border box, so the wipe eats the ascender and descender. See devices.md §4 |
|
|
347
|
+
| An image three times too tall, pushing its own label off the fold | `width` overridden in CSS while `height` still resolves to the HTML attribute. Override both or neither. See taste.md |
|
|
348
|
+
| An inverted section rendering its old ink, graded in the wrong direction | `--sc-ink` redefined on the subtree without restating `color`. See taste.md |
|
|
349
|
+
| A ground colour arriving a section late | `drift` on a page of short acts; several are part-way through at once. Paint grounds per section. See devices.md §10 |
|
|
350
|
+
| Every cue and reveal in a quiet act snapping 0 to 1 | A pinned act at `data-sc-span` ≤ 1, which is one pixel of travel. Minimum useful pinned span is ~1.2 |
|
|
351
|
+
| A clip that scrubs beautifully, stops, and then slides up the page as a still photograph | The clip was mapped to the act's pinned travel, which is 0 through the entire entry slide and 1 through the entire exit slide. The engine now maps clip time across the stage's whole visible life by default. See devices.md §1 |
|
|
352
|
+
| A custom fixed stage passing while its first screens do nothing | The page used `flow` markers, which are intentionally excluded from ordinary dead-scroll checks, but published no `data-sc-verify-state`. Report the actual rendered state and declare only genuine resolved holds |
|
|
353
|
+
| The hero clip frozen on a real iPhone, later clips fine, every probe green | iOS never paints an unplayed muted video, and the one-shot gesture prime was spent while the hero was still downloading. The engine now primes per clip at `loadedmetadata` and retries on every gesture, including `touchend` |
|
|
354
|
+
| A phone clip soft and stuttering while the same file is smooth on desktop | A landscape mobile encode in a portrait viewport: cover-fit decoded the full frame and threw three quarters of it away. Cut the phone clips portrait from the masters (see assets.md) |
|
|
355
|
+
| Four rounds of mobile fixes verified green, phone still broken | Headless Chrome cannot reproduce the iOS decoder, Low Power Mode, or touch. Deploy `references/device-diag.html` beside the site on the first mobile report and let the phone answer |
|
|
356
|
+
|
|
357
|
+
The first three are invisible to every check except looking at rendered output.
|
|
358
|
+
That is the argument for this whole pass.
|
|
359
|
+
|
|
360
|
+
Operational note: a `shoot.mjs` run can take the background server process down
|
|
361
|
+
with it when it finishes. Check the port before the next pass and restart
|
|
362
|
+
`serve.mjs` if it dropped.
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
## The harness will photograph the wrong site without telling you
|
|
366
|
+
|
|
367
|
+
`serve.mjs` fails with `EADDRINUSE` if something already holds the port. When
|
|
368
|
+
that server was started in the background, the failure is in a log nobody is
|
|
369
|
+
reading, and `shoot.mjs` then gets a perfectly good `200` from **whatever else
|
|
370
|
+
is on that port**. It walks that page, finds its worldflight, and writes a full
|
|
371
|
+
contact sheet and a clean report for a site you did not build.
|
|
372
|
+
|
|
373
|
+
Confirm the port is serving YOUR build before trusting any run:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
curl -s http://localhost:45XX | grep -o "<title>.*</title>"
|
|
377
|
+
curl -s -o /dev/null -w "%{http_code}
|
|
378
|
+
" http://localhost:45XX/assets/leg01.mp4
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
A 404 on an asset you know exists is the fastest tell.
|