diff --git a/compiler/fixtures/reveal.html b/compiler/fixtures/reveal.html
new file mode 100644
index 0000000..70ecc9a
--- /dev/null
+++ b/compiler/fixtures/reveal.html
@@ -0,0 +1,49 @@
+
+
+
+
+Reveal fixture
+
+
+
+Reveal fixture
+
+
+
+
Scroll-revealed content (IntersectionObserver)
+
+
+
+
+
Reveal keyed to a non-scroll trigger
+
+
+permanently hidden
+
+
+
diff --git a/compiler/src/generate/css.ts b/compiler/src/generate/css.ts
index 5df215c..b343609 100644
--- a/compiler/src/generate/css.ts
+++ b/compiler/src/generate/css.ts
@@ -3012,24 +3012,41 @@ export function collectNodeRules(ir: IR, assetMap: Map, includeN
// (declsForViewport emits `visibility:hidden`; parent-equality keeps it own-only).
} else if (ownNone || !node.visibleByVp[b.vp]) {
const shownAtBase = node.visibleByVp[baseVp] && (node.computedByVp[baseVp]?.display || "") !== "none";
- if (ownNone) {
- // Own display:none removes the box from layout entirely. Emit the hide even when the node
- // is ALSO hidden at base — a visibility:hidden base still bakes an OCCUPYING box (see
- // above), so without this band the canonical geometry would render at this width. Skip
- // only when the base itself is display:none (the band would be redundant).
- if ((node.computedByVp[baseVp]?.display || "") !== "none") {
+ // Hidden by an ANCESTOR's visibility here AND at base (the ancestor's own rule carries
+ // the hide at every width) — but a visibility:hidden box still PARTICIPATES in layout,
+ // and the base rule bakes CANONICAL geometry. Same policy as the own-hidden path above:
+ // a 0x0 box gets display:none; an occupying box falls through to the per-viewport delta
+ // so it sits where the capture measured it at THIS width — not parked at e.g. a desktop
+ // left:548px inside a 375px viewport (the cropin.com/cotton slider arrow, +210px of
+ // sideways scroll at 375 with the hide inherited from an elementor-widget ancestor).
+ const ancestorHiddenHere = !ownNone && /^(hidden|collapse)$/.test(vpCs.visibility || "");
+ if (ancestorHiddenHere && !shownAtBase) {
+ const bb = node.bboxByVp[b.vp];
+ if (!bb || bb.width <= 0 || bb.height <= 0) {
nr.bands.push({ media: b.media, decls: new Map([["display", "none"]]) });
+ continue;
}
- } else if (shownAtBase) {
- // Hidden by an ancestor (or zero-size / opacity:0): geometry overrides are breakpoint
- // noise — the ancestor's own hide (or the reveal replay, for scroll-reveal opacity)
- // covers it. Emit only the hide the node itself carries.
- const hide = new Map();
- if (pf(vpCs.opacity) === 0 && !animOwned.has("opacity")) hide.set("opacity", "0");
- if (/^(hidden|collapse)$/.test(vpCs.visibility || "")) hide.set("visibility", "hidden");
- if (hide.size) nr.bands.push({ media: b.media, decls: hide });
+ // Occupying: fall through to the normal per-viewport delta below.
+ } else {
+ if (ownNone) {
+ // Own display:none removes the box from layout entirely. Emit the hide even when the node
+ // is ALSO hidden at base — a visibility:hidden base still bakes an OCCUPYING box (see
+ // above), so without this band the canonical geometry would render at this width. Skip
+ // only when the base itself is display:none (the band would be redundant).
+ if ((node.computedByVp[baseVp]?.display || "") !== "none") {
+ nr.bands.push({ media: b.media, decls: new Map([["display", "none"]]) });
+ }
+ } else if (shownAtBase) {
+ // Hidden by an ancestor (or zero-size / opacity:0) but visible at base: geometry
+ // overrides are breakpoint noise — the ancestor's own hide (or the reveal replay,
+ // for scroll-reveal opacity) covers it. Emit only the hide the node itself carries.
+ const hide = new Map();
+ if (pf(vpCs.opacity) === 0 && !animOwned.has("opacity")) hide.set("opacity", "0");
+ if (/^(hidden|collapse)$/.test(vpCs.visibility || "")) hide.set("visibility", "hidden");
+ if (hide.size) nr.bands.push({ media: b.media, decls: hide });
+ }
+ continue;
}
- continue;
}
const centeredVp = stableCenter || (layoutParent ? centeredAtVp(node, layoutParent, b.vp) : false);
const vpDecls = finalizeDecls(declsForViewport(node, parentNode?.computedByVp[b.vp], b.vp, assetMap, centeredVp, colorVar, ir.doc.perViewport[b.vp]?.scrollHeight, widthPlan, gridColsByVp?.get(b.vp), gridRowsByVp?.get(b.vp), flowH, dropInsets, leftPct, heightFill, geometry, dropGridRows, dropViewportMaxWidth), tokenResolver);
diff --git a/compiler/test/css.test.ts b/compiler/test/css.test.ts
index 7877602..ad68257 100644
--- a/compiler/test/css.test.ts
+++ b/compiler/test/css.test.ts
@@ -100,3 +100,136 @@ describe("generateCss visibility", () => {
assert.ok(!baseRule(css, "n2").includes("visibility"));
});
});
+
+// Hidden-node banded geometry. A `visibility:hidden` box still PARTICIPATES in layout (unlike
+// `display:none`), so the emitter must not let the base rule's baked CANONICAL geometry stand at
+// widths where the capture measured something else — that is how a desktop `left:548px` slider
+// arrow ends up parked, invisibly, 210px past the right edge of a 375px viewport.
+describe("generateCss hidden-node banded geometry", () => {
+ const HVPS = [375, 1280, 1920];
+ type VpState = { cs?: StyleMap; bbox?: BBox; visible?: boolean };
+ function nodeAt(id: string, tag: string, byVp: Record, children: IRChild[] = []): IRNode {
+ const computedByVp: Record = {};
+ const bboxByVp: Record = {};
+ const visibleByVp: Record = {};
+ for (const vp of HVPS) {
+ const s = byVp[vp] ?? {};
+ computedByVp[vp] = computed(s.cs);
+ bboxByVp[vp] = s.bbox ?? { x: 0, y: 0, width: vp, height: 100 };
+ visibleByVp[vp] = s.visible ?? true;
+ }
+ return { id, tag, attrs: {}, visibleByVp, bboxByVp, computedByVp, children };
+ }
+ function ir3(root: IRNode): IR {
+ const ir = irWith(root);
+ ir.doc.viewports = HVPS;
+ ir.doc.sampleViewports = HVPS;
+ ir.doc.perViewport = Object.fromEntries(HVPS.map((vp) => [vp, { scrollHeight: 800, scrollWidth: vp, htmlBg: "rgb(255, 255, 255)", bodyBg: "rgb(255, 255, 255)", bodyColor: "rgb(0, 0, 0)", bodyFont: "Arial" }]));
+ return ir;
+ }
+ /** The `.c{…}` body inside the first @media block whose query matches `mediaRe`. */
+ function bandRule(css: string, mediaRe: RegExp, id: string): string {
+ for (const m of css.matchAll(/@media ([^{]+) \{\n([\s\S]*?)\n\}/g)) {
+ if (!mediaRe.test(m[1]!)) continue;
+ const r = m[2]!.match(new RegExp(`\\.c${id}\\{([^}]*)\\}`));
+ if (r) return r[1]!;
+ }
+ return "";
+ }
+ const arrowCs = (left: string, hidden: boolean): StyleMap =>
+ ({ display: "flex", position: "absolute", left, ...(hidden ? { visibility: "hidden" } : {}) });
+
+ it("emits the captured per-band geometry for a box its own visibility:hidden leaves occupying space", () => {
+ // Hidden at base AND at the mobile band with DIFFERENT lefts (the swiper-arrow shape): the
+ // band must carry the mobile left, not inherit the baked canonical one.
+ const arrow = nodeAt("n1", "div", {
+ 375: { cs: arrowCs("37px", true), bbox: { x: 37, y: 0, width: 46, height: 46 }, visible: false },
+ 1280: { cs: arrowCs("548px", true), bbox: { x: 548, y: 0, width: 46, height: 46 }, visible: false },
+ 1920: { cs: arrowCs("588px", false), bbox: { x: 588, y: 0, width: 46, height: 46 } },
+ });
+ const root = nodeAt("n0", "body", { 1280: { cs: { position: "relative" } } }, [arrow]);
+ const css = generateCss(ir3(root), new Map());
+ assert.ok(baseRule(css, "n1").includes("visibility:hidden"));
+ assert.ok(baseRule(css, "n1").includes("left:548px"));
+ const mobile = bandRule(css, /max-width/, "n1");
+ assert.ok(mobile.includes("left:37px"), `mobile band should carry the captured left, got: ${mobile}`);
+ assert.ok(!mobile.includes("display:none"), "an occupying hidden box must stay in layout");
+ // The wide band where the node becomes visible keeps working: visibility restored + its left.
+ const wide = bandRule(css, /min-width/, "n1");
+ assert.ok(wide.includes("visibility:inherit"), `wide band should restore visibility, got: ${wide}`);
+ assert.ok(wide.includes("left:588px"), `wide band should carry the 1920 left, got: ${wide}`);
+ });
+
+ it("hides a visibility:hidden box whose captured bbox is 0x0 with display:none at that band", () => {
+ // At 375 the hidden arrow occupied NOTHING (uninitialised swiper) — display:none reproduces
+ // "renders nothing, takes no space" and cannot extend the scrollable area.
+ const arrow = nodeAt("n1", "div", {
+ 375: { cs: arrowCs("calc(50% - 52px)", true), bbox: { x: 0, y: 0, width: 0, height: 0 }, visible: false },
+ 1280: { cs: arrowCs("548px", true), bbox: { x: 548, y: 0, width: 46, height: 46 }, visible: false },
+ 1920: { cs: arrowCs("588px", false), bbox: { x: 588, y: 0, width: 46, height: 46 } },
+ });
+ const root = nodeAt("n0", "body", { 1280: { cs: { position: "relative" } } }, [arrow]);
+ const css = generateCss(ir3(root), new Map());
+ const mobile = bandRule(css, /max-width/, "n1");
+ assert.ok(mobile.includes("display:none"), `0x0 hidden band should be display:none, got: ${mobile}`);
+ const wide = bandRule(css, /min-width/, "n1");
+ assert.ok(wide.includes("visibility:inherit") && wide.includes("left:588px"));
+ });
+
+ it("emits display:none at a band where the node turns display:none even when hidden at base", () => {
+ // The base rule bakes an OCCUPYING visibility:hidden box (canonical geometry); without the
+ // band that box would render at mobile widths where the source had display:none.
+ const wrap = nodeAt("n1", "div", {
+ 375: { cs: { display: "none", visibility: "hidden" }, bbox: { x: 0, y: 0, width: 0, height: 0 }, visible: false },
+ 1280: { cs: { visibility: "hidden" }, bbox: { x: 40, y: 0, width: 1200, height: 574 }, visible: false },
+ 1920: { cs: {}, bbox: { x: 320, y: 0, width: 1280, height: 533 } },
+ });
+ const root = nodeAt("n0", "body", {}, [wrap]);
+ const css = generateCss(ir3(root), new Map());
+ assert.ok(baseRule(css, "n1").includes("visibility:hidden"));
+ const mobile = bandRule(css, /max-width/, "n1");
+ assert.ok(mobile.includes("display:none"), `own display:none band must emit even when hidden at base, got: ${mobile}`);
+ });
+
+ it("emits per-band geometry for an occupying box an ancestor hides at base AND at the band", () => {
+ // The cropin.com/cotton slider arrow: the elementor-widget ANCESTOR is visibility:hidden at
+ // 375/1280 (so the arrow is never ownHidden and never shownAtBase) yet the arrow's absolute
+ // box still occupies layout. Without a band the base's canonical left:548px parks it 210px
+ // past the right edge of a 375px viewport — the band must carry the captured mobile left.
+ const arrow = nodeAt("n2", "div", {
+ 375: { cs: arrowCs("120px", true), bbox: { x: 112, y: 0, width: 46, height: 46 }, visible: false },
+ 1280: { cs: arrowCs("548px", true), bbox: { x: 548, y: 0, width: 46, height: 46 }, visible: false },
+ 1920: { cs: arrowCs("588px", false), bbox: { x: 588, y: 0, width: 46, height: 46 } },
+ });
+ const parent = nodeAt("n1", "div", {
+ 375: { cs: { position: "relative", visibility: "hidden" }, visible: false },
+ 1280: { cs: { position: "relative", visibility: "hidden" }, visible: false },
+ 1920: { cs: { position: "relative" } },
+ }, [arrow]);
+ const root = nodeAt("n0", "body", {}, [parent]);
+ const css = generateCss(ir3(root), new Map());
+ const mobile = bandRule(css, /max-width/, "n2");
+ assert.ok(mobile.includes("left:120px"), `mobile band should carry the captured left, got: ${mobile}`);
+ assert.ok(!mobile.includes("display:none"), "an occupying hidden box must stay in layout");
+ });
+
+ it("still emits only the hide for a box an ANCESTOR's visibility:hidden covers", () => {
+ // Inherited hides stay breakpoint noise: the ancestor's own rule (and its geometry
+ // correction) covers the subtree — the child emits its hide, not geometry overrides.
+ const child = nodeAt("n2", "div", {
+ 375: { cs: { position: "absolute", left: "10px", visibility: "hidden" }, bbox: { x: 10, y: 0, width: 40, height: 40 }, visible: false },
+ 1280: { cs: { position: "absolute", left: "500px" }, bbox: { x: 500, y: 0, width: 40, height: 40 } },
+ 1920: { cs: { position: "absolute", left: "500px" }, bbox: { x: 500, y: 0, width: 40, height: 40 } },
+ });
+ const parent = nodeAt("n1", "div", {
+ 375: { cs: { position: "relative", visibility: "hidden" }, visible: false },
+ 1280: { cs: { position: "relative" } },
+ 1920: { cs: { position: "relative" } },
+ }, [child]);
+ const root = nodeAt("n0", "body", {}, [parent]);
+ const css = generateCss(ir3(root), new Map());
+ const mobile = bandRule(css, /max-width/, "n2");
+ assert.ok(mobile.includes("visibility:hidden"), `child should carry the hide, got: ${mobile}`);
+ assert.ok(!mobile.includes("left:10px"), `ancestor-hidden child must not emit geometry, got: ${mobile}`);
+ });
+});
diff --git a/compiler/test/range206.test.ts b/compiler/test/range206.test.ts
new file mode 100644
index 0000000..767674f
--- /dev/null
+++ b/compiler/test/range206.test.ts
@@ -0,0 +1,102 @@
+import { describe, it, before, after } from "node:test";
+import assert from "node:assert/strict";
+import { createServer, type Server } from "node:http";
+import { mkdtempSync, readFileSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { captureSite, looksLikeVideoFile, type CaptureResult } from "../src/capture/capture.js";
+
+// A minimal-but-valid mp4 shape: [size]"ftyp" brand box followed by payload bytes.
+const FULL_MP4 = Buffer.concat([
+ Buffer.from([0x00, 0x00, 0x00, 0x18]),
+ Buffer.from("ftypisom"),
+ Buffer.from([0x00, 0x00, 0x02, 0x00]),
+ Buffer.from("isomiso2"),
+ Buffer.alloc(64_000, 0x07),
+]);
+// The pathological range fragment observed in the wild: a tail slice (moov atom region)
+// of the file — starts mid-container, no ftyp/EBML magic.
+const TAIL_FRAGMENT = FULL_MP4.subarray(FULL_MP4.length - 1000);
+
+describe("looksLikeVideoFile (container magic)", () => {
+ it("accepts mp4-family (ftyp), webm/mkv (EBML), and ogg (OggS) heads", () => {
+ assert.equal(looksLikeVideoFile(FULL_MP4), true);
+ assert.equal(looksLikeVideoFile(Buffer.concat([Buffer.from([0x1a, 0x45, 0xdf, 0xa3]), Buffer.alloc(100)])), true);
+ assert.equal(looksLikeVideoFile(Buffer.concat([Buffer.from("OggS"), Buffer.alloc(100)])), true);
+ });
+
+ it("rejects range fragments, HTML error bodies, and tiny buffers", () => {
+ assert.equal(looksLikeVideoFile(TAIL_FRAGMENT), false, "moov-tail fragment is not a video file");
+ assert.equal(looksLikeVideoFile(Buffer.from("404")), false);
+ assert.equal(looksLikeVideoFile(Buffer.from([0x00, 0x00])), false);
+ });
+});
+
+// A