23 KiB
P5 Motion library: evidence log
Package card: docs/phase2/PLAN.md §2.3 "P5 Motion library". Contract: docs/phase2/contracts/motion.md.
Owns: docs/phase2/research/springs.py (the one source), theme/02-motion.nowrap.css, device/shared/motion.js,
native/shared/motion_tokens.h (all three generated by springs.py --write), docs/phase2/contracts/motion.md.
Steam build at the time of writing: 11094443.
Status
M1 reached (2026-10-07 ~04:00): contract docs/phase2/contracts/motion.md written; the three outputs are generated and
complete (not stubs: tokens, keyframes and functions are inert until a caller uses them, so no flag is needed);
springs.py --check (MO-1) and --test-js (MO-2) pass offline; glass.py check-theme PASS.
M3 reached, card complete (2026-10-07 05:55): every Builds item is in and MO-1 to MO-5 pass (MO-3 and MO-4 live
in both input-mode stubs, pad and laser). P5 has no layer, plate or popup fragments, so M4's native gates do not apply
to it; M5 is V2's run. Version of the token table: ed4e044810 (all three outputs, springs.py --check).
Session 2 (2026-10-07 05:30–06:05, resumed after the usage-limit cut at 04:30):
- Live:
motion.jsloaded by P1's runtime (rt.shared.motion=window.__LGS_MOTION); MO-2 in CEF; MO-3 and MO-4 PASS (springs.py --live mo3|mo4|cef [--mode pad|laser]). - Filmstrips next to
window-nav-motion.html(shots/p2_cmp_p5_motion.png): alerts and sheets popped in, so large glass now fades over the glass channel (D-P5-8, contract changelog); after the fix all 5 storyboard rows match. - G-MOTION (P10's
gates) judges P5's keyframes through__LGS_MOTIONand catches non-token controls. The scroll-drivenlgs-edge-inis wrongly failed as "running at rest":isScrollDriven()added, REQ P5->P10 open. - MO-5: P9 builds glassd with the header; own run of
glassd --selftest phasePASS; header compiles on the Frame with-Wall -Wextra -Wpedantic, no warnings. - Reproducible from the repo:
springs.py --live cef|mo3|mo4|film(the filmstrip moved out of scratch intosprings.py;mo3/mo4now go back to the route they found).
Remaining (none blocking): REQ P5->P10 (skip scroll-driven animations in G-MOTION) and REQ P5->P7 (optional use of
create()) are answered by their owners; any REQ addressed to P5 later is handled in a new session.
How to regenerate
python docs/phase2/research/springs.py --write # rewrite the three outputs
python docs/phase2/research/springs.py --check # MO-1 + outputs up to date
python docs/phase2/research/springs.py --test-js # MO-2 (node)
python docs/phase2/research/springs.py --live cef # MO-2 inside Steam's CEF (locked lab step)
python docs/phase2/research/springs.py --live mo3 [--mode pad|laser] # MO-3 (one locked shot step on /zoo/buttons)
python docs/phase2/research/springs.py --live mo4 [--mode pad|laser] # MO-4 (same, --media reduce)
python docs/phase2/research/springs.py --live film [--media reduce] # filmstrips + shots/p2_cmp_p5_motion.png
Evidence
| Test | Command | Result | Shots / JSON | Date | Steam build |
|---|---|---|---|---|---|
P5-K1 probe: two 0% keyframe blocks with different easings |
python glass.py js "<probe1.js>" (temporary style + off-screen div in main, removed) |
getKeyframes() keeps 2 keyframes at offset 0 (linear(0 0%, 1 50%, 1 100%) for opacity, linear for translate); at 250 ms of 1000: opacity .4, translate −6 px, i.e. each property eased by its own block |
— | 2026-10-07 | 11094443 |
P5-K2 probe: @keyframes redefined inside @media |
same step | a matching @media (min-width: 1px) redefinition wins (from .7); a non-matching @media (max-width: 1px) one is ignored (from .3) |
— | 2026-10-07 | 11094443 |
| P5-K3 probe: implicit end keyframe | same step | 0%, 35% { opacity: 0; filter: blur(8px) } ends at the element's own opacity .8 and filter: none |
— | 2026-10-07 | 11094443 |
| MO-1 | python docs/phase2/research/springs.py --check |
b0, b15, b20, b25 linear() byte-identical to DESIGN2 §11.3 and present in 02-motion.nowrap.css; all 3 outputs up to date; version ff9644ee1e |
— | 2026-10-07 | n/a (offline) |
| MO-2 (node) | python docs/phase2/research/springs.py --test-js |
every token at 1 ms steps: max error 1.0e-3 (the settle clamp at the last sample; limit 2e-3); create() retarget keeps value and velocity (jumps ≤ 4e-5, ≤ 5e-4/s), done exactly at the CSS duration (441 / 488 / 735 / 210 / 250 ms) |
— | 2026-10-07 | n/a (offline) |
| check-theme | python glass.py check-theme |
PASS (35 files) | — | 2026-10-07 | n/a (offline) |
| MO-1 (re-run) | python docs/phase2/research/springs.py --check |
PASS: 4 curves byte-identical, 3 outputs up to date, version 2428c76f74 (changed from ff9644ee1e by the exit-curve fix, see D-P5-7) |
— | 2026-10-07 05:31 | n/a (offline) |
| MO-2 (node, re-run) | python docs/phase2/research/springs.py --test-js |
PASS: worst 1.01e-3 (limit 2e-3); create() done at 441 / 488 / 735 / 210 / 250 ms |
— | 2026-10-07 05:31 | n/a (offline) |
| Live load | python glass.py js "…__LGS_RT.shared…" |
rt.shared = [motion], rt.shared.motion.version = window.__LGS_MOTION.version = 2428c76f74, failedLoads empty |
— | 2026-10-07 05:33 | 11094443 |
| MO-2 in CEF | python docs/phase2/research/springs.py --live cef (evaluates the generated motion.js in SharedJSContext, samples every token at 1 ms, compares with the Python closed form on the PC) |
PASS: worst 1.01e-3 (limit 2e-3; before the settle clamp 5e-10); create('depth') done at 441 ms; the runtime's global restored |
— | 2026-10-07 05:40 | 11094443 |
| MO-3 | python docs/phase2/research/springs.py --live mo3 (one glass.py shot main p2_p5_mo3 --route /zoo/buttons --theme on step; its pre builds 20 glass test elements off-screen in the main window, one per keyframe as contract §2 calls it plus the three utility classes; samples f = 0, .15, .35, .5, .75, .999 while paused; then plays them all and waits until 1 s after the longest one ends) |
PASS 20 / 20: 1 s after, no lgs-* animation on any element; each element's opacity, scale, translate, filter, backdrop-filter, clip-path, background-color, box-shadow equal its own values before the animation (no resting scale, filter or clip-path); the .lgs-mat-out exit holds at opacity 0 (forwards, for a node removed at animationend); stage removed; timeline advanced 1844 ms (window visible); __LGS_MOTION.audit() finds every one a token duration and token easing. Samples match the contract, e.g. lgs-mat-glass-in scale 1.0375 → 1.0172 at .5 → 1, backdrop blur(0px) saturate(1) → 22 px at 92 %, coverage 1 by 28 %, content 0 until 35 %; lgs-sheet-in scale .97 → .9983 at .5; lgs-morph clip from the source inset to 0; lgs-page-in translate 16 → .9 px at .5 |
shots/p2_p5_mo3.png (the route after the step); full table in this session's output, rerun to see it |
2026-10-07 05:38 | 11094443 |
| MO-4 | python docs/phase2/research/springs.py --live mo4 (same step with --media reduce: step: media=reduce on 11 targets) |
PASS 20 / 20: matchMedia(reduce) true; tokens on html: snappy 180, sheet-in 200, hover-in 150, mat-in 180 ms, --lgs-ease-snappy = the b0 curve; every keyframe animates opacity only (entrances, exits, lgs-focus-in from .6) or nothing (lgs-mat-content-*, lgs-sheet-content-*, lgs-morph-content, lgs-catch, lgs-knob-lift, lgs-shift-in); every duration 150–200 ms; nothing left 1 s after |
shots/p2_p5_mo4.png |
2026-10-07 05:39 | 11094443 |
| MO-5 | P9: ./build/glassd --selftest phase and GLASSD=build/glassd sh tools/test_phase.sh with glassd built against native/shared/motion_tokens.h (-I../shared) |
PASS (P9's run): selftest worst 0.0010 (limit 0.002), test_phase.sh 40 values worst 0.001 (window cover, thick slab and plate on sheet-in / sheet-out; liquid on 250 / 350 ms) |
wp/P9.md rows MO-5, GL-4 |
2026-10-07 05:06 | 11094443 |
| Filmstrip v1 | python <scratch>/p5/film.py A (one glass.py shot main p2_p5_film_a --pre <film.js> step: 30 miniature scenes of our own nodes in the main window, the storyboard's 5 rows, each keyframe called as contract §2 says, P4's thick glass, every animation paused at f × row time; stage removed after) + tools/mockshot.py window-nav-motion.html |
Morph, sheet out and route match; alert and sheet present popped in (glass fully opaque at f .15, storyboard ≈ .37 / .50): fixed in springs.py (D-P5-8), version 2428c76f74 → ed4e044810 |
(overwritten by v2) | 2026-10-07 05:42 | 11094443 |
| MO-1, MO-2 after the fix | springs.py --write, --check, --test-js; glass.py check-theme; glass.py sync |
PASS: 4 curves byte-identical, outputs up to date, version ed4e044810; MO-2 worst 1.01e-3; check-theme PASS (38 files) |
— | 2026-10-07 05:44 | n/a |
| MO-3 after the fix | python docs/phase2/research/springs.py --live mo3 |
PASS 20 / 20, nothing left 1 s after, stage removed. New opacity samples at f .15 / .35 / .5 / .75: lgs-mat-large-in .20 / .48 / .68 / 1; lgs-sheet-in .50 / 1 / 1 / 1; lgs-sheet-out .75 / .21 / .07 / .01; lgs-toast-in exactly 1 from f .15 (was .998) |
shots/p2_p5_mo3.png |
2026-10-07 05:45 | 11094443 |
| Filmstrip v2 vs mockup | film.py A B, then <scratch>/p5/compose.py (storyboard rows above live rows) |
Match on all 5 storyboard rows: morph clip grows from the More circle (40 % at 91 ms vs 41 %), content in 15–50 %; alert glass faint at 66 ms, content from 35 % of 250 ms, swell 1.019 → 1, scrim on fade; sheet .97 → 1 with glass half at 110 ms, content 25–70 %; dismiss: content out first, glass gone by ~50 %; route: old out in 150 ms linear, new in on page after 40 ms with 16 px parallax. Known differences: no overshoot on the morph clip (D-P5-9); the storyboard tiles have a room photo behind the window. Set B (no storyboard; checked against MO §3.4, §4.4, §4.10, §4.15): small glass in (glass leads, content sharpens 35–100 %), small glass out (content gone by 55 %, then the glass), toast in (materialize in the first 250 ms, 8 px drop on b15), focus first frame at .6, toggle knob lifted into clear glass 20–65 % while it travels |
shots/p2_p5_film_a.png, shots/p2_p5_film_b.png, shots/p2_cmp_p5_motion.png |
2026-10-07 05:46 | 11094443 |
| G-MOTION with P5 keyframes | python glass.py gates main --route /zoo/buttons --only motion --json --pre <3 test divs: lgs-mat-glass-in on mat-in, lgs-sheet-in on sheet-in, lgs-fade-in 300ms ease>; then the same with lgs-fade-in var(--lgs-d-fade) ease |
P5's keyframes (with their composite per-keyframe curves) pass P10's token checks through __LGS_MOTION; the negative controls are caught (duration 300 ms, then keyframe easing ease); at rest 1 s after: 0 running, 0 lgs-* left; test divs removed. Without a pre: MOTION PASS |
— | 2026-10-07 05:47 | 11094443 |
P5-K4 probe: scroll-driven lgs-edge-in |
gates main --route /zoo/buttons --only motion --pre <scroller with scroll-timeline, sticky child animation: lgs-edge-in linear both; animation-timeline; animation-range: 0px 24px>; then glass.py js with __LGS_MOTION.audit() |
The animation is in document.getAnimations(), playState running, timeline ScrollTimeline, duration auto; opacity 0 at scrollTop 0, .5 at 12 px. G-MOTION FAILs it (running at rest, lgs left, non-token duration): REQ P5->P10. motion.js now exports isScrollDriven() and audit() marks the row scroll: true, tokenDuration: true (node test + live) |
— | 2026-10-07 05:49 | 11094443 |
| MO-3 / MO-4, both input modes | springs.py --live mo3 --mode pad, --live mo3 --mode laser, --live mo4 --mode pad |
PASS each: 20 / 20, nothing left, stage removed (step: mode=pad via classes, mode=laser via rt.test.input.set, media=reduce on 11 targets) |
— | 2026-10-07 05:53 | 11094443 |
| MO-5 (own run) | frame_ssh.py run "…/native/glassd/glassd --selftest phase" (offline, no SteamVR) |
PASS: worst 0.0010 (limit 0.002); sheet-in settles 735 ms, sheet-out 515 ms, linear 250 / 350, reduce 180 ms. The installed binary prints header version 2428c76f74: the header changed since only in kVersion (CSS composites are in the hash), no rebuild needed |
— | 2026-10-07 05:50 | 11094443 |
| Header compile test | g++ -std=c++17 -Wall -Wextra -Wpedantic -O2 on the Frame (GCC 15.1.1) of a test that includes motion_tokens.h and evaluates spring() at each token's settle time (/tmp/lgs/p5, deleted after) |
PASS, no warnings: |y| ≤ 0.00101 at every settle time; coverage(.15) .5000; version ed4e044810. settled() (which also bounds velocity) is still false at the exact settle ms for release-touch, morph-open, sheet-out and true 1 ms later, as in glassd's selftest (515 ms) |
— | 2026-10-07 05:51 | 11094443 |
| Filmstrip, reproducible | python docs/phase2/research/springs.py --live film (the filmstrip is now part of springs.py: two locked shot steps, stage removed after each, mockup rendered with tools/mockshot.py, comparison composed) |
Same frames as v2 (all 5 storyboard rows match; set B as above); stage gone after both steps | shots/p2_p5_film_a.png, shots/p2_p5_film_b.png, shots/p2_cmp_p5_motion.png |
2026-10-07 05:58 | 11094443 |
| Filmstrip, Reduce Motion | springs.py --live film --media reduce |
Every interaction is a cross-fade ≤ 200 ms (all animations 150–200 ms): menu appears whole at half opacity at 91 ms with no clip morph, alert and sheet fade in place with no swell, route new page with no parallax, small glass fades with no blur ramp, knob does not lift; nothing scales or moves | shots/p2_p5_film_a_reduce.png, shots/p2_p5_film_b_reduce.png |
2026-10-07 06:00 | 11094443 |
Decisions (PLAN §1.17 rule: safe default, noted here)
- D-P5-1 Reduce Motion durations. PLAN §1.5 and VP P-56 ask for fades of at most 200 ms. Every duration token
is redefined under
prefers-reduced-motion: reduce: 150 ms (hover-in,interactive,morph-close, CTL §4's "150 ms fade from .6"), 200 ms (sheet-in, MO §4.8), 180 ms for the rest (--lgs-d-reduce);glow-off,swap,page-outkeep 90 / 120 / 150 ms. Bounce curves become b0 (D2 §16). - D-P5-2 Reduce Motion keyframes. Instead of asking every caller for a media query, each
lgs-*keyframe is redefined inside the media query as opacity only (content and decorative keyframes empty). Proven mechanism: P5-K2. - D-P5-3 Extra keyframes. Context packages own no
*.nowrap.cssfile, so every keyframe they need must be here. Added beyond the card's list, from the concepts:lgs-mat-large-in(alerts, R8),lgs-sheet-content-*,lgs-morph-content,lgs-catch(MO §4.6),lgs-knob-lift(MO §4.15, CTL),lgs-shift-in(SET §SteamVR switch),lgs-fade-in/-out,lgs-edge-in(GP scroll edge). The card'slgs-morphis the clip keyframe of MO §6.4. - D-P5-4 Extra fixed tokens.
--lgs-d-page-out150,--lgs-d-glow-off90,--lgs-d-swap120,--lgs-d-reduce180 and the delay tokens (--lgs-delay-*,--lgs-stagger) carry durations the specs name outside D2 §11.2's table, so call sites never write literals (P-58). - D-P5-5 Not defined here.
--lgs-press-grow,--lgs-press-max,--lgs-focus-first,--lgs-card-lift,--lgs-disc-liftare P4's (contracts/tokens.md);lgs-focus-inreads--lgs-focus-first. - D-P5-6 No
@propertyin 02-motion. The card lists "@property declarations", but P4 already registers--lgs-hover,--lgs-press,--lgs-focus(identical to D2 §16) with the states that use them. A second registration in the later 02 file would silently override any future P4 change, so P5 registers none; no keyframe needs one. - D-P5-7 Exit curves sit at the hold offset. The first live MO-3 run (session 1, 04:28) showed that Chromium 126
merges the implicit start values of the other properties into an explicit
0%keyframe, easing included, so a custom coverage curve on0% { opacity: 1 }also drovescaleandbackdrop-filter.springs.py'stail_of()now finds where the exit curve leaves 1 (86.5 % forlgs-mat-glass-out, 26.5 % forlgs-sheet-out) and puts the curve on a keyframe at that offset;0%keeps the caller's easing. Versionff9644ee1e→2428c76f74. Proven by MO-3's samples:lgs-mat-glass-outscale stays 1 until 55 % and is 1.0167 at 75 % (the--lgs-ease-mat-outcurve). - D-P5-8 Large glass fades over the glass channel. First version: every keyframe used glassd's coverage alpha
smoothstep(0, .3, m)(MO §9). The live filmstrip next towindow-nav-motion.htmlshowed alerts and sheets popping in (fully opaque by 69 ms and 110 ms) where the storyboard fades them over most of the glass channel. In glassd the frost, tint and lensing keep ramping until m = .92, so the slab still materializes; a large CSS slab may not ramp its blur (MO R8), so its opacity is the only glass channel left. P5 now gives large glass the perceived optics ramp instead:min(1, m / .8)(LARGE_COVERinsprings.py; light 0–.6 with frost and tint .2–.92 average to about that), and the mirror on exit,min(1, (1 − p) / .8). The storyboard draws 1.3 g (alert), 1.25 p (sheet in) and 1 − ramp(p, .25, 1) (sheet out); measured live: alert .20 / .48 / .68 / 1 at f .15 / .35 / .5 / .75 of 250 ms, sheet in .50 at 110 ms (storyboard .50), sheet out .75 / .21 / .07 (storyboard .80 / .22 / .08). Small glass and toasts keep glassd's coverage, because their blur, tint and shade ramp alongside. glassd itself is unchanged. - D-P5-9 Menu morph clip has no overshoot. The storyboard's morph row shows the b .20
morph-openovershoot (1 %);lgs-morphclips on the b0 curve as MO §6.4 specifies (a negativeinset()is not dependable); glassd draws the overshoot in native mode. Measured: clip 40 % at 91 ms (storyboard 41 %), 83 % at 212 ms (90 %).
Requests
- REQ P5->P4: in
theme/00-tokens.nowrap.css, the Phase 1 Reduce Motion rule--lgs-t-fast: 1ms; --lgs-t: 1ms; --lgs-t-slow: 1msis the rule MO C8 withdraws ("fades stay"; VP P-56 wants fades ≤ 200 ms, not instant). Please map it to--lgs-t-fast: var(--lgs-d-interactive); --lgs-t: var(--lgs-d-hover-in); --lgs-t-slow: var(--lgs-d-fade)(P5 sets those to 150 / 150 / 180 ms under Reduce Motion), and consider aliasing--lgs-spring: var(--lgs-ease-b15)and--lgs-ease: var(--lgs-ease-b0)(D2 §11.3: they replace Phase 1's curves). Your call on timing; the aliases die with Phase 1 anyway. P4 (2026-10-07): done: under Reduce Motion--lgs-t-fast/-t/-t-slow=var(--lgs-d-interactive/-hover-in/-fade)(fallbacks 150/150/180 ms);--lgs-spring: var(--lgs-ease-b15, <P1 curve>),--lgs-ease: var(--lgs-ease-b0, <P1 curve>). - REQ P5->P9: please build glassd with
native/shared/motion_tokens.h(#include "../../shared/motion_tokens.h"or-I../shared) and take the phase constants from it:lgs_motion::kPhaseUp/kPhaseDown(d .5 / .35, b 0) instead of the literals0.5f/0.35finPhaseAnim::step,kMatInMs/kMatOutMs/kReduceMsinstead of0.25f/0.35f/0.18f,lgs_motion::spring()/settled()forspringB0, andkRampLight … kRampAlpha+coverage()inphaseMap. Thentools/test_phase.shis MO-5 / GL-4. The header compiles standalone withg++ -std=c++17 -Wall(see evidence) and its values equal the ones glassd uses today, so the build output is unchanged.- P9 (2026-10-07 05:12): done.
glassd.cppincludesmotion_tokens.h(-I../shared);PhaseAnimandphaseMapusekPhaseUp/kPhaseDown,kMat*Ms,kReduceMs,spring()/settled(), thekRamp*ranges andcoverage().glassd --selftest phasePASS (worst 0.0010) andtools/test_phase.shPASS (40 values, worst 0.001), evidence inwp/P9.md.
- P9 (2026-10-07 05:12): done.
- REQ P5->P8:
motion.jspublishesglobalThis.__LGS_MOTIONinvr:systemuiwhen you prepend it tolgs_sg.js(daemon §9). Onstop()please alsodelete globalThis.__LGS_MOTIONthere (or call__LGS_MOTION.remove()), so G-REMOVE finds no global left. In SharedJSContext P1's loader already deletes it.- P8 (2026-10-07 04:28): done.
motion.jsis prepended in the same scope aslgs_sg.js(opts.motion = true; the sg version hashes both files), and everydestroy()path (teardown, nothing left to show) also calls__LGS_MOTION.remove()and deletesglobalThis.__LGS_MOTIONinvr:systemui(SG_DESTROY_JS).
- P8 (2026-10-07 04:28): done.
- REQ P5->P7:
lgs_sg.jscan use__LGS_MOTION.create(token, {value})for the depth channel:.to(target, now)keeps value and velocity (MO C5),.done(now)latches exactly at the CSS settle time for a step (441 ms fordepth) with the criterion of your sg.md §3.4, and.value(now)is then exactly the target. Token names equal yours (depth,sheet-in,sheet-out,fade,snappy).__LGS_MOTION.spring(y0, v0, tSeconds, d, b)is MO §8 verbatim if you prefer to keep your own state. Contract §3.- P7 (2026-10-07 08:20): done the second way: with
motion.jsprependedlgs_sg.jsuses__LGS_MOTION.springandtokens[name].d/.b(status().motion="motion.js") and keeps its own channel state (it records every retarget and push fortimeline()); same settle criterion. SG-1 PASS with motion.js: max error 0.0002 mm against the closed form, 27 pushes, final push at 444 ms, 0 pushes after.
- P7 (2026-10-07 08:20): done the second way: with
- REQ P5->P10: for G-MOTION (
gatesMOTION,glass.py motion): CSS animations carry their easing in the keyframes (effect.getKeyframes()[i].easing, serialised by Chromium aslinear(0 0%, …, 1 100%)), and P5's keyframes use a few composite per-keyframe curves (coverage ramps, content windows). Please accept every curve in__LGS_MOTION.allowed.easings(or call__LGS_MOTION.isTokenEasing(str)/isTokenDuration(ms), which handle the serialisation), and every duration in__LGS_MOTION.allowed.durations(includes the Reduce Motion ones: 90, 120, 150, 180, 200).__LGS_MOTION.audit(doc)returns one row pergetAnimations()entry with both verdicts, if you want to call it directly (SharedJSContext, when the runtime is loaded).- P10 (2026-10-07 05:40): done.
gates(MOTION) andglass.py motioncall__LGS_MOTION.isTokenDuration(ms)/isTokenEasing(str)whenever__LGS_MOTIONis loaded in the page they audit (SharedJSContext or thevr:page), for the animation's easing and every per-keyframe easing; the local D2 §11.2 table is only the fallback without the runtime.motionalso records__LGS_MOTION.audit(doc)asmotionLibin its JSON. Only animations from our stylesheets (our keyframes, or transitions our rules declare) are judged; Steam's own are listed. contracts/lab.md §4.
- P10 (2026-10-07 05:40): done.
- REQ P5->P10: G-MOTION (
gatesMOTION andmotion) fails a scroll-driven animation: the scroll edge effect (lgs-edge-inon a scroll timeline, MO §4.13, contract §2) is listed asrunningat rest, aslgsLeft, and as a non-token "duration 2.85714% ms" (probe P5-K4 below), although nothing moves while the scroll is still. Please skip animations for which__LGS_MOTION.isScrollDriven(a)is true (timeline is aScrollTimeline/ViewTimeline, i.e. notdocument.timeline) in the at-rest,lgsLeftand duration checks, and keep the easing check.audit()rows carryscroll: truefor them.- P10 (2026-10-07 06:35): done: animations with
__LGS_MOTION.isScrollDriven(a)(elsea.timeline !== document.timeline) are skipped in the at-rest,lgsLeftand duration checks; their easing is still checked.
- P10 (2026-10-07 06:35): done: animations with