TASK: Build final.mp4, a 32 s 1920x1080 30 fps silent video of a four-stroke petrol engine cross-section (cutaway) as a HyperFrames project: one hand-built inline SVG, GSAP used only as the clock. You cannot ask questions: take the simpler reading, log it in REVIEW.md. 0. TOOLS Terminal calls time out at 180 s: run installs and renders in the background with a log and poll. Run 'npx hyperframes telemetry disable' once; never run publish, feedback, cloud render, auth login, events, usage, npx skills. Library (book hyperframes_en_2026-11; no slash commands): py -3.14 library.py search 'words' | read PATH [--part N] [--grep WORD]. Save your context with delegate_task (Bolt, 2 at once): Bolt 1 digests skills/hyperframes/SKILL, references/runtimes, references/format; Bolt 2 digests skills/hyperframes-cli/references/lint-validate-inspect, preview-render, references/types/explainer, registry/blocks/exploded-stack. Each digest: 400 words max (contract, commands, flags, gotchas), appended to NOTES.md. Read other pages only when an error names them. You write all code. 1. SPEC npx hyperframes init four-stroke-2d --non-interactive --skip-skills. Root data-composition-id='engine' data-start='0' data-duration='32' data-width='1920' data-height='1080'; bg #0E1116; no audio. Write SPEC.md first (shot list; mark 'a chain turns the cam the same way as the crank' as standard mechanics, unsourced). theta = 45*t degrees (7.5 rpm): 4.0 s per 180-degree stroke, 32 s = two 720-degree cycles, EngineState pose(32) = pose(0), last frame 31.967 s. 2. ENGINE (all numbers in assets/engine-state.js; invent none) th = theta mod 720, a = theta mod 360; th 0 = intake TDC, 360 = firing TDC. Strokes of 180 from th 0: INTAKE, COMPRESSION, POWER, EXHAUST. Crank r 40, rod l 136, bore 80 (mm), axis at origin. Pin height x = r*cos(a) + sqrt(l^2 - r^2*sin(a)^2): 176.00 TDC, 96.00 BDC, 129.98 at 90 (46.02 travelled, not half). Crank pin (r*sin a, r*cos a), clockwise on screen; wrist pin stays on the axis; pin-to-pin always 136. Crown 26 above pin; roof 212. Valves 22 either side, open downward, max lift 8, visible springs; INTAKE left, head 28; EXHAUST right, head 24; plug on axis, 10 wide. Timing (typical; engines vary) is given at 1.0 mm lift: intake 710 and 225, exhaust 495 and 10, spark 330. lift = 4*(1-cos(2*pi*u)), u = ((th-start) mod 720)/305, 0 if u>1; start 675 intake, 460 exhaust; peaks 8 at th 107.5, 612.5. So lift is 0 for intake th 260-675, exhaust 45-460; 1.0 (+-0.02) at the edges; 1.6 at th 0; 1.0-2.3 in the overlap. Open flags are windows: intake th>=710 or <=225; exhaust th>=495 or <=10; both only th>=710 or <=10 (20 degrees). Two overhead cams at th/2 (half speed, same sense), axes 22 either side, 298 up. ROLLER followers (radius 4) on the line through the cam axis. Pitch curve R(psi) = 10 + 4*(1+cos(2*pi*psi/152.5)) for |psi| <= 76.25 from the nose, else 10; roller centre 288 closed; drawn lobe = R minus 4. lift = R - 10 holds only for this follower. Nose angle clockwise from up = theta/2 + 126.25 (intake), theta/2 - 126.25 (exhaust): straight down at peak. Valve lift is read from the lobe at the contact; never animate a valve alone. Gas (state, never on metal): fresh #3FA7FF, compressed #FFD23F, flame #FF7A1A, burnt #E5383B. th 710-40: red to blue smoothstep; 40-225 blue; 225-330 mix(blue, yellow, k), k = (71.26-s)/64.42, s = travel below TDC; 330-390 chamber stays yellow plus a red burnt disc about the plug, radius b*40, b = smoothstep(330,390,th), height = live chamber (roof minus crown), clipped to the chamber, thin orange rim; 390-710 red. gasColor yellow until th 390, then red. spark = max(0, 1-|th-330|/12), white flash. Port dots: blue in at intake, red out at exhaust, only while that window is open; phase from a cumulative-lift table built at load; dot i at i/N; no randomness. Parts (cut at centre plane): piston with 3 rings, rod, crank with counterweight, flywheel R 70 behind, block (bore wall 40, jacket 44-54 in #4C6A73, outer 60), head, valves with springs and rollers, 2 cams, plug, ports bore 24, crankcase, sump to -100. 3. ON-SCREEN COPY (exact; nothing else) Inter for words (title 700, stroke name 900, rest 400), JetBrains Mono 400 or 700 for numbers; bundled, no @font-face, no other weights. Text >= 30 px, contrast >= 4.5:1. HUD only in x 96-560 and x 1360-1824 (width 464, wrap OK), y 54-1026; engine only in x 580-1240. Left, top y: 'FOUR-STROKE ENGINE' 44 px + 'Single-cylinder petrol cutaway. Slow motion, 7.5 rpm.' (54); 'CRANK ANGLE' + floor(th) as '345°', mono 72 px (215); 'CAM ANGLE' + floor(th/2), mono 48 px (345); 'CYCLE 1 OF 2' / 'CYCLE 2 OF 2' (switch 16.0 s) and 'STROKE 1 OF 4' to '4 OF 4' (445); stroke name 'INTAKE' 'COMPRESSION' 'POWER' 'EXHAUST' 56 px (540); caption 32 px (625); footnote 'Valve and spark angles are typical values. Real engines vary. Geometry, flow and burn are simplified.' (870). Right: dial centre (1592,260) radius 150, every position and the pointer at th/2 degrees clockwise from the top: blue arc intake open (710 to 225), red arc exhaust open (495 to 730), bright overlap mark (710-730), tick 'SPARK' at 330, ticks '0°' '180°' '360°' '540°', labels 'IN' 'EX', caption 'ONE LAP = 720° CRANK = 360° CAM' (465); timing inset (550): 18-mark crank sprocket beside a 36-mark cam sprocket twice its size, chain line, marks at theta and theta/2, caption 'CRANK 2 : CAM 1'; callout pill (720-810); legend swatches 'Fresh charge' 'Compressed charge' 'Flame front' 'Burnt gas' 'Dial arcs: valve open' (825). Captions, one per 4 s from 0: 1 'Piston moves down. The intake valve is open and fresh air-fuel mix flows in.' 2 'The intake valve closes just after the bottom. The rising piston squeezes the charge.' 3 'Hot gas expands and pushes the piston down. Only this stroke delivers power.' 4 'The exhaust valve is open. The rising piston pushes burnt gas out.' 5 'The camshaft turns at half crank speed: 360° of cam for 720° of crank.' 6 'Each valve is pushed open by its cam lobe and closed by its spring.' 7 'One power stroke every two crank turns. The flywheel carries the other three.' 8 'Both valves are open together for only about 20°, at the end of exhaust and the start of intake.' Stroke name + caption fade out over the 0.25 s ending on each boundary and in over the 0.25 s starting on it; frame 0 and the last frame fully shown. Callouts (30 px, opaque #0E1116 pill, leader line to the part; fades lie INSIDE the window, 0.25 s each end): 4.4-6.6 and 20.4-22.6 'INTAKE CLOSES 45° AFTER BOTTOM' (intake valve); 6.8-9.0 and 22.8-25.0 'SPARK 30° BEFORE TOP' (plug); 10.4-12.6 and 26.4-28.6 'EXHAUST OPENS 45° BEFORE BOTTOM' (exhaust valve); 15.0-17.2 'OVERLAP: BOTH OPEN, ONLY 20°' (between valves); same text 30.0-32.0, fade-in only, held to the last frame. 4. TECHNIQUE assets/engine-state.js: globalThis.EngineState in an IIFE; EngineState.at(t) is pure (no clock, random, earlier frames) and returns theta, th, stroke, strokeName, cycle, pinY, s, crankPin, rodTilt, intakeLift, exhaustLift, intakeOpen, exhaustOpen, camAngle, camNoseIntake, camNoseExhaust, spark, burn, flameR, chamberH, gasColor, zoom; every drawn thing and HUD number reads it. One SVG viewBox 0 0 1920 1080, 2 px/mm, crank axis (900,800): sx = 900+2x, sy = 800-2y. (static rect x 580-1240, y 70-1010) around ; 2 px #3A4350 border outside both. Cut faces: diagonal hatch (#C9D1D9 on #1B2027); steel #8E99A6, graphite #2B3038; gradients only, no filters. HUD = one class='clip' wrapper (data-start='0' data-duration='32'); each caption group a clip with its own data-start and data-duration, opacity on a child. GSAP: save https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js once into assets/vendor/gsap.min.js and reference it locally (no CDN tag; no network when rendering). const clock = {t:0}; ONE paused tl; tl.to(clock, {t:32, duration:32, ease:'none', onUpdate: () => render(clock.t)}, 0); render(0) at load; compute EVERY opacity inside render(t), no other tweens; await document.fonts.ready; window.__timelines['engine'] = tl as the LAST statement; never tl.play(). If snapshots at 0, 2, 4 s show one piston height, onUpdate is not firing on seek: use hf-seek (root data-no-timeline; addEventListener('hf-seek', e => render(e.detail.time)); section 'The seek protocol' in runtimes) and drop GSAP. Frames seek out of order: carry nothing over. Spark flash and flame are the brightest things; all else calm. Engine-group zoom about (900,390), smoothstep keys at t mod 16: 6.4 s 1.0, 8.0 s 1.2, 9.4 s 1.2, 11.8 s 1.0. Never: audio, media files, network requests, Date.now, performance.now, Math.random, setTimeout, setInterval, own requestAnimationFrame, repeat:-1, CSS perspective, glow, bounce easing, faces, stock footage, compression ratio, horsepower, a brand, any rpm but 7.5; both valves open outside the overlap; sinusoidal piston; valve events exactly at TDC or BDC; a gas colour changing meaning. 5. COMMANDS (only these; invent no flags) 1) npx hyperframes doctor ; npx hyperframes browser ensure (offline: set HYPERFRAMES_BROWSER_PATH to Chrome). Log both in REVIEW.md. 2) Write SPEC.md, the module and assets/engine-state.check.cjs (require('vm').runInThisContext(require('fs').readFileSync('assets/engine-state.js','utf8'))). Assert every number above, and: stroke 0,1,1,2,2,3,3,0 at t 3.999,4,7.999,8,11.999,12,15.999,16; lift 0 inside the zero windows and 1.0 (+-0.02) at th 710, 225 (intake), 495, 10 (exhaust); pin-to-pin 136 (1e-6) for t 0-32 step 0.01; lift peak 8.00 at t=2.3889 (intake) and 13.6111 (exhaust); both flags true exactly for th>=710 or <=10; lobe radius at the contact minus 10 = lift (+-0.01) at th 10, 107.5, 180 (intake), 505, 612.5, 700 (exhaust); spark 1 at t=22/3 and 70/3, 0 beyond +-0.267 s; gasColor #3FA7FF at t=3, #E5383B at t=11.5; t=32 equals t=0 (1e-9). node assets/engine-state.check.cjs must print PASS before you draw. 3) Build index.html; npx hyperframes lint after each big edit. 4) npx hyperframes check --json: zero errors, browserSkipped not true, zero console errors; film_silent is expected (accept in REVIEW.md); fix or justify other warnings. 5) npx hyperframes snapshot --at 0,2,4,5,6.5,7.3333,7.4,8,11,13.61,15.9,16,31.9 (PNGs: .hyperframes/artifacts/snapshots//frame-NN-at-Xs.png). Send each PNG with its Expect line to a Bolt subagent (vision); record its answer in REVIEW.md; if nobody can view it write NOT VIEWED, never passed. Expect: t=0 piston TDC, both valves about 1.6 up (inside the overlap), 000°; t=2 pin 129.98, intake about 7.7, chamber blue; t=4 BDC 96.00, intake about 4.3; t=5 intake 1.0 (closing), charge pure blue, intake callout; t=6.5 blue-yellow (k about 0.64); t=7.3333 spark flash, piston 6.8 below TDC, valves shut, zoom 1.12; t=7.4 flash fading; t=8 TDC, flame half grown INSIDE the chamber, zoom 1.2; t=11 exhaust 1.0, piston about 71 below TDC, gas red; t=13.61 exhaust 8; t=15.9 overlap callout, 715°; t=16 pose of t=0, CYCLE 2 OF 2, caption 5; t=31.9 overlap callout. Also: wrist pin on axis, rod joins both pins, roller touches the nose at peak lift, no gas colour on metal, no clipped or overlapping text. 6) snapshot --at 4,0,4 --no-end: the two 4 s PNGs (frame-00-at-4s.png and frame-02-at-4s.png in the printed folder) must be identical (cmp or sha256sum). 7) snapshot --review; follow references/review; write REVIEW.md; at most 3 fix rounds. 8) render --quality draft --output review.mp4 --workers 1; fix; render --quality delivery --output final.mp4 --workers 1. 9) ffprobe -v error -show_entries stream=codec_type,width,height,r_frame_rate,duration -of default=nw=1 final.mp4: one video stream, 1920x1080, 30/1, 32.0 (+-0.05), no audio. REPORT: files (absolute paths), each command with exit code, decisions, what you could not verify; never claim a check you did not run.