motionsites中文站 / Quantum Human
Quantum Human
aadmin2026-09-19 发布Site模板1 阅读

Quantum Human

类型:Site模板 · 分类:未分类 · 收录:2026-09-19 14:02:05 · 源码:会员可见

[hidecontent type="vip1"]
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]
← 返回首页