类型:Site模板 · 分类:未分类 · 收录:2026-09-19 14:02:05 · 源码:会员可见
Recreate Halion (new-13-v2 CDN) exactly. Buildless. One origin over HTTP (npx serve . -l 5190). file:// will not work: ES modules + video Range requests need a real origin. Internet required. Do not invent extra pages, extra colors, FAQ content, a menu drawer, or local media files.
════════════════════════════════════
0. PRODUCT + VOICE
════════════════════════════════════
Halion is a 128-qubit quantum AI chip worn on the forehead. Engineered in Zurich. Copy is placeholder for that concept. Voice: all-caps UI + hero/product titles; About statement in sentence case. Tracking is always negative. Two colors only: pure black and pure white (plus their transparencies). No accent, no red, no strokes/outlines on buttons, capsule, chips, or chrome. Hairlines exist only as section-frame rules at ink 20%.
<title>Halion — quantum AI you wear</title>
<meta name="description" content="Halion is a 128-qubit quantum AI chip you wear on your forehead. Engineered in Zurich.">
<meta name="theme-color" content="#000000">
lang="en"
Exact copy (do not rewrite):
- Logo: HALION + ®
- Nav: About · Product · FAQ (FAQ is a dead hash; there is NO FAQ section)
- Clock: live Zurich time, format "03:48 PM ZURICH | UTC+1" (offset DST-aware)
- Header CTA: Submit +
- Burger: aria-label="Menu", aria-expanded="false", does NOTHING
- Hero H1 (3 explicit <br> lines, staircase, one word per line): Augmented / human / mind
- Hero lead: A quantum AI chip you wear on your forehead. 128 qubits, engineered in Zurich.
- Hint: [ Scroll to explore ]
- Hero CTA: Submit +
- Socials: Inst · TG · X (href="#", aria-labels Instagram / Telegram / X)
- About intro: Quantum AI, worn on the forehead.
- About dim: Put it on. Be the smartest person in the room.
- About stats: 128 qubits · 99.9% fidelity · 250 µs (µs MUST keep text-transform:none so µ does not become Μ)
- About statement (sentence case, ~5 lines @1440): It weighs every option at once, keeps the best one and hands it to you before you finish the question.
- Chip 02
- Product title (explicit <br>): Three parts. / One mind.
- Callouts:
traces: 0.3 ms / Brain link latency (ms not uppercased)
glass: 9H / Space-grade glass
ring: 12 TB / Neural nets on board
- Chip 03
No footer. No FAQ. Burger does not open anything. Submit and socials are href="#".
════════════════════════════════════
1. STACK (exact CDN URLs, this order)
════════════════════════════════════
Inline BEFORE first paint (must stay inline, not a deferred module):
<script>document.documentElement.classList.add('js')</script>
Without this, html.js { visibility:hidden } never applies and the rest state flashes.
Fonts:
preconnect fonts.googleapis.com + fonts.gstatic.com (crossorigin)
https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500&display=swap
ONLY Inter Tight 400 and 500. No other face, no italics, no 600/700.
Fallback @font-face so the page does not reflow:
font-family: 'Inter Tight Fallback';
src: local('Arial');
ascent-override: 96.88%;
descent-override: 24.15%;
line-gap-override: 0%;
size-adjust: 100%;
body: 'Inter Tight', 'Inter Tight Fallback', -apple-system, BlinkMacSystemFont, sans-serif
Icons:
https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css
Only bi-plus and bi-list. Both drawn 1/16 em thick. .btn .bi { font-size: 1.25em } — ONE kegel so strokes match to the pixel.
Engine (plain <script defer>, this exact order, BEFORE the module):
https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js
https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js
https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js
https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/Flip.min.js
https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/CustomEase.min.js
https://cdn.jsdelivr.net/npm/locomotive-scroll@5.0.0/bundled/locomotive-scroll.min.js
Then: <script type="module" src="main.js"></script>
Register GSAP plugins once. Create named eases matching CSS tokens:
CustomEase.create('magic', '0.33, 0, 0, 1')
CustomEase.create('quart', '0.41, 0.35, 0.2, 1')
CustomEase.create('outExpo', '0.19, 1, 0.22, 1')
CustomEase.create('base', '0.46, 0.03, 0.12, 1')
CSS twins:
--ease-magic: cubic-bezier(.33, 0, 0, 1)
--ease-out-expo: cubic-bezier(.19, 1, .22, 1)
--ease-quart: cubic-bezier(.41, .35, .2, 1)
--ease-base: cubic-bezier(.46, .03, .12, 1)
--ease-out: cubic-bezier(0, 0, .2, 1)
Smooth scroll: Locomotive Scroll 5 (Lenis 1.3.17 inside).
new LocomotiveScroll({
lenisOptions: {}, // defaults: lerp .1, smoothWheel
initCustomTicker: (render) => gsap.ticker.add(render),
destroyCustomTicker: (render) => gsap.ticker.remove(render),
})
history.scrollRestoration = 'manual'; window.scrollTo(0,0) on boot
lenisInstance.on('scroll', ScrollTrigger.update)
gsap.ticker.lagSmoothing(0)
In-page anchors: preventDefault, lenis.scrollTo(hash, { duration: 2 })
[data-scroll] elements receive .is-inview from Locomotive's IntersectionObserver (rootMargin -1px), honoring data-scroll-offset.
CSS layers, declared once, later wins:
@layer reset, tokens, base, layout, components, motion, utilities;
════════════════════════════════════
2. MEDIA — EXACT CLOUDFRONT URLS (do not substitute)
════════════════════════════════════
Both <video>s: muted playsinline preload="auto" crossorigin="anonymous" object-fit:cover, absolute inset 0.
CloudFront has CORS * and byte ranges (required for currentTime scrub).
CLIP 1 — hero / About hold (all-intra H.264, 8.04s, 1920×1080, 24fps, 193 I-frames)
src: https://d2ol7oe51mr4n9.cloudfront.net/user_3GJaYKPxdnQG0Q9O26lu6DPmcHu/fb4c80b0-ed9d-4719-be32-99a9e18356c5.mp4
poster: https://d2ol7oe51mr4n9.cloudfront.net/user_3GJaYKPxdnQG0Q9O26lu6DPmcHu/6dbdcd75-2556-435f-8180-60ced6f60321.jpg
Shot: slow push-in on a bald figure in darkness, a square glass chip glowing on the forehead. Poster = frame 0.
CLIP 2 — Product (all-intra H.264, 10.04s, 1920×1080, 24fps, 241 I-frames) [v2]
src: https://d2ol7oe51mr4n9.cloudfront.net/user_3GJaYKPxdnQG0Q9O26lu6DPmcHu/ed15df08-46fa-46f8-8aed-70bd06ebc547.mp4
poster: https://d2ol7oe51mr4n9.cloudfront.net/user_3GJaYKPxdnQG0Q9O26lu6DPmcHu/6acf44e4-ef4d-4565-a50c-3caa7c19399a.jpg
Shot: a hand lifts the chip off the forehead; the chip floats on blue glow and disassembles into glass case / ring / traces.
Last frame of clip 1 === first frame of clip 2. Switch is a class toggle with NO fade/transition.
Videos must be all-intra so every currentTime seek lands in ~10ms. Do not re-encode to long-GOP.
════════════════════════════════════
3. COLOR + SHAPE + TYPE TOKENS
════════════════════════════════════
--black:#000 --white:#fff
--bg = black; --ink = white; --display-ink = ink; --bg-footer = bg (hook unused)
Derivatives via color-mix(in oklab, …):
--ink-55 ink 55% (hero lead, hint, stats, callout labels, clock sep, callout lines/tips)
--ink-40 ink 40% (stat middots)
--line-faint ink 20% (About hairlines)
--scrim black 50%
--caption-ink ink 80%
--tint-image ink 10%
--glass-bg white 10%
--glass-bg-hover white 16%
--glass-blur 12px
--radius 12px
--btn-h 2.5rem
::selection { background:white; color:black; text-shadow:none }
NO borders. NO box-shadow. Buttons/capsule/chip = fills only.
@supports (corner-shape: squircle) { .btn, .header__bar, .chip { corner-shape: squircle } }
Safari/Firefox: plain 12px radius.
Scene scrim over the clip (absolute inset 0, pointer-events none):
linear-gradient(180deg,
rgb(0 0 0 / .35) 0%,
rgb(0 0 0 / .15) 35%,
rgb(0 0 0 / .15) 65%,
rgb(0 0 0 / .55) 100%)
Fluid root:
html { font-size: 15px }
≥640px: clamp(15px, 12.9574px + .3191vw, 18px)
≥1580px: clamp(18px, 1.1765vw - .5882px, 22px)
antialiased, tap-highlight transparent, overscroll-behavior:none, tab-size:4.
Type scale (rem). Weight 400 for text, 500 for UI.
--fs-display 3rem → md 7rem lh 1, tracking -.05em
--fs-display-tight uses display size, lh .85 from md, tracking -.035em
--fs-statement 1.875 → md 3rem lh 1, -.04em
--fs-statement-lg 2.25 → md 4rem (About statement only)
--fs-sublead 1.5 → md 1.875 lh 1, -.01em (logo, callout values)
--fs-ui 1rem lh 1.5 tracking -.001em weight 500
--fs-caption .875rem lh 1.43 tracking 0 weight 500
--fs-body-lg 1.125rem (lead) lh 1.3 tracking -.01em ← ONLY body lh exception; caps need air
.upper { text-transform:uppercase }
.no-upper { text-transform:none }
.text-trim { text-box: trim-both cap alphabetic } on hero title + lead AND product title
.t-display-tight on hero H1 and product H2. At md, lh becomes .85.
Durations:
--dur-tap .3s --dur-mid .6s --dur-body .7s --dur-hover .75s
--dur-image 1.2s --dur-line 1.4s --dur-word 1.6s
--step-line 50ms --step-word 75ms --step-char 75ms
--split-mask-pad .08em --split-mask-bleed-y .3em
Spacing:
--pad-x 1rem --gap-grid 1.25rem --header-py 1rem
--hero-pb 1rem
--hold 60svh (About reading pause after statement parks)
--product-run 220svh (section 03 clip window)
--section-gap 100px (breath between hero and next section)
--frame-gap 48px (text ↔ hairline)
--frame-gap-bottom 56px (48 + descender, so parked statement reads as the side gap)
--z-header 60
Buttons .btn:
inline-flex, height 2.5rem, padding-inline 1rem, gap .5rem, nowrap, lh 1
.btn--glass: glass-bg + backdrop-blur 12px; hover → 16%
.btn--solid: white fill, black text; hover → white 86%
.btn--icon: square 2.5rem, padding 0
:active { scale(.97) }
transition background/color .3s magic, transform .15s ease-out
Chip .chip:
inline-flex, height/min-width 1.75rem, padding-inline .625rem, glass-bg + blur, squircle 12px, no stroke
appears via .inview-fade (opacity 0→1, .3s) with data-scroll-offset="5%"
Link dim: hover opacity .6 (.3s) at (hover:hover)
════════════════════════════════════
4. EXACT DOM / COMPOSITION
════════════════════════════════════
html.js .header, html.js .hero { visibility: hidden } until the intro's first GSAP frame.
Without JS everything is simply visible.
HEADER (fixed, inset-inline 0, top 0, z 60, pointer-events:none on wrapper; bar pointer-events auto)
.header__bar: flex space-between, padding 1rem var(--pad-x), gap 2rem
LEFT (gap 3rem):
logo .t-sublead data-header-logo data-header-item → HALION<span class="header__mark">®</span>
.header__mark { font-size:.4em; vertical-align:top; margin-left:.06em }
nav .t-ui.upper data-header-nav (gap 2.5rem): About #about, Product #product, FAQ #faq
links .link-dim data-header-item
RIGHT (gap .5rem):
clock .t-ui.upper data-clock data-header-item
<time data-clock-time>03:48 PM</time> Zurich <span class="header__sep">|</span> <span data-clock-zone>UTC+1</span>
.header__clock { margin-right: 1.5rem; nowrap }
.header__sep { color: ink-55; margin-inline: .35em }
a.btn.btn--glass.header__cta.upper href="#" data-header-cta data-header-item
<span.btn__label data-header-cta-label>Submit</span><i class="bi bi-plus">
button.btn.btn--glass.btn--icon.header__burger type=button data-header-burger data-header-item
<i class="bi bi-list">
COMPACT (.header.is-compact) — fires when hero top hits -4% of viewport:
bar: width:fit-content; margin:.75rem auto 0; padding:.25rem .25rem .25rem 1rem; gap:1rem;
background glass-bg; backdrop-blur 12px
logo font-size 1.375rem
nav, clock, .btn__label { display:none }
CTA becomes square --btn-h, SOLID WHITE, black "+", no blur
Morph via GSAP Flip (see H4). Glass/blur/CTA color via CSS transition .6s magic.
STAGE (wraps hero + about + product)
.stage relative
.stage__scene sticky top 0, 100lvh, margin-bottom:-100lvh, overflow hidden, z 0, bg black
.stage__par[data-scene-par] absolute inset 0 — cursor parallax (H7′)
video.stage__media[data-hero-video] clip 1
video.stage__media.stage__media--2[data-video-2] clip 2, opacity 0
.stage__scrim
.stage.is-second .stage__media--2 { opacity:1 } /* NO transition */
sections inside stage: relative z 1
HERO section.hero[data-hero] height 100lvh (chrome only; scene is the stage)
.hero__chrome absolute inset-inline 0 top 0 height 100svh (NOT sticky — it leaves with the page)
.hero__grid absolute; inset-inline pad-x; bottom 1rem;
grid 12 cols × rows "1fr auto"; column-gap 1.25rem
h1.hero__title.t-display-tight.text-trim.upper[data-hero-title]
grid-column 1/9, grid-row 1/3, align-self end, margin-left:-.03em
Augmented<br>human<br>mind
At 1440: ~715 / 441 / 307 px. First word ≤11 chars so it fits 8 cols from 1280; last word ≤4 cols so the hint can sit on its baseline, centered.
p.hero__lead.t-lead.text-trim.upper[data-hero-lead]
grid-column 9/13, grid-row 1, align-self start, max-width 17em, color ink-55
17em → exactly 3 lines. text-trim so its cap height = title cap height.
p.hero__hint.t-ui.upper[data-hero-item]
grid-column 5/9, grid-row 2, justify-self center, align-self center, ink-55, nowrap
a.btn.btn--solid.hero__cta.upper[data-hero-item]
grid-column 9/11, grid-row 2, justify-self start, align-self end
ul.hero__social.t-ui.upper[data-hero-item]
grid-column 11/13, grid-row 2, justify-self end, align-self center, flex, gap 1.5rem
Grid height = title height. Bottom row sits on the title baseline. Everything 1rem from the bottom of the screen.
ABOUT section#about.about[data-about]
flex column; min-height: calc(100svh + 60svh); margin-top:100px; padding-top:1.25rem
FIRST CHILD — the pin (must be first so its margin-box pushes the section bottom):
.pin[data-statement-pin]
sticky top 0; height 100svh; pointer-events none; grid-12; align-content end;
padding-inline pad-x; padding-bottom 56px
h2.about__statement.t-statement-lg[data-statement]
grid-column 7/13; padding-left 48px ← RIGHT half of the frame (reference had it left; this site mirrors)
THEN .about__top.px-global.grid-12
margin-top: -100svh (lays over the pin; pin keeps a real margin-box so the section bottom can still push it)
align-items: last baseline; padding-bottom 48px
.about__intro grid-column 1/8
p.t-lead.upper.reveal-lines data-split="lines" data-scroll data-scroll-offset="20%"
p.t-lead.upper.is-dim.reveal-lines same + style="--line-offset: 1" (queued 50ms after first)
p+p { margin-top:.25rem }
p.about__stats.t-ui.upper.reveal-lines grid-column 9/13; justify-self end; text-align right; ink-55; nowrap
data-split="lines" data-scroll data-scroll-offset="20%" style="--line-offset: 2"
middots .about__dot { color ink-40; margin-inline .6em }
THEN a wrapped .divider.reveal-divider data-scroll data-scroll-offset="20%" (top hairline, ink 20%)
THEN .about__frame.px-global flex:1; relative
.about__vline.divider--v.reveal-divider--v data-scroll data-scroll-offset="10%"
absolute top 0 bottom 0 left 50%
THEN wrapped .divider.reveal-divider data-scroll data-scroll-offset="5%" (bottom hairline; 5% because it sits near page-bottom)
OUTSIDE the section:
.foot.px-global flex justify-end padding-block 1.25rem
span.chip.t-caption.inview-fade data-scroll data-scroll-offset="5%" → 02
Do NOT put a negative margin on the pin itself — that collapses its margin-box and the pin no longer pushes the section bottom.
PRODUCT section#product.product[data-product]
relative; min-height 220svh; margin-top 100px
.callouts[data-callouts] aria-hidden="true"
sticky top 0; 100svh; overflow hidden; pointer-events none
svg.callouts__lines[data-callout-lines] absolute inset 0 100%
for each of traces|glass|ring:
<g data-line="…"><line/><circle class="callouts__dot" r="3"/><circle class="callouts__tip" r="2"/></g>
lines: stroke ink-55, stroke-width 1, stroke-dasharray 1, stroke-dashoffset 1, pathLength=1 (set in JS)
dots fill ink, tips fill ink-55, both scale 0, transform-box fill-box, origin center
.callout[data-callout=traces|glass|ring] position absolute top 0 left 0 will-change transform
.callout__box display grid gap .4rem opacity 0
.callout__value 1.875rem (sublead) lh 1 tracking -.01em nowrap
.callout__label.t-caption.upper ink-55 nowrap
.product__screen.px-global
height 100svh; margin-top -100svh (lays over the sticky callouts)
grid-12; align-content end; padding-bottom 1rem
h2.product__title.t-display-tight.text-trim.upper[data-title]
grid-column 1/9; margin-left:-.03em
Three parts.<br>One mind.
.foot.foot--abs.px-global absolute bottom 0 inset-inline 0
chip 03 (so the clip window runs to the section's very bottom)
════════════════════════════════════
5. MOTION CATALOGUE — IMPLEMENT THESE EXACTLY
════════════════════════════════════
prefers-reduced-motion: every reveal rests in final state; no intro, no scrub, no Flip (header just toggles class); videos stay on posters; parallax/gravity off; callouts static at tailT=1.
H1 · PAGE INTRO (lib/hero.js + lib/header.js)
Wait Promise.all([document.fonts.ready, heroVideo.ready]) then rAF.
Failsafe 4s: if not started and tab not hidden, just show() rest state. Background tab re-arms until visible.
No scroll lock.
Before start: header + hero visibility hidden. fromTo with explicit ends. While GSAP writes transform/opacity, set transition:none; clearProps at end. Show() AFTER the fromTos have painted their first frame.
Beat 0 [data-header-item] ×7 y 1em → 0, autoAlpha 0→1 .6s magic, stagger .08 (header's own tween)
Beat .15 title .word (SplitText words+mask, autoSplit:false because <br> freeze lines)
yPercent 130 → 0 1.6s outExpo, stagger .075 (A3)
Beat .5 lead .line (SplitText lines+mask, autoSplit:true)
y 1em → 0, autoAlpha 1.4s magic, stagger .05 (A2)
Beat .8 hint · CTA · socials y 1em → 0, autoAlpha .6s magic, stagger .1
H2 · TITLE ROLLS OUT (scrub, built in intro onComplete so start values are rest)
ScrollTrigger { trigger:hero, start:'top top', end:'+=50%', scrub:true } ease none
words → yPercent −130, duration .5, stagger { each:.06, from:'start' }
Words leave UP into the same mask they entered from (drum: in from below, out above).
H3 · REST OF HERO CHROME FADES (same scrub timeline)
hint → autoAlpha 0, y −1em, .3 at 0
lead CONTAINER (not .line) → autoAlpha 0, y −1em, .4 at .05
CTA + socials → .4, stagger .08 at .1
Also tween state.progress 0→1 duration 1; onUpdate sets --hero-p on <html>.
H4 · HEADER FOLDS (GSAP Flip)
ScrollTrigger { trigger:hero, start:'top -4%' } onEnter compact true, onLeaveBack false.
If intro still running: kill the whole staggered tween and clearProps — a folding header and a running entrance must never write the same transforms.
Else:
Flip.getState([bar, cta, burger, nav, clock, ctaLabel])
toggle .is-compact
Flip.from(state, { duration:.6, ease:'magic', nested:true, absoluteOnLeave:true, clearProps:true
onLeave: els → autoAlpha 0, y −.5em, .25s magic
onEnter: els → autoAlpha 0→1, y .5em→0, .4s, delay .2, magic, clearProps
})
After Flip.from, set moving (cta, burger) transition to 'transform 0s, background-color var(--dur-mid) var(--ease-magic), color …' so tap transitions don't smooth Flip's per-frame transforms.
Logo: CSS font-size per state; GSAP scale from (fromPx/toPx) → 1, transformOrigin 0% 50%, .6s magic, clearProps. Layout box does not reflow neighbors.
H5 · CLOCK
Europe/Zurich via Intl. DateTimeFormat en-US hour:'2-digit' minute:'2-digit' hour12:true.
Offset from timeZoneName longOffset: "GMT+02:00" → "UTC+2", "GMT+05:30" → "UTC+5:30", "GMT" → "UTC".
setInterval 1000ms; touch DOM only when the minute string changes.
H6 · SCROLL-SCRUBBED CLIPS
initHeroVideo() seek-coalescing scrubber:
at most ONE seek in flight; pending always chases latest target; on seeked apply next
30ms dead zone (~¾ of a 24fps frame)
loadedmetadata → pause + currentTime .001 (poster-pin)
canplay → warm decoder: seek to min(.12, duration*.03) then back to .001
ready resolves on metadata+first frame OR error OR 4s timeout (missing clip must not block intro)
scrub(p01): pause, target = clamp(p01 * duration, 0, duration-.001)
Two instances: [data-hero-video] and [data-video-2].
initStage clips (ScrollTrigger scrub:true; outside the window the last landed frame HOLDS — that IS the reading pause):
1. trigger: stage, start 'top top', endTrigger '#about', end 'top top'
→ heroVideo.scrub(p) // whole hero + About entrance; freezes the instant the statement parks; holds through --hold
2. trigger: '#product', start 'top bottom', end 'bottom bottom'
→ callouts.setScroll(p)
onEnter / onEnterBack → .stage.is-second = true
onLeaveBack → false
// clip 2 takes over on the shared frame as 03 rides in; window is the full 220svh
H7′ · SCENE PARALLAX (shared cursor field)
Cursor (one global):
nx, ny = (client - viewportCenter) / halfViewport, clamp ±1
eased x,y lerp 0.07 on gsap.ticker
mouseleave / touchend → nx,ny = 0 (release to center)
mousemove + touchmove
[data-scene-par] drifts AGAINST cursor:
translate3d(-x*26px, -y*18px, 0) scale(1.05)
scale hides the edges. getShift() returns {x,y} so callout anchors stay glued to the picture.
H7 · ABOUT STATEMENT (lib/statement.js)
[data-statement] inside [data-statement-pin]. NO .reveal-* — GSAP owns transforms.
SplitText lines+mask, autoSplit, rebuild tween in gsap.context onSplit.
lines yPercent 130 → 0, 1.6s outExpo, stagger .075
ScrollTrigger { trigger: pin, start: 'top 8%', toggleActions: 'play none none reverse' }
Down through the point (~70px before park, first lines already in frame) they rise; up through the point the tween reverses into the masks. After park they do NOT disappear — they leave only with the section.
H8 · PRODUCT TITLE = HERO H1 (lib/title.js)
[data-title], explicit <br>, SplitText words+mask, autoSplit:false.
ONE master timeline owns the words (two tweens on the same targets fight on re-entry):
0 → 'in' yPercent 130→0, 1.6s outExpo, stagger .075
'in' → end yPercent 0→−130, .5, ease none, stagger { each:.06, from:'start' }
paused. Two ScrollTriggers NEVER touch words directly:
title top 85%: onEnter master.tweenTo('in'); onLeaveBack tweenTo(0, {ease:'none'})
section top top → +=50%, scrub: killTweensOf(master) then master.time(in + p*outDur, true)
skip the scrub writer while p<=0 AND time<=in (entrance zone owns it)
Always gsap.killTweensOf(master) before any drive — exactly one writer.
H9 · CALLOUTS IN GRAVITY (lib/callouts.js)
Frame 16/9. AMP 140px. TAIL 1.2s (last 1.2s of clip 2 owned by cursor). TAIL_LERP .06.
Keyframe fractions of the 1920×1080 clip (measured on frames 8.84s → 10.0s of the 10.04s v2 clip). Anchors sit on object EDGES so boxes rest on black, not on the product:
traces: a0 [0.240, 0.300] a1 [0.240, 0.268] rest [-40, -150] depth .55 attach 'bottom'
(the traces' ring)
glass: a0 [0.234, 0.556] a1 [0.221, 0.556] rest [-90, 80] depth .8 attach 'top'
(left corner of the case; box below-left)
ring: a0 [0.780, 0.395] a1 [0.780, 0.385] rest [90, -110] depth 1 attach 'bottom'
(top of outer rim at the right edge; box above-right in the corner)
coverRect(): scale = max(vw/1920, vh/1080); rect of the object-fit:cover video.
Anchor px = coverRect origin + lerp(a0,a1, tailT)*rect + scene-parallax shift. HARD glue.
Box target = anchor + rest − easedCursor * 140 * depth (AGAINST the cursor)
Inertia: gsap.quickTo x/y, duration 0.7 + 0.5*depth, ease power3.out
Each frame: line x1/y1 = anchor, x2/y2 = box attach edge; dot at anchor r=3; tip at attach r=2.
Clip time while Product window is active:
w = smoothstep(.85, 1, p) // Hermite: t*t*(3-2*t)
tailT lerps toward (cursor.seen ? (nx+1)/2 : 1) // no pointer yet → tail completed (chip fully apart)
t = p*(D−1.2) + w*tailT*1.2
video.scrub(t/D)
setScroll(p) only stores p; the ticker writes currentTime. Do NOT also scrub p directly on clip 2.
Entrance at 85% of the Product window, reversed on the way up:
ScrollTrigger start: () => `bottom ${innerHeight + 0.15 * section.offsetHeight}px`
toggleActions 'play none none reverse', invalidateOnRefresh
per item i:
box y 1em + autoAlpha 0→1, .6s magic, at i*.12
line strokeDashoffset 1→0, .6s magic, at i*.12
dot+tip scale 0→1, .4s magic, at i*.12+.1
A2 · CSS line reveals (About intro/stats)
.reveal-lines .line { translate 0 1em; opacity 0; transition 1.4s magic }
.is-inview .line { translate 0 0; opacity 1; delay: calc((var(--line)+var(--line-offset,0))*50ms) }
SplitText propIndex → --line 1-based. --line-offset inline queues a second block in the same screen.
A4 / A4b · hairlines
.reveal-divider: scaleX(0) origin 0 → scaleX(1), 1.4s outExpo
.reveal-divider--v: scaleY(0) origin 50% 0 → scaleY(1), 1.4s outExpo (draws top → bottom)
SplitText masks (hero title, statement, product title, About lines):
.line-mask display block
.word-mask/.char-mask/.line/.word/.char inline-block
masks: clip-path inset(calc(var(--split-mask-bleed-y)*-1) 0 …) and overflow:visible !important
word/char-mask pad-right .08em, margin-right -.08em
════════════════════════════════════
6. FILE / BOOT ORDER
════════════════════════════════════
index.html
style.css (all tokens + layout + reveal contract)
main.js (entry)
lib/gsap.js register plugins + eases, export reduced
lib/scroll.js Lenis owner
lib/split.js [data-split] after fonts.ready
lib/reveal.js image-load + footer spy (no footer here) + replay()
lib/clock.js
lib/header.js
lib/hero.js
lib/hero-video.js
lib/stage.js
lib/parallax.js
lib/cursor.js
lib/statement.js
lib/title.js
lib/callouts.js
main.js boot order (every module no-ops if its markup is missing):
initScroll()
initSplit()
initImageLoad(); initFooterSpy()
initClock()
initHeader()
heroVideo = initHeroVideo()
productVideo = initHeroVideo(document.querySelector('[data-video-2]'))
scenePar = initSceneParallax()
callouts = initCallouts({ video: productVideo, shift: scenePar.getShift })
initStage({ root, clips: [clip1, clip2] })
fonts.ready → rAF → initStatements(); initTitles()
initHero({ header, ready: heroVideo.ready, onProgress: p => html.style.setProperty('--hero-p', p.toFixed(3)) })
Dev hook window.__site = { scroll, gsap, ScrollTrigger, header, hero, heroVideo, productVideo, stage, callouts, cursor, clock, replay, statements, titles, refresh, drive(y), state() }
════════════════════════════════════
7. HARD DON'TS
════════════════════════════════════
- Don't add a third color, a logo mark image, a menu overlay, a FAQ block, a footer, or extra sections.
- Don't put strokes/borders on buttons, header capsule, chips.
- Don't use CSS transitions on transform/opacity that GSAP writes (kill them during tweens).
- Don't fade between the two videos.
- Don't scrub clip 2 with raw section progress; last 1.2s belong to the cursor via callouts.
- Don't autoSplit the hero or product titles (explicit <br>).
- Don't put two GSAP tweens on the same word/header-item at once.
- Don't localise the clock; always Europe/Zurich.
- Don't self-host fonts; use the Google Fonts URL above (this is the CDN build).
- Don't use file://. Serve over HTTP.
- Don't change the four CloudFront URLs.
- Don't use positive tracking. Don't use Inter Tight italic or weights other than 400/500.
- Burger and Submit do not navigate. FAQ hash has no target.
Match pixel composition at 1440: staircase 3-word hero title bottom-left, 3-line 17em lead top-right aligned to title cap-height, "[ Scroll to explore ]" centered on the last word's baseline, solid white Submit + socials on that same baseline, 1rem from the bottom. After first scroll the header is a centered glass pill: smaller HALION® · white + · burger. About is a hairline frame with a vertical rule at 50%, statement sticky bottom-right. Product title behaves like the hero title; near the end three gravity callouts pin to traces/glass/ring of the disassembled chip.
```
---
That is the full recreation spec. The four media URLs that must not change:
| Role | URL |
|---|---|
| Hero clip (8.04s) | `https://d2ol7oe51mr4n9.cloudfront.net/user_3GJaYKPxdnQG0Q9O26lu6DPmcHu/fb4c80b0-ed9d-4719-be32-99a9e18356c5.mp4` |
| Hero poster | `https://d2ol7oe51mr4n9.cloudfront.net/user_3GJaYKPxdnQG0Q9O26lu6DPmcHu/6dbdcd75-2556-435f-8180-60ced6f60321.jpg` |
| Product clip v2 (10.04s) | `https://d2ol7oe51mr4n9.cloudfront.net/user_3GJaYKPxdnQG0Q9O26lu6DPmcHu/ed15df08-46fa-46f8-8aed-70bd06ebc547.mp4` |
| Product poster | `https://d2ol7oe51mr4n9.cloudfront.net/user_3GJaYKPxdnQG0Q9O26lu6DPmcHu/6acf44e4-ef4d-4565-a50c-3caa7c19399a.jpg` |
One intentional gap vs the nav: **FAQ is linked and does not exist.** Keep it that way if you want an exact clone.[/hidecontent]