diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts
index 2715ff5a9c..6ee7f65713 100644
--- a/packages/lint/src/rules/gsap.test.ts
+++ b/packages/lint/src/rules/gsap.test.ts
@@ -1698,4 +1698,25 @@ describe("GSAP rules", () => {
const finding = result.findings.find((f) => f.code === "gsap_non_transform_motion");
expect(finding).toBeUndefined();
});
+
+ it("scene_layer_missing_visibility_kill: does NOT mistake fromTo entrance opacity for an exit", async () => {
+ const html = `
+
+
+
+`;
+ const result = await lintHyperframeHtml(html);
+ const finding = result.findings.find((f) => f.code === "scene_layer_missing_visibility_kill");
+ expect(finding).toBeUndefined();
+ });
});
diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts
index b54b7c8732..0ce612fd04 100644
--- a/packages/lint/src/rules/gsap.ts
+++ b/packages/lint/src/rules/gsap.ts
@@ -1020,9 +1020,16 @@ export const gsapRules: LintRule[] = [
// For each scene, check if there's a visibility:hidden set after exit tweens
for (const tag of sceneElements) {
const id = readAttr(tag.raw, "id") || "";
- // Check if this scene has exit tweens (opacity: 0)
- const exitPattern = new RegExp(`["']#${id}["'][^)]*opacity\\s*:\\s*0`);
- const hasExit = exitPattern.test(content);
+ // Check for an actual exit tween. A fromTo entrance often puts
+ // opacity:0 in its FIRST vars object; that is not an exit and the final
+ // scene must remain visible through the last frame.
+ const toExitPattern = new RegExp(
+ `\\.to\\(\\s*["']#${id}["']\\s*,\\s*\\{[^}]*opacity\\s*:\\s*0`,
+ );
+ const fromToExitPattern = new RegExp(
+ `\\.fromTo\\(\\s*["']#${id}["']\\s*,\\s*\\{[^}]*\\}\\s*,\\s*\\{[^}]*opacity\\s*:\\s*0`,
+ );
+ const hasExit = toExitPattern.test(content) || fromToExitPattern.test(content);
if (!hasExit) continue;
// Check if there's a hard visibility kill