SERVICE.md documents the /events polling endpoint, the event persistence
model, the in-memory backend's enqueue-202 contract, and the app-preview
route; DEPLOY.md notes the job_events migration and that db:migrate is
journal-tracked/idempotent (with the manual-psql alternative).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ported from PR #15 (packages-side surface only; the fork's dev UI page is
intentionally dropped - the API surface is the product):
- job_events table + repository, worker emits stage transitions, additive
GET /v1/clones/:id/events polling route. The fork's hand-written
migration was never registered in Drizzle's journal (it silently never
applied); regenerated properly via drizzle-kit from schema.ts with the
(job_id, seq) index declared, and verified against a live Postgres
- In-memory backend moves to the documented enqueue-202 + poll contract
with single-flight BUSY guard; POST honors backend httpStatus
- app-preview static serving routes; the preview BUILD half is deferred
until the compiler exports buildApp/DEFAULT_HARNESS_DIR - routes 404
cleanly until then
- preview option threaded through core types; cache key intentionally
unchanged while the flag is inert
All workspaces typecheck; api 18/18, core 13/13, worker 1/1 pass;
migration verified applying (table + index + FK) on postgres:16.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ported from PR #15: a SHA256-pinned catalog of 49 generic library and
platform fingerprints (carousel/consent/chat/commerce vendors) with an
O(1) signature index. Catalog hits strengthen recipe recognition (small
confidence deltas, marquee->logo-cloud, carousel false-grid skip,
lightbox->gallery, commerce prior) and are recorded in the manifest as
pattern evidence with node references - they never override captured
geometry, which remains gated by the existing plan-agreement checks.
The integrity pin is unconditional: a catalog/lock mismatch always
throws (the fork's env-var soft-warning mode is removed).
515 tests pass (10 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Salvaged and re-implemented from PR #15:
- Deterministic environment shim (seeded Math.random, pinned-but-advancing
Date consistent across constructor/now/getTime/valueOf/toISOString) so
time/random-dependent pages capture reproducibly; epoch and seed are
recorded run metadata, performance.now stays real for motion sampling
- Post-snapshot lazy-asset discovery sweep for channels the walker misses:
noscript contents (DOMParser), inline style url(), data-background(-image),
img[loading=lazy] currentSrc
- Navigation gets a total 90s budget, an early bot-wall probe that aborts
with a pollution-style error instead of capturing garbage (WALL_RE now
shared with the validator gate), and one fresh-context retry scoped to
the initial nav on retryable failure classes only
505 tests pass (29 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- viewportMinHeightLaw: a box authored min-h-screen / min-h-[calc(100vh -
Cpx)] had its per-viewport px baked at capture, leaving whitespace (or
overflow) at window heights between captured viewports; when the source
class list corroborates a vh token and per-regime offsets agree, emit
the authored law (100vh / calc(100vh - Cpx)) instead of frozen px
- isCappedCenteredContainer: max-width-capped centered content columns
emit mx-auto (including as flex/grid items, where auto margins center
along the main axis) so pages stay centered at non-captured window
widths instead of pinning left at frozen px
- Circular-slide guard skips fluid full-bleed sections so
viewport-spanning shrink-0 items keep fluid width
476 tests pass (4 new, with corroboration-guard negatives), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A grafted frame's position:fixed children re-resolved against the MAIN
page viewport and escaped the frame box's overflow clip (fixed ignores
overflow ancestors) - a full-viewport backdrop inside an embedded iframe
could paint across the entire page. Grafted subtrees now demote
fixed->absolute and sticky->relative, and a static graft host is promoted
to relative so demoted absolutes anchor and clip inside the frame box.
472 tests pass (4 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The IR's canonical tree came from one viewport, so DOM that exists only
at other widths (mobile-only pagination bullets, per-viewport product
sets) either vanished or left empty display:none shells.
- Unmatched children at non-canonical viewports graft into the IR at
deterministic anchored positions with per-viewport data scoped to their
source widths; emission gives them a base hide plus a reveal band
(hidden max-[...]:block), capped per parent, invisible subtrees skipped
- Whole-set identity drift (a container serving disjoint same-family
child sets per viewport) is detected by mutual mismatch; canonical
content stands in at the drift viewport instead of an empty shell, and
manifest.json records the divergence
- Per-item divergence resolves naturally via grafting: each viewport
shows exactly its captured content, mutually exclusive by banding
468 tests pass (8 new), typecheck clean, double-build byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-to-nearest 0.5% could land a nowrap flex row's width fractionally
below its intrinsic content width, wrapping it to two lines. When the
sizing probe shows a box painting at its max-content width with no slack,
the snap now ceils to the next step; all other boxes keep nearest
rounding, and the existing reproduce-verification still bails the plan on
any overshoot.
460 tests pass (2 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Lazy-loading semantics: an @font-face never matched by rendered text
stays "unloaded" forever - the readiness poll now waits only on faces
in "loading" state, ending the instant fetches settle (removes ~3s of
dead deadline per viewport on affected pages); unreferenced faces log
per-face informationally instead of warning
- Asset gate counts a failure only for real HTTP >= 400 or a failed
fetch whose target file is genuinely absent from the export - aborted
srcset-candidate image fetches with valid files on disk are excused,
matching the existing streaming-media treatment
458 tests pass (11 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The render walk and screenshots now await document.fonts.ready plus a
bounded state-based poll until every declared face is terminal
(loaded/error); pending families surface as a font_wait_warning event
instead of skewing text metrics with fallback-font boxes
- position:sticky full-width wrappers share the static/relative
fluid-fill branch (a sticky box fills its containing block like any
in-flow box) instead of freezing at canonical px; fixed-width sticky
rails still freeze
447 tests pass (12 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- A grid-cols-N recipe plan may only override the authored template when
computed tracks are near-equal - asymmetric sidebar templates
(260px 1fr) keep their authored geometry
- Source-intent named tokens (max-w-md etc.) re-emit only when their
Tailwind-4 value matches the captured computed px; mismatches (older
token scales) emit the captured value instead
- Per-axis source-variant selection keeps the highest-min-width active
variant instead of last-in-class-order, matching real Tailwind cascade
(lg:grid-cols-3 no longer loses to a later md:grid-cols-2 at desktop) -
latent since the initial commit
- New non-blocking textHiddenInClone metric in the dom gate: counts
capture-visible text that renders hidden in the clone, so silent
disappearances surface in reports
435 tests pass (10 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A progressively-growing feed panel (rows injected after cap tagging)
passed the rotator leaf guard - which only rejected containers with
CAPPED children - and the rotator runtime's textContent writes then
flattened 51 statically-emitted descendants to one text node. Three
layers: emission drops rotators whose IR target has element children;
the capture leaf guard rejects any element child (capped or not); the
DittoMotion runtime saves/restores cloned child nodes via
replaceChildren so even a misclassified rotator can't destroy structure.
425 tests pass (6 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Authored fixed h-/w- utilities from the source cascade, when the
captured geometry corroborates them, suppress fill/flow re-derivation
and emit as captured computed px per band (root-font-size independent) -
fixes logo tiles collapsing to h-full and replaced-element sizes
dropping to intrinsic
- aspectHeightLaw rejects ratios where the CSS aspect-ratio/max-height
width transfer would clamp width below the captured box, falling back to
per-band explicit dimensions
419 tests pass (4 new, each verified discriminating), typecheck clean,
double-regen byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Items never visible at the canonical viewport (carousel slides off-screen
at every captured width) now fall through to per-viewport geometry bands
when they occupy layout, instead of freezing canonical geometry
- The explicit-height probe trust excludes body/html/main and any wrapper
whose height matches the page scrollHeight - page-height nodes keep
flowing, and the layout height gate stays meaningful
- @font-face and style-rule url() harvesting resolves against the OWNING
stylesheet's href (walking @import nesting), not document.baseURI;
font bytes are magic-byte validated at the storage choke point (HTML
bodies from catch-all 200 routes are rejected); font-face dedup prefers
the variant whose source actually resolved, deterministically
415 tests pass (16 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Off-viewport in-flow items (carousel slides beyond the right edge) keep
their per-viewport geometry deltas - they occupy layout and set track
cross-sizes even when not visible
- Height chains: single-viewport probe verdicts accepted for nodes that
only paint at one band; un-probed picture/object-fit:cover children
filling the parent count as height-deriving; a node whose own probe says
authored-explicit keeps its height at emission (twin of the flow guard)
- Symmetric authored-width circular guard in the walker (height-only
before), harvesting shadow-root adopted/style sheets for custom elements
- Auto margins only when width is genuinely content-constrained; gutter-
inset full-width blocks keep literal margins
- grid-template-rows: subgrid never dropped; variant-only subgrid intent
re-enters via partial-coverage escape; grid-item percentage widths bail
to auto (they resolve against the track, not the container)
- Banded raw-prop detection counts non-raw band overrides so a banded
bg-none reset isn't beaten by an inlined gradient
399 tests pass (13 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v.load() resets the media element's show-poster flag; pause cancels
autoplay and the at-zero seek skip left nothing to clear it, so
screenshots after a source re-selection rendered the poster instead of
the new source's frame 0. Reloaded videos now force an epsilon seek
(awaiting seeked, which also serves as the readiness signal on slow
CDNs), and re-selection emits a video_source_reselected log event.
386 tests pass (4 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Walker serializes the composed (flattened) tree: open shadow roots walk
as host children with <slot> replaced by its assigned light nodes (or
fallback), so custom-element content (product cards, web components) is
captured instead of arriving childless
- In-flow visibility:hidden nodes with a nonzero border box survive the
prune as sized invisible placeholders - ghost spacers carry load-bearing
geometry
- When pruning drops leading in-flow children of a horizontally-translated
track, the baked translateX is re-anchored by their aggregate margin-box
width (settled carousel tracks return to origin instead of pushing real
content off-screen); animation-owned transforms unaffected
- Circular-shrink slide guard counts block-level fill children as
width-deriving regardless of wAuto (block auto-width IS fill), pinning
library-sized slide widths uniformly
- javascript: hrefs emit as "#" and the link gate normalizes both sides
382 tests pass (18 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Capture:
- Hover/focus states are driven with CDP CSS.forcePseudoState instead of
moving the real cursor - transparent full-viewport overlay layers were
swallowing every pointer hover, silently capturing zero hover states on
hover-rich sites
- text-wrap (balance/pretty) captured and emitted (Tailwind text-balance/
text-pretty, arbitrary fallback)
- Videos re-run <source> selection per viewport before frame-0
normalization: resize-without-reload kept aspect-gated variants stuck on
the load-time choice, poisoning mobile ground-truth screenshots
- svg roots capture their computed paint; fill="none" with a computed
paint (the fill-current pattern) recovers the real color instead of
rendering blank
Tokens & naming:
- Full CSS-Color-4 parsing (oklab/oklch/lab/lch/hsl) so modern colors
cluster and earn semantic roles; visually-equal literals share one token;
decoration/gradient/shadow colors consult the palette before minting
opaque tokens (ridge: 45 opaque tokens -> 15, anthropic: 12 -> 7)
- Expanded role vocabulary (background/foreground/primary/accent/border/
surface/muted) with deterministic tiebreaks and a chroma guard
- Section names mine CMS section ids and js-* hooks with hashy-suffix
stripping (split_callout_JtTWTt -> split-callout-section)
364 tests pass (42 new), typecheck clean; determinism verified by
double-regen byte-comparison on two reference runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Painting the body's color onto a transparent source html flips CSS 2.1
background propagation: body's background stops propagating to the canvas
and paints above negative-z descendants, burying full-bleed z<0 hero
backdrops under an opaque page background. Emit the html rule only when
the source html actually painted one; fall back to #ffffff only when both
html and body are transparent. Applies to the Next globals, Tailwind, and
multi-route Vite paths via shared helpers.
6 new tests; suite at 322 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Overlay detection resolves effective z-index through stacking ancestors,
treats a zero-size max-z wrapper plus its full-viewport descendant as one
removable unit, pierces open shadow roots, and relaxes the z floor when
the page is scroll-locked; a lock persisting after dismissal reports
blocking even with no detected overlay
- clickDismiss runs in every frame (close buttons live in cross-origin
popup iframes), matches decline/no-thanks/aria-label close affordances,
and re-runs before every snapshot AND screenshot to catch late-mounting
dialogs
- rootUnclamp triggers from IR content extent (a scroll-lock collapses
captured scrollHeight) and strips vendor-injected body position/overflow
locks when firing
- Pollution gate fails on the scroll-lock contradiction: scrollHeight
pinned to one viewport while IR content spans multiple
- Frame graft skips popup-creative vendor hosts; inline embedded signup
forms remain graftable (regression-tested)
322 tests pass (24 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- serveStatic answers single-range requests with proper 206/Content-Range
(416 for unsatisfiable), so Chromium streams large media in bounded
chunks instead of holding one throttled progressive request open past
the navigation budget
- goto policy: waitUntil "load" + up to 10s of sustained-quiet polling
replaces the hard networkidle timeout - long autoplaying media can no
longer make validation unreachable (applied to renderApp, probe widths,
and the interaction gate)
- validateRun catches render crashes and records them as runtime errors,
so a failed render writes a report with a failed gate 0 instead of
crashing reportless
292 tests pass (14 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Visible canvases (>=48px) are captured as PNG stills at capture time
(toDataURL, element-screenshot fallback for tainted/webgl) and emitted as
cid-carrying <img> elements filling the captured box, so canvas-driven
bands no longer render blank
- Source AND clone full-page screenshots now capture via CDP
Page.captureScreenshot with captureBeyondViewport: no scroll-stitching,
no scroll events, so scroll-linked animations can't smear mid-scrub
states into perceptual evidence (pixel-identical to the old path on
static pages; Playwright fallback retained)
- DittoLottie's runtime overlay stays position:absolute/inset:0 for its
whole life and the host carries data-ditto-lottie with a scoped
svg/canvas fit rule, so the runtime animation always fills the pinned
per-viewport box instead of escaping to aspect height
259 tests pass (12 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Nodes carrying an infinite CSS animation at ANY viewport get their frozen
mid-animation transform zeroed at EVERY viewport (the @keyframes owns the
property at runtime); animOwnedProps is now viewport-aware so breakpoint-
gated marquees can't bake a mid-scroll translateX into other bands
- contentSizedFlexRow/contentSizedFlexItemAuto honor the sizing probe: a
flex item whose captured width the probe proved load-bearing
(wAuto=false) is never converted to width:auto, preserving narrow
centered text columns
247 tests pass (7 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- heightFlows() bails when the capture probe proved a height authored-
explicit (hAuto=false, hFill=false), so circular parent/child flow
reasoning can no longer drop 100vh heroes to 0px; the probe verdict now
vetoes the drop through both OR'd paths at the call site
- Space-distributing flex columns (space-between/around/evenly, center,
flex-end) disqualify the content-driven read - the box height is load-
bearing for the distribution
- Lottie mounts pin their captured per-viewport height like other replaced
content, so aspect-fit runtime svgs can't inflate past the captured box
- DittoLottie forwards the placeholder's data-cid onto the runtime svg/
canvas so the media stays addressable after the swap
240 tests pass (9 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
detectSections gave up on real pages: <body> swallowed everything, a single
oversized-wrapper expansion pass required >=3 candidates, and a 62px navbar
failed the 64px height bar - so nearly every clone shipped as one section
and one monolithic component.
Rewritten around a shared recursive core (detectSectionNodes): containers
covering >50% of the page are wrappers to descend into; a split is accepted
only when >=2 stacked bands tile >=80% of the container (side-by-side
columns and overlays never split; truly single-band pages legally stay 1).
Semantic tags get a 24px bar with nav-evidence detection for thin bars.
The validator's section list and the emitted section components now consume
the same root set, so gate-5 bands and sections/*.tsx files always agree.
Naming is evidence-based and deterministic: navbar/header/hero (only when
actually near the top), recipe names (logo-cloud, product-grid...), heading
slugs, contact/media, positional fallback.
Frozen-capture probes: 1 -> 6..17 sections across the reference runs, with
the old whole-page "hero-section" monoliths splitting into honest bands.
231 tests pass (13 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Capture:
- Preserve real asset extensions (.lottie no longer truncated); extract the
default animation JSON from dotLottie ZIP containers at materialization
(minimal built-in ZIP reader, no new deps)
- Reset scroll + settle before every viewport snapshot so scroll-linked
styles can't bake into captured computed values
- Marquee detection now requires content overflow (>1.35x), sustained
velocity across separated windows, scroll independence (jiggle test), and
genuine content repetition — kills scroll-settle-lerp false positives
- Sizing probe trusts authored explicit heights (px/vh/calc) over circular
parent/child reflow verdicts, fixing dropped 100vh heroes
Normalize/generate:
- Canonicalize identity transforms to "none" per viewport; emit explicit
transform resets across bands when any band has a real transform
- DittoLottie keeps the captured placeholder until DOMLoaded; failed loads
no longer blank the container
- mx-auto only for true auto-margin centering; literal per-band margins are
preserved (fixes full-bleed regressions)
- Spacing-scale snap tightened to 0.25px (3.5px stays p-[3.5px]); emit
whitespace-nowrap for wrap-vulnerable single-line text leaves
- Grid recipes no longer override captured track counts or column spans;
responsive re-flow plans apply only when geometry agrees
Quality scoring:
- Rewritten 6-dimension rubric (payload, decomposition, duplication,
semantics, hygiene, runtime) with catastrophe caps and per-dimension
reporting; thresholds are documented calibration guides
218 tests pass (74 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- DittoWire/Accordion/DropdownMenu runtime templates: AbortController-based
listener cleanup, idempotent effects, orphan panel removal on unmount
- Emit a single src/lib/utils.ts cn() instead of a copy per component file
- Collapse captured whitespace runs under white-space:normal (pre* preserved)
- Tag source-injected font-metric probe nodes at capture; drop them from IR
- Snap sub-pixel arbitrary lengths (integer within 0.1px, else 1 decimal)
- Emit SITE_ORIGIN (src/lib/site.ts, env-overridable): sitemap/robots/
canonical/og:url/JSON-LD resolve against the clone's own origin instead of
the source domain
- Pin lottie-web in generated package.json whenever DittoLottie is emitted
144 tests pass (21 new), typecheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three clone-fidelity fixes from manual review of the acceptance screenshots.
Fix 1 — mobile nav chip-strip overlap (generate/css.ts). A horizontally
scrollable flex strip (overflow-x:auto flex <ul>) of nowrap chips collapsed
because the base viewport reported min-width:0px on the chips (a mobile-only
strip that is 0-wide at desktop), so the generator emitted `min-w-0`, letting
the chips shrink below their content instead of overflowing — the nowrap text
then collided. Suppress `min-w-0` for a nowrap flex item whose flex parent
scrolls horizontally; scoped away from legitimate min-w-0 truncation (whose
parent does not scroll-x). Verified on a fresh ooni.com clone: 0 chip overlaps
at 375, scrollWidth 776 > clientWidth 375.
Fix 2 — reveal-replay stagger leaves tiles unpainted (generate/motion.ts).
DittoMotion re-hid each revealed element and replayed its entrance on
scroll-into-view preserving the captured delay/duration, so a fast scroll /
full-page screenshot caught most grid tiles mid-entrance. Cap the replayed
delay (<=300ms) and duration (<=600ms), settle each tile per-element a bounded
time after it enters view, and make the global failsafe settle (not re-animate)
so nothing stays hidden. Validator settle path (__dittoMotionStop) unchanged.
Verified: cropin cotton renders 12/12 crop tiles in a normal full-page pass;
motion gate reveals 24/24.
Fix 3 — scroll-linked text-fill frozen at end state (capture/stabilize.ts,
capture/capture.ts, generate/css.ts). A view-timeline text-fill
(animation-duration resolves to `auto`) was baked filled: the clone has no
scroll timeline, so emitting the animation made it jump to its end keyframe via
fill-mode:both. Capture-side, cancel scroll/view-timeline animations before each
snapshot so the DOM records the at-rest state; generation-side, suppress the
animation-* props when animation-duration is `auto` (we render the at-rest
state, never replay a scroll-linked animation). Teach the motion gate to exclude
these from the static-CSS expectation so the gate stays honest. Verified on a
fresh ooni clone: the "world's no.1 pizza ovens" em is white/gray (rgb 240,240,
240), not filled yellow, at rest; motion gate still passes.
Tests: +7 fixtures (css scroll-strip + scroll-timeline, reveal-replay caps,
motion-gate scroll-timeline exclusion); 123/123 compiler tests green, full test
suite green, typecheck clean. No gate regression on the fresh ooni run vs the
20260704-024247 baseline (all gate pass/fail unchanged; perceptual within
0.0004; motion restored to pass).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- motionGate: a captured CSS entrance animation the clone replays through
a DittoMotion reveal (re-hide on mount, restart @keyframes on scroll)
is graded by the scroll-reveal check, not the static-CSS expectation —
previously every such cid failed emitted=false. Also drive every
captured reveal instead of the first 16 (pages with more reveals could
never pass; this was the spurious "reveals 16/24").
- render/harness: the validator copies app sources over the preinstalled
harness without installing deps, so a Lottie clone's injected
lottie-web crashed webpack ("build failed" with no real signal).
Missing app deps are now installed into the harness pinned to the
app's exact version (--no-save, idempotent), with a clear gate issue
on install failure.
Cotton motion gate: 33 static + 10 replayed + 24/24 reveals -> pass.
Cropin home build gate: pass (was webpack Module not found).
116/116 compiler tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-3 fixes from gate validation + human visual review of fresh
cropin.com and ooni.com clones:
- app.ts: section data-var rename used an unanchored substring replace,
so Tile2_data matched inside MediaTile2_data and emitted a corrupted
Mediatile2Data usage (ReferenceError at prerender). Word-boundary
regex; every map site now shares one derivation.
- graft.ts: about:blank iframes were blanket-skipped, missing the
Klaviyo newsletter form (mounted into a blank same-origin frame via
JS) — the audit's #1 complaint. Blank frames now graft; the
visibility gate still excludes 0-size tracking pixels.
- walker.ts: isVisible() now rejects boxes wholly outside the viewport
(off-left always; off-right only when the page isn't horizontally
scrollable; fixed boxes fully above/below), so a closed slide-in
drawer's contents no longer count as expected text (91.8% -> 93.4%).
- css.ts: four sizing-regime corrections — pin captured px for
circular shrink-0 slides (Splide slide chain collapsed 0x0);
flex-basis:100% for full-width shrink-0 slides; keep fixed-px grid
templates for scrolling track lists (repeat(N,1fr) squished a
50-track carousel); single full-bleed fixed track -> minmax(0,1fr);
keep authored heights whose in-flow children are fill children
(aspect-video heroes inflated 240 -> 720px); width:100% for
inset-spanned aspect boxes.
Gate scores: ooni home 88.7 -> 93.3 (responsive 32 fails -> pass),
pizza-ovens 90.8 -> 97.6 (perceptual 46.7% -> 17.2%).
107/107 compiler tests, all workspaces green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Capture (4a):
- stabilize: dwell-scroll settling pass + known-library pre-reveal
neutralization after lazy promotion, so every viewport snapshot records
the post-reveal steady state (about-page sections no longer captured
blanket-hidden: 24 base `invisible` wrappers -> 0)
- probeReveals: extended to the visibility + entrance-animation family;
DittoMotion replays them (JS re-hide on mount, IntersectionObserver
reveal, 4s force-reveal failsafe) so SSR/non-JS still shows content
- assets: 206 range-response bodies are no longer stored as full assets
(full-fetch fallback) + video magic-byte validation - the about-page
hero video is now a valid 6.0MB ftyp mp4 instead of 18.9KB of garbage
Generate (4b):
- css: hidden-node geometry policy - a visibility:hidden box with a 0x0
captured bbox at a band becomes display:none there; an OCCUPYING hidden
box gets the captured per-viewport geometry instead of the baked
canonical one. Also covers the ancestor-hidden-at-base case (the
cropin.com/cotton slider arrow: hide inherited from an elementor-widget
ancestor at every width parked a desktop left:548px box inside a 375px
viewport -> body.scrollWidth 585; now 375 on both verified pages)
- tailwind: bare-zero utility gating - font-size:0 -> text-[0px] and
letter-spacing:0 -> tracking-[0px] (text-0/tracking-0 are silently
invalid in Tailwind v4; fixes permanently-visible map labels on the
cotton Global-presence section)
Verified: 87/87 compiler tests green, full-workspace suite green,
regenerated cropin.com + /cotton/ builds measure 375px scrollWidth at a
375px viewport, and a fresh /about/ clone renders all reveal sections
with scroll-replay against the live site.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>