Chalk

Strength

  • Scroll
  • Landing Page
  • HTML
  • CSS
  • Scroll-Scrub

The build prompt

3,418 chars
Build a scroll-scrubbed cinematic page for CHALK — "Strength". ONE self-contained HTML file: inline <style>, inline <script>, Google Fonts via <link>. No build step, no framework. The only external asset is one video.

SCROLL MECHANIC — THE SCRUBBED VIDEO IS THE HERO:
There is NO autoplay hero above the scrub. The sticky stage is the FIRST element on the page.
- ONE <video>. NO autoplay attribute, NO loop attribute, and .play() is NEVER called.
- Frame 0 IS the landing state — on load the user sees currentTime = 0 as a still frame.
- Scroll position is the only thing that moves the video.
- The opening headline is the first caption beat (progress 0.00) on that same sticky stage.
If the video plays by itself on load you built two sections instead of one — start over.

Fetch the clip as a Blob and set src to URL.createObjectURL(blob): some hosts don't serve HTTP
byte-range requests, which pins video.seekable to [0,0] and freezes every seek.

THE VIDEO:
https://zxdefgavgwfxastwmmjm.supabase.co/storage/v1/object/public/assets/chalk.mp4
<video id="film" muted playsinline preload="auto"></video> — absolute, inset 0, 100%/100%, object-fit cover. Set src from the Blob (see above). Show a minimal mono "Loading" overlay until 'loadeddata', then fade it out and set currentTime = 0.

STRUCTURE:
- <section class="stage"> is the FIRST element on the page, height 700vh, containing one position:sticky, top:0, height:100vh child.
- Inside the sticky child: the video, a vignette (inset box-shadow 0 0 220px 90px rgba(0,0,0,.85)), a top scrim (28vh, rgba(0,0,0,.82) to transparent), a bottom scrim (40vh, rgba(0,0,0,.92) to transparent), and the caption beats.
- Fixed chrome outside the stage: nav at the top (wordmark CHALK, mono links Method / Coaches / Membership / Visit, outlined "Start training" pill), a scrub percentage + hairline rail bottom-left, and a spec row bottom-centre: 4:45 doors open / 0 mirrors / 1 rule.
- After the stage, a short closing section with the solid "Start training" pill.

THE SCRUB LOOP (one requestAnimationFrame loop):
progress = clamp(-stageRect.top / (stageHeight - innerHeight), 0, 1)
Ease it: current += (target - current) * 0.12 — this is what makes the scrub glide rather than snap.
video.currentTime = current * (video.duration - 0.05), but only write when the delta exceeds 0.005 so you don't thrash the decoder.
Update the rail width and the two-digit percentage from the same value.

CAPTION BEATS — absolutely positioned, opacity 0, gaining .on when progress is inside the window:
- 0.00-0.14 (upper third): "Before the lift."
- 0.24-0.38 (lower): "Five in the morning. Empty room."
- 0.72-0.88 (lower): "The dust is the receipt."
Leave the middle band (roughly 0.40-0.70) EMPTY on purpose — the footage carries it alone. That silence is what makes it feel authored; do not fill it.

THEME: base #080808, single accent bone #dcdcdc, ink near-white, hairlines rgba(255,255,255,.12). Headlines Inter Tight 500 at clamp(30px,4.6vw,64px), letter-spacing -.035em, with a heavy text-shadow so they read over the footage. Mono (JetBrains) for nav, specs, percentage.

RESPONSIVE: below 820px hide the nav links and the spec row.
REDUCED MOTION: drop the stage to auto height, make the sticky child static, show every beat at once, and give the video normal controls (still no autoplay).

VOICE: three short lines total across the whole scroll. The footage is the story.

More like this