THE HERO REBUILD

7 rounds. 3 browsers. 1 screenshot that solved it.

Frontend EngineeringGSAP / Next.js / WebKit2024

The Brief

The IronWorks homepage needed a hero that matched the studio's editorial ambition: full-viewport pinned scroll with a slow zoom-in on the background video, large Anton display type that splits and exits on scroll, floating badges, plasma gradient, corner brackets. Reference: a CodePen by Margarita (MIT) — a faithful adaptation of an existing scroll-zoom pattern, client-adapted with IronWorks copy and the studio's particle video.

The build took one afternoon. The cross-browser story took seven rounds over two days and ended with a single screenshot from a real iPhone.

System diagram showing the seven-round cross-browser debugging loop: observe device failure, instrument with on-page HUD, capture real device screenshot, identify root cause, apply targeted fix.
SYSTEM DIAGRAM — SEVEN-ROUND EVIDENCE-FIRST DEBUG LOOP

The Methodology

The debugging approach was simple: evidence before hypothesis, device evidence over emulator evidence, and if the emulator says PASS but the device says FAIL, trust the device.

Linux headless WebKit is useful for engine-matched behavior checks — CSS layout, JavaScript API surface, font rendering. It is not a real iPhone. Apple's scroll engine runs on a separate thread with its own physics. iOS accessibility settings can silently modify web behavior. Low Power Mode can suppress autoplay. Address-bar show/hide fires resize events that can disrupt pinned scroll. None of these are reproducible headlessly.

We debug with evidence, not guesses. When the emulator disagrees with the device, the emulator is wrong.

The key move in round six was deciding not to guess a seventh time, and instead shipping instrumentation. A lightweight on-page debug overlay displayed live scroll position, GSAP progress, pin state, and every browser feature flag relevant to the animation: reduced-motion preference, pointer coarseness, scroll container identity.

One screenshot from the real device, taken by the client, showed all the values. The reduced-motion flag was set. GSAP was correctly honoringprefers-reduced-motionby taking the static branch — as designed. iOS had the flag enabled by default under Settings › Accessibility › Motion. The animation was never broken. It was disabled.

The Seven Rounds

R01
Chrome / DesktopInitial scroll-zoom hero ships. Pinned ScrollTrigger works. Text splits animate. Badge floats in.
PASS
R02
Safari / DesktopVideo doesn't play. Safari requires HTTP 206 partial content for video autoplay — a full 200 response blocks it silently.
FAIL
R03
Safari / DesktopServer configured to emit Accept-Ranges and serve partial content. Video plays. Scroll zoom works.
PASS
R04
iPhone / iOS SafariPin fires. Scroll advances. Effect never moves — scrub reads 0 every frame despite active scrolling.
FAIL
R05
iPhone / iOS SafariRoot cause isolated: body overflow-x-hidden makes WebKit treat the body element as the scroll container, freezing ScrollTrigger's scrub position at 0. Fixed with overflow-x-clip. Effect still doesn't animate on device.
FAIL
R06
iPhone / iOS SafariLinux emulators pass 5/5. Real device still frozen. Evidence gap: we can't see what the device is actually reporting.
FAIL
R07
iPhone / iOS SafariShipped an on-page debug overlay. One screenshot from the real device revealed: prefers-reduced-motion: reduce was active (iOS Accessibility › Reduce Motion). ScrollTrigger was silently taking the static branch. Disabled the guard. Effect runs.
PASS

Three Bugs, Three Causes

BUG 01 — Safari Video Autoplay

Safari requires the server to support HTTP range requests for video autoplay. A 200 response to a video request — even with correct MIME type and autoplay muted playsInline attributes — will silently refuse to play in Safari. The fix: configure the static server to emit Accept-Ranges: bytes and handle partial content requests with 206 responses. One server header. Nothing in the React component changed.

BUG 02 — WebKit Scroll Container Trap

overflow-x: hidden on the body element causes WebKit (and only WebKit) to designate the body as the scroll container instead of the document root. GSAP's ScrollTrigger reads scroll position from the document root. On an affected device, scroll progress reads 0 for every frame — the pin fires, the user scrolls, nothing moves.

The fix is one character: overflow-x: clip. Clip provides the same visual behavior — horizontal overflow is cut — without promoting the element to a scroll container. The layout.tsx comment now documents this specifically so the next developer doesn't revert it.

BUG 03 — iOS Reduce Motion (The Final Boss)

iOS enables Reduce Motion under Settings › Accessibility › Motion. With this setting active, the browser emits prefers-reduced-motion: reduce. GSAP's ScrollTrigger was correctly honoring this — taking the static branch that shows the hero without animation. The hero wasn't broken. It was correctly respecting an accessibility preference the user had never deliberately set.

The emulator never reproduced this because it doesn't model iOS system preferences. Six rounds of Linux headless tests passed cleanly every time. The telemetry HUD made the difference: a reduced: true value in the green overlay was the only evidence needed. Per client preference, the animation now runs regardless of the reduced-motion setting — the flag is documented in the component with an explicit override constant.

Stack

GSAP 3.12ScrollTriggerNext.js 13TypeScriptSafari / WebKit

The Lesson

Cross-browser work above a certain complexity level cannot be resolved by reasoning from first principles. At some point you are guessing — and guessing on a real user's device is expensive. The right move is to ship evidence collection, get one screenshot from the actual device, and let the data answer the question.

The IronWorks hero keeps its on-page debug overlay in the codebase. If a device-specific bug surfaces in the future, the debugging loop starts at round seven, not round one.

Instrument first. Hypothesize after. The screenshot is cheaper than the sixth guess.

The Evidence Loop