diff --git a/marketing/product-film/src/components/Crane.tsx b/marketing/product-film/src/components/Crane.tsx
new file mode 100644
index 0000000..6596614
--- /dev/null
+++ b/marketing/product-film/src/components/Crane.tsx
@@ -0,0 +1,52 @@
+// Blueprint tower-crane line art, ported from src/pages/Landing.jsx (dark-theme colors inlined).
+const STRONG = "rgba(255,255,255,0.16)";
+const MEDIUM = "rgba(255,255,255,0.10)";
+const SUBTLE = "rgba(255,255,255,0.06)";
+const SURFACE = "rgba(255,255,255,0.03)";
+
+export const Crane: React.FC<{ width?: number }> = ({ width = 1100 }) => (
+
+);
diff --git a/marketing/product-film/src/components/CraneCard.tsx b/marketing/product-film/src/components/CraneCard.tsx
new file mode 100644
index 0000000..2c253cc
--- /dev/null
+++ b/marketing/product-film/src/components/CraneCard.tsx
@@ -0,0 +1,47 @@
+import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
+import { theme } from "../theme";
+import { swingRotation } from "./craneSwing";
+
+export const CraneCard: React.FC<{ dropStartFrame?: number }> = ({ dropStartFrame = 0 }) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const local = frame - dropStartFrame;
+
+ // descend into place over the first ~1.2s
+ const drop = spring({ frame: local, fps, durationInFrames: 36, config: { damping: 14, mass: 0.8 } });
+ const dropY = interpolate(drop, [0, 1], [-220, 0]);
+ const rotate = swingRotation(Math.max(0, local), fps);
+
+ return (
+
+ {/* attachment point connecting to the cable */}
+
+
✓ Project Ready
+
Ready to Build?
+
+ Whether you protect one home or manage a thousand roofs, the future of roofing starts here.
+
+
+
Schedule Demo →
+
Start Free Trial →
+
+
+ );
+};
diff --git a/marketing/product-film/src/components/craneSwing.test.ts b/marketing/product-film/src/components/craneSwing.test.ts
new file mode 100644
index 0000000..2404352
--- /dev/null
+++ b/marketing/product-film/src/components/craneSwing.test.ts
@@ -0,0 +1,19 @@
+import { describe, it, expect } from "vitest";
+import { swingRotation } from "./craneSwing";
+
+describe("swingRotation", () => {
+ it("starts at the +3deg keyframe, amplified by the entry boost", () => {
+ // t=0: base = +3, boost = 3 -> 9deg
+ expect(swingRotation(0, 30)).toBeCloseTo(9, 5);
+ });
+ it("settles to within +/-3deg after the settle window", () => {
+ for (let f = 30 * 3; f < 30 * 12; f++) {
+ expect(Math.abs(swingRotation(f, 30))).toBeLessThanOrEqual(3.0001);
+ }
+ });
+ it("is periodic at 4s once settled (same phase 4s apart)", () => {
+ const a = swingRotation(30 * 4, 30); // t=4s
+ const b = swingRotation(30 * 8, 30); // t=8s
+ expect(a).toBeCloseTo(b, 5);
+ });
+});
diff --git a/marketing/product-film/src/components/craneSwing.ts b/marketing/product-film/src/components/craneSwing.ts
new file mode 100644
index 0000000..cb76e3e
--- /dev/null
+++ b/marketing/product-film/src/components/craneSwing.ts
@@ -0,0 +1,10 @@
+// Reproduces the landing page .crane-swing animation: rotate(+3deg) <-> rotate(-3deg),
+// 4s ease-in-out loop, transform-origin top center. A cosine approximates the eased pendulum.
+// An entry "boost" damps a larger initial swing down to +/-3deg over settleSeconds.
+export function swingRotation(frame: number, fps: number, settleSeconds = 2): number {
+ const t = frame / fps;
+ const period = 4; // seconds
+ const base = 3 * Math.cos((2 * Math.PI) / period * t); // +3 at t=0
+ const boost = 1 + 2 * Math.max(0, 1 - t / settleSeconds); // 3x -> 1x across settle window
+ return base * boost;
+}