Clone-fidelity fix waves 1-3 (+ wave 4 in flight): capture settling, media, iframes

Fixes from the cropin.com / ooni.com audit reviews:
- walker: preserve whitespace-only text in inline elements ("ofthe" bug);
  capture ::placeholder styles for inputs
- css: emit visibility (hidden-at-rest wordmark artifact); exempt list
  markers from inherited elision (lost bullets); ::placeholder rules
- seo: sanitize og:type to Next's enum (render crash -> dev badge leak);
  generated next.config disables devIndicators
- capture/stabilize: promote lazy-loader data attrs before snapshots
  (collapsed sections, viewport-inconsistent captures); settle autoplay
  carousels to home slide before every snapshot; force-reveal hidden
  videos for poster shots + log failures
- generate: ship downloaded video files as local <video src>; keep
  picture>source through IR prune with srcset rewrite (mobile-crop-on-
  desktop heroes); pin Tailwind named screens to computeBands boundaries
- capture/graft: capture cross-origin iframe subtrees (Klaviyo signup
  forms) with element-screenshot fallback; asset download retry +
  visual-assets-missing reporting

Checkpoint commit: wave 4 (reveal settling, 206 range-response fix,
hidden-geometry banding, bare-zero utility gating) is mid-implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samraaj Bath
2026-07-03 17:45:42 -07:00
co-authored by Claude Fable 5
parent da537e68b8
commit 9aed2540aa
31 changed files with 2579 additions and 121 deletions
+83
View File
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Autoplay carousel fixture</title>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; color: #1a1a1a; }
main { max-width: 720px; margin: 0 auto; padding: 40px 24px; }
/* Splide-like autoplaying carousel: clipped track, flex list moved by translateX,
pagination bullets, pause-on-hover (the library default this fixture mimics). */
.splide { position: relative; }
.splide__track { overflow: hidden; border-radius: 10px; }
.splide__list { display: flex; transition: transform .2s ease; margin: 0; padding: 0; list-style: none; }
.splide__slide { flex: 0 0 100%; min-width: 100%; height: 200px; display: flex; align-items: center; justify-content: center; font-size: 34px; color: #fff; }
.a0 { background: #4f46e5; } .a1 { background: #0891b2; } .a2 { background: #16a34a; } .a3 { background: #db2777; }
.splide__pagination { display: flex; gap: 8px; justify-content: center; margin-top: 12px; padding: 0; list-style: none; }
.splide__pagination__bullet { width: 10px; height: 10px; border-radius: 50%; border: 0; padding: 0; cursor: pointer; background: #d1d5db; }
.splide__pagination__bullet.is-active { background: #4f46e5; }
/* A control-less advanced track (what an interaction pass can leave behind):
no bullets, no arrows, not loop-mode — settle must pin it back to home. */
.swiper { overflow: hidden; border-radius: 10px; margin-top: 32px; }
.swiper-wrapper { display: flex; }
.swiper-slide { flex: 0 0 100%; min-width: 100%; height: 120px; display: flex; align-items: center; justify-content: center; font-size: 24px; color: #fff; background: #ea580c; }
</style>
</head>
<body>
<main>
<h1>Autoplay carousel</h1>
<div id="auto" class="splide" aria-roledescription="carousel" aria-label="Autoplay">
<div class="splide__track">
<ul class="splide__list">
<li class="splide__slide a0 is-active">Slide 1</li>
<li class="splide__slide a1">Slide 2</li>
<li class="splide__slide a2">Slide 3</li>
<li class="splide__slide a3">Slide 4</li>
</ul>
</div>
<ul class="splide__pagination">
<li><button class="splide__pagination__bullet is-active" aria-label="Go to slide 1"></button></li>
<li><button class="splide__pagination__bullet" aria-label="Go to slide 2"></button></li>
<li><button class="splide__pagination__bullet" aria-label="Go to slide 3"></button></li>
<li><button class="splide__pagination__bullet" aria-label="Go to slide 4"></button></li>
</ul>
</div>
<div id="stuck" class="swiper">
<div class="swiper-wrapper" style="transform: translateX(-100%);">
<div class="swiper-slide">A</div>
<div class="swiper-slide">B</div>
<div class="swiper-slide">C</div>
</div>
</div>
</main>
<script>
(() => {
const root = document.getElementById("auto");
const list = root.querySelector(".splide__list");
const slides = [...root.querySelectorAll(".splide__slide")];
const bullets = [...root.querySelectorAll(".splide__pagination__bullet")];
let i = 0;
let paused = false;
const go = (n) => {
i = ((n % slides.length) + slides.length) % slides.length;
list.style.transform = "translateX(-" + (i * 100) + "%)";
slides.forEach((s, k) => s.classList.toggle("is-active", k === i));
bullets.forEach((b, k) => b.classList.toggle("is-active", k === i));
};
bullets.forEach((b, k) => b.addEventListener("click", () => go(k)));
// pause-on-hover, the Splide/Slick default this fixture mimics
root.addEventListener("mouseenter", () => { paused = true; });
root.addEventListener("mouseleave", () => { paused = false; });
window.__autoplayTicks = 0;
setInterval(() => { if (!paused) { window.__autoplayTicks++; go(i + 1); } }, 250);
})();
</script>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Newsletter embed</title>
<style>
body { margin: 0; background: #1a1a2e; color: #ffffff; font-family: Arial, sans-serif; }
form { padding: 12px; }
label { display: block; margin-bottom: 6px; font-size: 13px; }
input { display: block; width: 200px; color: #ffffff; background: #333344; border: 1px solid #555577; padding: 6px; }
input::placeholder { color: rgb(200, 150, 100); }
button { margin-top: 8px; background: #e94560; color: #ffffff; border: 0; padding: 8px 16px; }
a { color: #8888ff; font-size: 12px; }
</style>
</head>
<body>
<form id="signup" action="/subscribe">
<label for="email">Email</label>
<input id="email" type="email" placeholder="Enter your email">
<button type="submit">Sign up</button>
<a href="#terms">Terms</a>
<img src="/pixel.png" alt="" width="10" height="10">
</form>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Iframe host</title>
<style>
body { margin: 0; font-family: Arial, sans-serif; background: #ffffff; }
h1 { margin: 20px; font-size: 24px; }
iframe.signup {
display: block;
margin: 30px 0 0 40px;
width: 320px;
height: 160px;
border: 0;
}
</style>
</head>
<body>
<h1>Host page</h1>
<!-- {{FRAME_ORIGIN}} is replaced by the test server with the second (cross-origin) server's origin. -->
<iframe class="signup" title="Newsletter" aria-label="Modal Overlay Box Frame" src="{{FRAME_ORIGIN}}/iframe-embed.html"></iframe>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lazy-media fixture</title>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; padding: 24px; }
img { display: block; }
.bg { width: 300px; height: 150px; background-size: cover; background-repeat: no-repeat; }
</style>
</head>
<body>
<h1>Lazy media</h1>
<!-- WP Rocket style: 0-size placeholder in src, real URL in data-lazy-src. -->
<img id="lazy1" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-lazy-src="og-image.png" alt="hero">
<!-- lazysizes style: no src at all, data-src + data-sizes="auto" (a swap flag, not a value). -->
<img id="lazy2" data-src="seo-icon.png" data-sizes="auto" alt="icon">
<picture>
<source id="lazysource" type="image/png" data-srcset="og-image.png 1x">
<img id="lazy3" data-src="og-image.png" alt="picture">
</picture>
<!-- Lazy background image (WP Rocket url(...) form). -->
<div id="lazybg" class="bg" data-bg="url('brand.svg')"></div>
<!-- Must NOT be promoted: the data attr is not a plausible URL. -->
<img id="notaurl" data-src="{{ placeholder }}" alt="template token">
<!-- Must NOT count as a promotion: src already equals the target. -->
<img id="already" src="seo-icon.png" data-src="seo-icon.png" alt="already swapped">
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Placeholder styling</title>
<style>
body { margin: 0; font-family: Arial, sans-serif; }
input.styled { color: rgb(10, 20, 30); font-size: 16px; }
input.styled::placeholder { color: rgb(120, 30, 200); font-size: 14px; }
textarea.styled::placeholder { color: rgb(5, 100, 5); }
</style>
</head>
<body>
<input class="styled" type="email" placeholder="Enter your email">
<textarea class="styled" placeholder="Your message"></textarea>
<input class="plain" type="text" value="no placeholder here">
</body>
</html>