cinematic-scroll-skill 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/COMPATIBILITY.md +244 -0
  2. package/LICENSE +21 -0
  3. package/MODELS.md +92 -0
  4. package/README.md +250 -0
  5. package/SKILL.md +1003 -0
  6. package/audit-mode.md +497 -0
  7. package/bin/install.mjs +91 -0
  8. package/compile-choreography.mjs +296 -0
  9. package/decision-log.md +241 -0
  10. package/examples/GETTING_STARTED.md +279 -0
  11. package/examples/KNOWN_ISSUES.md +50 -0
  12. package/examples/PROMPTS.md +166 -0
  13. package/examples/luxe/README.md +88 -0
  14. package/examples/luxe/index.html +662 -0
  15. package/examples/noir/README.md +72 -0
  16. package/examples/noir/index.html +634 -0
  17. package/examples/pop/README.md +81 -0
  18. package/examples/pop/index.html +711 -0
  19. package/examples/renaissance/README.md +39 -0
  20. package/examples/renaissance/index.html +648 -0
  21. package/examples/studio/README.md +77 -0
  22. package/examples/studio/chapters.js +105 -0
  23. package/examples/studio/index.html +520 -0
  24. package/manifest.json +92 -0
  25. package/manifest.md +136 -0
  26. package/package.json +56 -0
  27. package/references/film-archetypes.md +211 -0
  28. package/references/performance-budget.md +499 -0
  29. package/references/scroll-patterns.md +693 -0
  30. package/scroll-choreography-compilation.md +543 -0
  31. package/scroll-choreography.json +1512 -0
  32. package/taste-guardrails.md +164 -0
  33. package/templates/nextjs/.env.example +41 -0
  34. package/templates/nextjs/app/api/fal/proxy/route.ts +33 -0
  35. package/templates/nextjs/app/api/fal/webhook/route.ts +132 -0
  36. package/templates/nextjs/app/api/generate-edition-asset/route.ts +66 -0
  37. package/templates/nextjs/app/globals.css +80 -0
  38. package/templates/nextjs/app/layout.tsx +21 -0
  39. package/templates/nextjs/app/page.tsx +10 -0
  40. package/templates/nextjs/components/ChapterDemoVisual.tsx +212 -0
  41. package/templates/nextjs/components/ChapterScene.tsx +373 -0
  42. package/templates/nextjs/components/EditionsPage.tsx +116 -0
  43. package/templates/nextjs/components/SmoothScrollProvider.tsx +8 -0
  44. package/templates/nextjs/lib/api-guard.ts +110 -0
  45. package/templates/nextjs/lib/editions-manifest.ts +224 -0
  46. package/templates/nextjs/lib/fal-client.ts +12 -0
  47. package/templates/nextjs/lib/fal-generate.ts +86 -0
  48. package/templates/nextjs/lib/fal-models.ts +213 -0
  49. package/templates/nextjs/lib/prompt-contract.ts +97 -0
  50. package/templates/nextjs/lib/use-device.ts +42 -0
  51. package/templates/nextjs/lib/use-lenis.ts +35 -0
  52. package/templates/nextjs/next.config.ts +29 -0
  53. package/templates/nextjs/package-lock.json +6455 -0
  54. package/templates/nextjs/package.json +41 -0
  55. package/templates/nextjs/package.patch.json +28 -0
  56. package/templates/nextjs/postcss.config.js +6 -0
  57. package/templates/nextjs/scripts/generate-chapter-assets.mjs +243 -0
  58. package/templates/nextjs/scripts/setup.mjs +170 -0
  59. package/templates/nextjs/tailwind.config.ts +37 -0
  60. package/templates/nextjs/tsconfig.json +23 -0
  61. package/troubleshooting.md +1284 -0
@@ -0,0 +1,711 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <title>BLOOM — your day, but make it fun · cinematic-scroll example</title>
7
+ <meta name="description" content="A contemporary-pop, Gen-Z cinematic-scroll page — candy-pink + electric-lime gradients, bold rounded sans, fast parallax and a floating app-UI layer. An example for the cinematic-scroll Agent Skill." />
8
+
9
+ <!-- Open Graph -->
10
+ <meta property="og:title" content="BLOOM — your day, but make it fun" />
11
+ <meta property="og:description" content="Gen-Z pop cinematic scroll — candy pink + electric lime, glassy floating UI, fast parallax. Built with the cinematic-scroll Agent Skill." />
12
+ <meta property="og:type" content="website" />
13
+
14
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
16
+ <!-- Plus Jakarta Sans (bold rounded display + UI), Space Grotesk (mono-ish labels) -->
17
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" />
18
+
19
+ <!-- GSAP + ScrollTrigger (now 100% free) — powers the dedicated "THE DROP" showcase:
20
+ a JUMP-SCARE micro (back.out overshoot, taste-guardrails §2) + a VELOCITY-REACTIVE
21
+ skew (scroll-patterns #3). Deferred + feature-detected: if the CDN fails, the
22
+ hand-rolled rAF pop engine below still runs and the beat stays at a complete static state. -->
23
+ <script defer src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js"></script>
24
+ <script defer src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/ScrollTrigger.min.js"></script>
25
+
26
+ <style>
27
+ /* ───────────────────────────────────────────────────────────────
28
+ cinematic-scroll · Contemporary-pop / Gen-Z example ("BLOOM")
29
+ Single-file, no build, GitHub-Pages friendly.
30
+ Visual system: Warm Scrapbook (warmth + Gen-Z heart) crossed with
31
+ Storybook Geometry snap pacing — fast, playful, but art-directed.
32
+ Motion grammar = the skill's vanilla engine:
33
+ sticky pin + rAF-throttled scroll + depth-multiplied parallax
34
+ (wide gaps 0.15→1.40) + IntersectionObserver scroll-spy + bg morph.
35
+ Compositor-only: per frame we mutate ONLY transform + opacity.
36
+ Reduced-motion + touch + mobile (<=680px) degrade to a clean static
37
+ mid-state (full opacity, words settled, no scrolljack).
38
+ ─────────────────────────────────────────────────────────────── */
39
+
40
+ :root{
41
+ --pink:#FF2E93; --pink-2:#FF74C0; --lime:#C6FF3D; --lime-2:#9BE000;
42
+ --violet:#7C3AED; --cyan:#22E0E0;
43
+ --ink:#160B1F; --paper:#FFF4FB;
44
+ --bg:#FF2E93; --fg:#160B1F; --fg-muted:#5A2F4E; --panel:#FFFFFF;
45
+ --accent:var(--lime-2);
46
+
47
+ --disp:"Plus Jakarta Sans", system-ui, sans-serif;
48
+ --mono:"Space Grotesk", ui-monospace, "SFMono-Regular", Menlo, monospace;
49
+ }
50
+ *{ margin:0; padding:0; box-sizing:border-box; }
51
+ html{ scroll-behavior:smooth; }
52
+ body{
53
+ background:var(--bg); color:var(--fg);
54
+ font-family:var(--disp); font-weight:600;
55
+ -webkit-font-smoothing:antialiased; overflow-x:hidden;
56
+ transition:background .55s cubic-bezier(.22,1,.36,1);
57
+ }
58
+ ::selection{ background:var(--lime); color:var(--ink); }
59
+ a{ color:inherit; }
60
+
61
+ /* gradient wash + grain — fixed, cheap, never animated in scroll loop */
62
+ .wash{ position:fixed; inset:0; z-index:-2; opacity:.9;
63
+ background:
64
+ radial-gradient(120% 90% at 12% 4%, rgba(198,255,61,.55), rgba(198,255,61,0) 46%),
65
+ radial-gradient(120% 90% at 92% 98%, rgba(124,58,237,.40), rgba(124,58,237,0) 52%);
66
+ transition:background .55s cubic-bezier(.22,1,.36,1); }
67
+ .grain{ position:fixed; inset:0; z-index:-1; pointer-events:none; opacity:.06; mix-blend-mode:overlay;
68
+ background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
69
+
70
+ /* ── fixed masthead ───────────────────────────────────────── */
71
+ .masthead{
72
+ position:fixed; inset:0 0 auto 0; z-index:50;
73
+ display:flex; align-items:center; justify-content:space-between;
74
+ padding:clamp(16px,2.4vw,24px) clamp(18px,4vw,52px);
75
+ padding-top:max(clamp(16px,2.4vw,24px), env(safe-area-inset-top));
76
+ }
77
+ .masthead .mark{ display:flex; align-items:center; gap:9px; pointer-events:auto; }
78
+ .masthead .mark .glyph{
79
+ width:26px; height:26px; border-radius:9px; flex:none;
80
+ background:conic-gradient(from 200deg, var(--lime), var(--cyan), var(--pink), var(--lime));
81
+ box-shadow:0 4px 14px rgba(22,11,31,.25);
82
+ }
83
+ .masthead .mark b{ font-family:var(--disp); font-weight:800; font-size:18px; letter-spacing:-.02em; color:var(--ink); }
84
+ .masthead nav{ display:flex; gap:clamp(8px,1.6vw,18px); align-items:center; }
85
+ .masthead nav a{
86
+ font-family:var(--mono); font-size:11px; font-weight:500; letter-spacing:.08em;
87
+ text-transform:lowercase; text-decoration:none; color:var(--ink); opacity:.78;
88
+ padding:7px 12px; border-radius:999px; transition:opacity .25s, background .25s;
89
+ }
90
+ .masthead nav a:hover{ opacity:1; }
91
+ .masthead nav a.pill{ background:var(--ink); color:var(--lime); opacity:1; font-weight:700; }
92
+ @media (max-width:680px){ .masthead nav a:not(.pill){ display:none; } }
93
+
94
+ /* ── progress rail ─────────────────────────────────────────── */
95
+ .rail{
96
+ position:fixed; right:clamp(12px,2vw,26px); top:50%; transform:translateY(-50%);
97
+ z-index:40; display:flex; flex-direction:column; gap:14px;
98
+ padding-right:env(safe-area-inset-right);
99
+ }
100
+ .rail .r{ display:flex; align-items:center; gap:10px; justify-content:flex-end;
101
+ background:none; border:none; cursor:pointer; padding:0;
102
+ font-family:var(--mono); font-size:11px; font-weight:700; color:var(--ink); opacity:.45;
103
+ transition:opacity .3s; }
104
+ .rail .r .dot{ width:9px; height:9px; border-radius:50%; background:currentColor; transition:transform .3s, background .3s; }
105
+ .rail .r.on{ opacity:1; }
106
+ .rail .r.on .dot{ transform:scale(1.5); background:var(--ink); box-shadow:0 0 0 4px rgba(22,11,31,.14); }
107
+ @media (max-width:680px){ .rail{ display:none; } }
108
+
109
+ /* ── chapter section ──────────────────────────────────────── */
110
+ section{ position:relative; min-height:200vh; } /* short pins → snappy pace */
111
+ .stage{
112
+ position:sticky; top:0; height:100vh; overflow:hidden;
113
+ display:grid; grid-template-columns:1fr; align-content:center;
114
+ padding:0 clamp(20px,6vw,96px);
115
+ }
116
+
117
+ /* giant rounded type — the hero */
118
+ .copytext{ position:relative; z-index:6; max-width:min(94vw,1180px); will-change:transform,opacity; }
119
+ .eyebrow{ display:inline-flex; align-items:center; gap:8px;
120
+ font-family:var(--mono); font-size:clamp(10px,1vw,12px); font-weight:700; letter-spacing:.12em;
121
+ text-transform:uppercase; color:var(--ink);
122
+ background:var(--lime); padding:7px 13px; border-radius:999px; margin-bottom:clamp(16px,2vw,26px);
123
+ box-shadow:0 6px 18px rgba(155,224,0,.4); }
124
+ .eyebrow .spark{ width:13px; height:13px; flex:none; }
125
+ h2.giant{
126
+ font-family:var(--disp); font-weight:800; line-height:.92; letter-spacing:-.035em;
127
+ font-size:clamp(2.7rem,10.5vw,9.4rem); color:var(--ink);
128
+ }
129
+ h2.giant .w{ display:inline-block; margin-right:.18em; will-change:transform,opacity; }
130
+ h2.giant .w.accent{
131
+ background:linear-gradient(100deg, var(--violet), var(--pink) 60%, var(--lime-2));
132
+ -webkit-background-clip:text; background-clip:text; color:transparent;
133
+ }
134
+ /* body copy sits on a solid panel → legible over any gradient */
135
+ .lede{ margin-top:clamp(20px,2.6vw,32px); max-width:50ch;
136
+ display:inline-block; padding:16px 20px; border-radius:18px;
137
+ background:var(--panel); color:#3A2030;
138
+ font-family:var(--disp); font-weight:600; font-size:clamp(1rem,1.45vw,1.22rem); line-height:1.5;
139
+ box-shadow:0 14px 40px -18px rgba(22,11,31,.5); }
140
+ .ctarow{ margin-top:clamp(20px,2.4vw,30px); display:flex; gap:12px; flex-wrap:wrap; align-items:center; }
141
+ .btn{ font-family:var(--disp); font-weight:700; font-size:14px; text-decoration:none;
142
+ padding:13px 24px; border-radius:999px; transition:transform .25s; will-change:transform; }
143
+ .btn.solid{ background:var(--ink); color:var(--lime); box-shadow:0 12px 30px -10px rgba(22,11,31,.6); }
144
+ .btn.ghost{ background:rgba(255,255,255,.7); color:var(--ink); }
145
+ .btn:hover{ transform:translateY(-2px) scale(1.03); }
146
+ .ctameta{ font-family:var(--mono); font-size:11px; font-weight:500; color:var(--ink); opacity:.7; }
147
+
148
+ /* ghost watermark word — deep background, fast horizontal sweep */
149
+ .ghost{
150
+ position:absolute; z-index:1; font-family:var(--disp); font-weight:800; text-transform:lowercase;
151
+ font-size:clamp(8rem,30vw,26rem); line-height:.8; letter-spacing:-.05em;
152
+ color:#fff; opacity:.14; white-space:nowrap; pointer-events:none;
153
+ top:50%; left:50%; transform:translate(-50%,-50%); will-change:transform; }
154
+
155
+ /* ── the floating app-UI layer (high depth, drifts up fast) ── */
156
+ .appwrap{ position:absolute; z-index:7; right:clamp(20px,6vw,96px); top:50%;
157
+ width:clamp(220px,26vw,330px); aspect-ratio:9/19; transform:translateY(-50%);
158
+ will-change:transform,opacity; }
159
+ .phone{ position:relative; width:100%; height:100%; border-radius:38px; overflow:hidden;
160
+ background:#0E0716;
161
+ border:5px solid #160B1F;
162
+ box-shadow:0 40px 90px -28px rgba(22,11,31,.7), 0 0 0 1px rgba(255,255,255,.12) inset; }
163
+ .phone .notch{ position:absolute; top:9px; left:50%; transform:translateX(-50%); z-index:3;
164
+ width:34%; height:18px; border-radius:999px; background:#160B1F; }
165
+ /* the screenshot — real image if present, else CSS-only UI mock */
166
+ .shot{ position:absolute; inset:0; background-size:cover; background-position:center top; }
167
+ .shot.placeholder{
168
+ background:
169
+ radial-gradient(120% 60% at 20% 0%, rgba(198,255,61,.9), rgba(198,255,61,0) 55%),
170
+ linear-gradient(170deg, var(--pink) 0%, var(--violet) 100%);
171
+ }
172
+ /* CSS-only app chrome that reads as a real screen even with NO image */
173
+ .uimock{ position:absolute; inset:0; padding:34px 14px 16px; display:flex; flex-direction:column; gap:11px; }
174
+ .uimock .status{ display:flex; justify-content:space-between; align-items:center;
175
+ font-family:var(--mono); font-size:10px; font-weight:700; color:#fff; opacity:.85; padding:0 4px; }
176
+ .uimock .hero{ border-radius:18px; padding:13px 14px; background:rgba(255,255,255,.16);
177
+ backdrop-filter:blur(8px); border:1px solid rgba(255,255,255,.28); color:#fff; }
178
+ .uimock .hero .big{ font-family:var(--disp); font-weight:800; font-size:21px; line-height:1; letter-spacing:-.02em; }
179
+ .uimock .hero .sub{ font-family:var(--mono); font-size:9.5px; margin-top:6px; opacity:.85; }
180
+ .uimock .ring{ width:46px; height:46px; border-radius:50%; flex:none;
181
+ background:conic-gradient(var(--lime) 0% 72%, rgba(255,255,255,.22) 72% 100%); position:relative; }
182
+ .uimock .ring::after{ content:""; position:absolute; inset:6px; border-radius:50%; background:rgba(20,7,30,.55); }
183
+ .uimock .row{ display:flex; align-items:center; gap:11px;
184
+ border-radius:15px; padding:11px 12px; background:rgba(255,255,255,.14);
185
+ border:1px solid rgba(255,255,255,.22); }
186
+ .uimock .row .ic{ width:30px; height:30px; border-radius:10px; flex:none; }
187
+ .uimock .row .tx{ flex:1; }
188
+ .uimock .row .tx b{ display:block; font-family:var(--disp); font-weight:700; font-size:12px; color:#fff; }
189
+ .uimock .row .tx span{ display:block; font-family:var(--mono); font-size:9px; color:#fff; opacity:.7; margin-top:3px; }
190
+ .uimock .row .chk{ width:18px; height:18px; border-radius:6px; flex:none; }
191
+ .uimock .tabbar{ margin-top:auto; display:flex; justify-content:space-around;
192
+ border-radius:18px; padding:11px 8px; background:rgba(255,255,255,.18);
193
+ border:1px solid rgba(255,255,255,.26); }
194
+ .uimock .tabbar i{ width:18px; height:18px; border-radius:6px; background:rgba(255,255,255,.5); }
195
+ .uimock .tabbar i.act{ background:var(--lime); }
196
+
197
+ .figcap{ position:absolute; left:0; bottom:-26px; width:100%; display:flex; justify-content:space-between;
198
+ font-family:var(--mono); font-size:10px; font-weight:500; letter-spacing:.08em; color:var(--ink); opacity:.7; }
199
+
200
+ /* playful sticker accents (CSS shapes) — mid-fast depth */
201
+ .sticker{ position:absolute; z-index:5; will-change:transform; pointer-events:none;
202
+ font-family:var(--disp); font-weight:800; }
203
+ .sticker.tag{ display:inline-flex; align-items:center; gap:6px;
204
+ background:#fff; color:var(--ink); font-size:13px; padding:9px 14px; border-radius:999px;
205
+ box-shadow:0 12px 30px -10px rgba(22,11,31,.5); transform:rotate(-7deg); }
206
+ .sticker.tag .em{ width:15px; height:15px; }
207
+ .blob{ position:absolute; z-index:2; border-radius:46% 54% 60% 40%/ 52% 44% 56% 48%;
208
+ will-change:transform; pointer-events:none; opacity:.9; mix-blend-mode:overlay; }
209
+
210
+ /* floating glassy stat card (foreground, fastest depth) */
211
+ .glasscard{ position:absolute; z-index:8; left:clamp(20px,6vw,96px); bottom:clamp(56px,11vh,118px);
212
+ width:min(280px,74vw); padding:15px 17px; border-radius:20px;
213
+ background:rgba(255,255,255,.62); backdrop-filter:blur(14px);
214
+ border:1px solid rgba(255,255,255,.7); box-shadow:0 20px 50px -20px rgba(22,11,31,.5);
215
+ will-change:transform,opacity; }
216
+ .glasscard .t{ display:flex; align-items:center; gap:8px; font-family:var(--mono); font-size:11px; font-weight:700;
217
+ letter-spacing:.04em; color:var(--violet); margin-bottom:8px; }
218
+ .glasscard .pulse{ width:8px; height:8px; border-radius:50%; background:var(--lime-2); animation:pulse 1.8s infinite; }
219
+ .glasscard .b{ font-family:var(--disp); font-weight:600; font-size:12.5px; line-height:1.45; color:#2A1622; }
220
+ @keyframes pulse{ 0%,100%{ opacity:1; transform:scale(1);} 50%{ opacity:.4; transform:scale(.8);} }
221
+
222
+ /* scroll cue (first chapter only) */
223
+ .cue{ position:absolute; left:50%; bottom:24px; transform:translateX(-50%); z-index:30;
224
+ display:flex; flex-direction:column; align-items:center; gap:7px;
225
+ font-family:var(--mono); font-size:10px; font-weight:700; letter-spacing:.16em; text-transform:uppercase; color:var(--ink); }
226
+ .cue .bar{ width:2px; height:34px; border-radius:2px; background:var(--ink); transform-origin:top;
227
+ animation:cuebar 1.8s infinite ease-in-out; }
228
+ @keyframes cuebar{ 0%,100%{ transform:scaleY(1);} 50%{ transform:scaleY(.4);} }
229
+
230
+ /* ── footer ──────────────────────────────────────────────── */
231
+ footer{ position:relative; padding:16vh clamp(20px,6vw,96px) 14vh; }
232
+ footer .big{ font-family:var(--disp); font-weight:800; text-transform:none;
233
+ font-size:clamp(2rem,8vw,5.6rem); line-height:.94; letter-spacing:-.03em; color:var(--ink); }
234
+ footer .big .grad{ background:linear-gradient(100deg, var(--violet), var(--pink) 55%, var(--lime-2));
235
+ -webkit-background-clip:text; background-clip:text; color:transparent; }
236
+ footer .fcta{ margin-top:30px; display:inline-block;
237
+ font-family:var(--disp); font-weight:700; font-size:15px; text-decoration:none;
238
+ padding:15px 30px; border-radius:999px; background:var(--ink); color:var(--lime);
239
+ box-shadow:0 16px 36px -12px rgba(22,11,31,.6); transition:transform .25s; }
240
+ footer .fcta:hover{ transform:translateY(-2px) scale(1.03); }
241
+ footer .meta{ margin-top:30px; font-family:var(--mono); font-size:11px; font-weight:500;
242
+ letter-spacing:.06em; color:var(--ink); opacity:.78; max-width:60ch; }
243
+ footer .meta a{ color:var(--violet); text-decoration:underline; text-underline-offset:2px; }
244
+
245
+ /* ── mobile (<=680px): clean STATIC mid-state, no scrolljack ── */
246
+ @media (max-width:680px){
247
+ section{ min-height:auto; padding:13vh 0; }
248
+ .stage{ position:static; height:auto; display:block; padding:0 22px; }
249
+ .ghost{ display:none; }
250
+ .appwrap{ position:static; width:min(64vw,260px); transform:none!important; opacity:1!important;
251
+ margin:30px 0 8px; }
252
+ .figcap{ position:static; margin-top:30px; }
253
+ .sticker, .blob{ display:none; }
254
+ .glasscard{ position:static; margin-top:20px; width:100%; transform:none!important; opacity:1!important; }
255
+ h2.giant{ font-size:15vw; }
256
+ .copytext .w{ opacity:1!important; transform:none!important; }
257
+ .lede{ display:block; }
258
+ .cue{ display:none; }
259
+ }
260
+ /* ── reduced motion: settled mid-state, no scroll-driven motion ── */
261
+ @media (prefers-reduced-motion:reduce){
262
+ html{ scroll-behavior:auto; }
263
+ .copytext .w, .copytext, .appwrap, .glasscard, .ghost, .sticker, .blob{
264
+ opacity:1!important; transform:none!important; }
265
+ .glasscard .pulse, .cue .bar{ animation:none; }
266
+ }
267
+
268
+ /* ── GSAP SHOWCASE — "THE DROP" ─────────────────────────────────────
269
+ A dedicated viewport demoing two techniques the rAF pop engine doesn't:
270
+ • JUMP-SCARE micro (taste-guardrails §2 "Jump scare — Gen-Z energy"):
271
+ the big sticker snaps scale 0.8→1.05 + rotateZ(-2→0) with back.out(2).
272
+ • VELOCITY-REACTIVE (scroll-patterns #3): the marquee rows skewX + scaleY
273
+ react to lerped scroll SPEED (transform-only, capped, desktop-only).
274
+ GSAP pins .drop-stage; the STATIC DEFAULTS below keep it whole when GSAP is
275
+ absent / reduced-motion / mobile (sticker landed at rest, rows un-skewed). */
276
+ section.showcase{ min-height:0; } /* GSAP pin owns the scroll room */
277
+ .drop-stage{ position:relative; height:100vh; overflow:hidden;
278
+ display:grid; place-items:center; padding:0 clamp(20px,6vw,96px); }
279
+ /* candy-pink + electric-lime backdrop wash, self-contained to the beat */
280
+ .drop-stage .dropwash{ position:absolute; inset:-10%; z-index:0; pointer-events:none;
281
+ background:
282
+ radial-gradient(80% 60% at 50% 14%, rgba(198,255,61,.6), rgba(198,255,61,0) 56%),
283
+ radial-gradient(90% 70% at 50% 100%, rgba(124,58,237,.45), rgba(124,58,237,0) 60%),
284
+ linear-gradient(165deg, var(--pink) 0%, var(--violet) 100%); }
285
+ .drop-inner{ position:relative; z-index:2; display:flex; flex-direction:column;
286
+ align-items:center; gap:clamp(22px,3vw,40px); text-align:center; width:100%; }
287
+ .drop-eyebrow{ display:inline-flex; align-items:center; gap:8px;
288
+ font-family:var(--mono); font-size:clamp(10px,1vw,12px); font-weight:700; letter-spacing:.16em;
289
+ text-transform:uppercase; color:var(--ink);
290
+ background:var(--lime); padding:7px 14px; border-radius:999px;
291
+ box-shadow:0 6px 18px rgba(155,224,0,.4); }
292
+ /* the velocity-reactive marquee rows — giant rounded type, skew on scroll speed */
293
+ .drop-rows{ display:flex; flex-direction:column; gap:clamp(4px,.6vw,10px); width:100%; }
294
+ .drop-rows .vrow{ font-family:var(--disp); font-weight:800; line-height:.92; letter-spacing:-.04em;
295
+ font-size:clamp(2.4rem,9vw,8rem); color:#fff; transform-origin:left center; white-space:nowrap; }
296
+ .drop-rows .vrow.accent{
297
+ background:linear-gradient(100deg, var(--lime), #fff 55%, var(--cyan));
298
+ -webkit-background-clip:text; background-clip:text; color:transparent; }
299
+ /* the JUMP-SCARE sticker / CTA — lands at its resting state by default */
300
+ .drop-stamp{ position:relative; z-index:3;
301
+ font-family:var(--disp); font-weight:800; font-size:clamp(1rem,2.2vw,1.5rem);
302
+ color:var(--ink); background:#fff; text-decoration:none;
303
+ padding:16px 30px; border-radius:999px; transform:rotate(0deg) scale(1);
304
+ box-shadow:0 18px 44px -14px rgba(22,11,31,.6); transform-origin:center;
305
+ display:inline-flex; align-items:center; gap:10px; }
306
+ .drop-stamp .em{ width:20px; height:20px; flex:none; }
307
+ @media (max-width:680px){
308
+ /* static / stacked — no pin, no skew, sticker simply present */
309
+ .drop-stage{ height:auto; padding:13vh 22px; }
310
+ .drop-stage .dropwash{ inset:0; }
311
+ .drop-rows .vrow{ font-size:13vw; white-space:normal; transform:none!important; }
312
+ .drop-stamp{ transform:none!important; }
313
+ }
314
+ </style>
315
+ </head>
316
+ <body>
317
+ <div class="wash" id="wash" aria-hidden="true"></div>
318
+ <div class="grain" aria-hidden="true"></div>
319
+
320
+ <header class="masthead">
321
+ <div class="mark">
322
+ <span class="glyph" aria-hidden="true"></span>
323
+ <b>BLOOM</b>
324
+ </div>
325
+ <nav>
326
+ <a href="#focus">focus</a>
327
+ <a href="#flow">flow</a>
328
+ <a href="#crew">crew</a>
329
+ <a class="pill" href="#start">get the app</a>
330
+ </nav>
331
+ </header>
332
+
333
+ <nav class="rail" id="rail" aria-label="Chapters"></nav>
334
+
335
+ <main id="app"></main>
336
+
337
+ <footer id="start">
338
+ <div class="big">your day,<br><span class="grad">but make it bloom.</span></div>
339
+ <a class="fcta" href="#" aria-label="Download BLOOM (demo link)">Download free → it&rsquo;s giving productive</a>
340
+ <p class="meta">
341
+ Built with the <a href="https://github.com/MustBeSimo/cinematic-scroll-skill">cinematic-scroll</a>
342
+ Agent Skill · BLOOM is a fictional brand for demo purposes · one of many possible aesthetics.
343
+ </p>
344
+ </footer>
345
+
346
+ <script>
347
+ /* ───────────────────────────────────────────────────────────────
348
+ Manifest — drive the page from data, exactly as the skill teaches.
349
+ Edit this array to retheme; DOM + motion are generated from it.
350
+ ─────────────────────────────────────────────────────────────── */
351
+ const CHAPTERS = [
352
+ {
353
+ id:'0-hero', n:'01', eyebrow:'new · 2026',
354
+ title:[['your day,',0],['but make it',0],['fun.',1]],
355
+ lede:'BLOOM is the productivity app that doesn’t feel like homework. Plan it, track it, share the wins — and actually want to open it tomorrow.',
356
+ cta:{ a:'Get the app', b:'free, obviously' },
357
+ sticker:'no more sticky notes',
358
+ card:{ tag:'streak: 14 days', body:'You just hit a two-week streak. The crew can see it. Flex earned. 🔥' },
359
+ fig:'BLOOM · v3.0', figLabel:'home',
360
+ morph:'#FF2E93', cue:true,
361
+ /* UI mock theme (CSS-only fallback) */
362
+ ui:{ grad:'linear-gradient(170deg,#FF2E93 0%,#7C3AED 100%)', greet:'Hey, you ✨', sub:'3 things today. you got this.' }
363
+ },
364
+ {
365
+ id:'1-focus', n:'02', eyebrow:'feature · focus',
366
+ title:[['lock in.',0],['blooms',1],['follow.',0]],
367
+ lede:'Start a Focus Bloom and a little plant grows while you work. Phone face-down, brain on. Bail early and… well, it wilts. Gentle pressure, big payoff.',
368
+ cta:{ a:'Try a focus bloom', b:'25 min · zero guilt' },
369
+ sticker:'+40% deep work',
370
+ card:{ tag:'focus · live', body:'18:42 left in this Bloom. Your record this week is 4 in a row. Beat it? 🌱' },
371
+ fig:'BLOOM · focus', figLabel:'session',
372
+ morph:'#7C3AED',
373
+ ui:{ grad:'linear-gradient(170deg,#7C3AED 0%,#22E0E0 100%)', greet:'Focus Bloom', sub:'face down. plant up. let it grow.' }
374
+ },
375
+ {
376
+ id:'2-flow', n:'03', eyebrow:'feature · flow',
377
+ title:[['plans that',0],['actually',1],['flow.',0]],
378
+ lede:'Drag, drop, done. BLOOM reshuffles your day the second life happens — meeting moved, energy dipped, vibe shifted. Your schedule keeps up with you, not the other way round.',
379
+ cta:{ a:'See flow mode', b:'auto-reschedule' },
380
+ sticker:'drag · drop · done',
381
+ card:{ tag:'flow · synced', body:'Bumped 3 tasks to tomorrow, kept your gym slot. Day re-bloomed in 0.2s. ✅' },
382
+ fig:'BLOOM · flow', figLabel:'today',
383
+ morph:'#C6FF3D',
384
+ ui:{ grad:'linear-gradient(170deg,#9BE000 0%,#22E0E0 100%)', greet:'Today, sorted', sub:'auto-shuffled around your energy.' }
385
+ },
386
+ {
387
+ id:'3-crew', n:'04', eyebrow:'feature · crew',
388
+ title:[['better',0],['together.',1]],
389
+ lede:'Add your crew, share streaks, send a little nudge when someone’s slacking (lovingly). Productivity is way more fun when it’s a group chat that gets stuff done.',
390
+ cta:{ a:'Build your crew', b:'invite 5 friends' },
391
+ sticker:'group chat that ships',
392
+ card:{ tag:'crew · 6 online', body:'Maya finished her Bloom. Leo nudged you. The group streak is alive — don’t break it. 💪' },
393
+ fig:'BLOOM · crew', figLabel:'circle',
394
+ morph:'#22E0E0',
395
+ ui:{ grad:'linear-gradient(170deg,#22E0E0 0%,#FF2E93 100%)', greet:'Your crew', sub:'6 blooming right now.' }
396
+ },
397
+ ];
398
+
399
+ /* per-chapter wash gradients (background morph behind everything) */
400
+ const WASH = {
401
+ '#FF2E93':'radial-gradient(120% 90% at 12% 4%, rgba(198,255,61,.55), rgba(198,255,61,0) 46%), radial-gradient(120% 90% at 92% 98%, rgba(124,58,237,.40), rgba(124,58,237,0) 52%)',
402
+ '#7C3AED':'radial-gradient(120% 90% at 88% 6%, rgba(34,224,224,.45), rgba(34,224,224,0) 50%), radial-gradient(120% 90% at 8% 96%, rgba(255,46,147,.42), rgba(255,46,147,0) 52%)',
403
+ '#C6FF3D':'radial-gradient(120% 90% at 14% 6%, rgba(34,224,224,.45), rgba(34,224,224,0) 50%), radial-gradient(120% 90% at 90% 96%, rgba(124,58,237,.30), rgba(124,58,237,0) 52%)',
404
+ '#22E0E0':'radial-gradient(120% 90% at 90% 4%, rgba(255,46,147,.48), rgba(255,46,147,0) 50%), radial-gradient(120% 90% at 8% 98%, rgba(198,255,61,.48), rgba(198,255,61,0) 52%)',
405
+ };
406
+ /* foreground color is dark ink on every chapter — gradients are bright */
407
+
408
+ const SPARK = "<svg class='spark' viewBox='0 0 24 24' fill='none' aria-hidden='true'><path d='M12 2l2.2 6.6L21 12l-6.8 3.4L12 22l-2.2-6.6L3 12l6.8-3.4z' fill='%23160B1F'/></svg>".replace('%23','#');
409
+
410
+ const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));
411
+ const smooth=(e0,e1,x)=>{ if(x<=e0)return 0; if(x>=e1)return 1; const t=(x-e0)/(e1-e0); return t*t*(3-2*t); };
412
+ const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
413
+ const isTouch = matchMedia('(hover: none) and (pointer: coarse)').matches;
414
+ const isMobile = () => innerWidth <= 680;
415
+
416
+ const app = document.getElementById('app');
417
+ const rail = document.getElementById('rail');
418
+ const wash = document.getElementById('wash');
419
+
420
+ /* preflight: which real screenshots exist? fall back to CSS UI mock */
421
+ function probe(src){ return new Promise(res=>{ const i=new Image(); i.onload=()=>res(true); i.onerror=()=>res(false); i.src=src; }); }
422
+
423
+ const ghostWord = t => (t.flat().filter(x=>typeof x==='string').join(' ').split(' ')[0]||'').toLowerCase().replace(/[^a-z]/g,'');
424
+
425
+ (async () => {
426
+ for (const ch of CHAPTERS){
427
+ const sec = document.createElement('section');
428
+ sec.id = ch.id.replace(/^\d+-/,'');
429
+ sec.dataset.morph = ch.morph;
430
+
431
+ const titleHTML = ch.title.map(([chunk,acc]) =>
432
+ chunk.split(' ').map(w=>`<span class="w${acc?' accent':''}">${w}</span>`).join(' ')
433
+ ).join(' ');
434
+
435
+ const imgPath = `assets/${ch.id}.jpg`;
436
+ const hasImg = await probe(imgPath);
437
+
438
+ // floating phone — real screenshot, or a CSS-only app mock
439
+ const screen = hasImg
440
+ ? `<div class="shot" style="background-image:url('${imgPath}')"></div>`
441
+ : `<div class="shot placeholder" style="background:${ch.ui.grad}">
442
+ <div class="uimock">
443
+ <div class="status"><span>9:41</span><span>BLOOM</span></div>
444
+ <div class="hero">
445
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:10px">
446
+ <div><div class="big">${ch.ui.greet}</div><div class="sub">${ch.ui.sub}</div></div>
447
+ <div class="ring"></div>
448
+ </div>
449
+ </div>
450
+ <div class="row"><span class="ic" style="background:var(--lime)"></span>
451
+ <span class="tx"><b>Morning bloom</b><span>25 min · deep work</span></span>
452
+ <span class="chk" style="background:var(--lime)"></span></div>
453
+ <div class="row"><span class="ic" style="background:var(--pink-2)"></span>
454
+ <span class="tx"><b>Reply to Maya</b><span>2 min · quick win</span></span>
455
+ <span class="chk" style="background:rgba(255,255,255,.3)"></span></div>
456
+ <div class="row"><span class="ic" style="background:var(--cyan)"></span>
457
+ <span class="tx"><b>Move your body</b><span>20 min · later</span></span>
458
+ <span class="chk" style="background:rgba(255,255,255,.3)"></span></div>
459
+ <div class="tabbar"><i class="act"></i><i></i><i></i><i></i></div>
460
+ </div>
461
+ </div>`;
462
+
463
+ sec.innerHTML = `
464
+ <div class="stage">
465
+ <div class="ghost" data-depth="0.15">${ghostWord(ch.title)}</div>
466
+ <span class="blob" data-depth="0.40" style="left:6%; top:18%; width:clamp(120px,18vw,260px); height:clamp(120px,18vw,260px); background:radial-gradient(circle at 35% 30%, var(--lime), var(--cyan));"></span>
467
+
468
+ <div class="appwrap" data-depth="1.40">
469
+ <div class="phone">
470
+ <div class="notch"></div>
471
+ ${screen}
472
+ </div>
473
+ <div class="figcap"><span>${ch.fig}</span><span>${ch.figLabel}</span></div>
474
+ </div>
475
+
476
+ <span class="sticker tag" data-depth="0.85" style="right:clamp(20px,12vw,180px); top:24%;">
477
+ ${SPARK}${ch.sticker}
478
+ </span>
479
+
480
+ <div class="copytext">
481
+ <div class="eyebrow">${SPARK}${ch.eyebrow}</div>
482
+ <h2 class="giant">${titleHTML}</h2>
483
+ <p class="lede">${ch.lede}</p>
484
+ <div class="ctarow">
485
+ <a class="btn solid" href="#start">${ch.cta.a} →</a>
486
+ <span class="ctameta">${ch.cta.b}</span>
487
+ </div>
488
+ </div>
489
+
490
+ <div class="glasscard" data-depth="1.20">
491
+ <div class="t"><span class="pulse"></span>${ch.card.tag}</div>
492
+ <div class="b">${ch.card.body}</div>
493
+ </div>
494
+
495
+ ${ch.cue ? `<div class="cue"><span>scroll</span><span class="bar"></span></div>` : ''}
496
+ </div>`;
497
+ app.appendChild(sec);
498
+
499
+ const r = document.createElement('button');
500
+ r.type='button'; r.className='r'; r.dataset.goto=sec.id;
501
+ r.setAttribute('aria-label', `Go to chapter ${ch.n}`);
502
+ r.innerHTML = `<span>${ch.n}</span><span class="dot"></span>`;
503
+ rail.appendChild(r);
504
+ }
505
+
506
+ /* ── INSERT THE GSAP SHOWCASE BEAT ("THE DROP") ─────────────────────
507
+ Inserted right after the hero so it reads: hero → the drop → focus.
508
+ Built BEFORE `sections`/`rails` are collected (and the center-spy uses
509
+ `sections.indexOf` against the parallel `rails` array), so a matching
510
+ lightweight rail entry is inserted at the SAME index to keep them aligned.
511
+ Pure markup here; motion is wired at the end (desktop-only, only if GSAP loaded). */
512
+ const drop = document.createElement('section');
513
+ drop.className = 'showcase'; drop.id = 'drop';
514
+ drop.dataset.morph = '#C6FF3D';
515
+ drop.innerHTML = `
516
+ <div class="drop-stage">
517
+ <div class="dropwash" aria-hidden="true"></div>
518
+ <div class="drop-inner">
519
+ <span class="drop-eyebrow">${SPARK}the drop</span>
520
+ <div class="drop-rows" aria-hidden="true">
521
+ <div class="vrow">make it</div>
522
+ <div class="vrow accent">loud.</div>
523
+ <div class="vrow">make it yours.</div>
524
+ </div>
525
+ <a class="drop-stamp" href="#start">${SPARK}get the app</a>
526
+ </div>
527
+ </div>`;
528
+ app.insertBefore(drop, app.children[1] || null); // after hero (index 0)
529
+ const dr = document.createElement('button');
530
+ dr.type='button'; dr.className='r'; dr.dataset.goto='drop';
531
+ dr.setAttribute('aria-label', 'Go to the drop');
532
+ dr.innerHTML = `<span>✦</span><span class="dot"></span>`;
533
+ rail.insertBefore(dr, rail.children[1] || null);
534
+
535
+ /* nav clicks (rail + masthead anchors handled natively) */
536
+ document.querySelectorAll('[data-goto]').forEach(el=>{
537
+ el.addEventListener('click',()=>{ document.getElementById(el.dataset.goto)?.scrollIntoView({ behavior: reduce ? 'auto':'smooth' }); });
538
+ });
539
+
540
+ const sections = [...document.querySelectorAll('section')];
541
+ const rails = [...rail.querySelectorAll('.r')];
542
+
543
+ /* background morph + active rail — collapse observer to a 1px center band
544
+ via rootMargin so it works even though sections are taller than viewport. */
545
+ function applyActive(sec){
546
+ const idx = sections.indexOf(sec);
547
+ document.body.style.background = sec.dataset.morph;
548
+ wash.style.background = WASH[sec.dataset.morph] || '';
549
+ rails.forEach((r,i)=> r.classList.toggle('on', i===idx));
550
+ }
551
+ const spy = new IntersectionObserver((entries)=>{
552
+ entries.forEach(e=>{ if (e.isIntersecting) applyActive(e.target); });
553
+ },{ rootMargin:'-50% 0px -50% 0px', threshold:0 });
554
+ sections.forEach(s=>spy.observe(s));
555
+ applyActive(sections[0]); // paint initial state immediately
556
+
557
+ /* mobile / reduced-motion: skip the rAF engine entirely → static mid-state */
558
+ if (reduce || isMobile()) return;
559
+
560
+ /* desktop parallax — FAST pop engine.
561
+ Unified approach→pin→exit timeline T∈[0,1]; never starts invisible.
562
+ Wide depth gaps (0.15 → 1.40) so layers separate hard and move at very
563
+ different speeds — the "app" (1.40) drifts UP faster than the title.
564
+ Compositor-only: per frame we touch ONLY transform + opacity. */
565
+ let ticking=false;
566
+ function parallax(){
567
+ const vh=innerHeight;
568
+ for(const sec of sections){
569
+ const rect=sec.getBoundingClientRect();
570
+ // perf: skip sections fully off-screen
571
+ if(rect.bottom < -vh*0.2 || rect.top > vh*1.2) continue;
572
+ const total=sec.offsetHeight+vh;
573
+ const T=clamp((vh-rect.top)/total,0,1);
574
+ const enter=smooth(0.06,0.30,T);
575
+ const exit=1-smooth(0.70,0.90,T);
576
+ const live=enter*exit;
577
+ const drift=0.46-T; // signed, ~0 at hold
578
+
579
+ // floating app — highest depth, big UPWARD travel (faster than title)
580
+ const appw=sec.querySelector('.appwrap');
581
+ if(appw){ const d=+appw.dataset.depth; // 1.40
582
+ const y = drift*340*d; // strong drift
583
+ const sc = 0.94 + 0.06*enter;
584
+ const rz = drift*4; // tiny playful tilt
585
+ appw.style.transform=`translateY(calc(-50% + ${y.toFixed(1)}px)) rotate(${rz.toFixed(2)}deg) scale(${sc.toFixed(3)})`;
586
+ appw.style.opacity=(0.15+0.85*live).toFixed(3);
587
+ }
588
+ // ghost watermark — deepest, slow long horizontal sweep
589
+ const ghost=sec.querySelector('.ghost');
590
+ if(ghost){ const d=+ghost.dataset.depth; // 0.15
591
+ ghost.style.transform=`translate(calc(-50% + ${(drift*520*d).toFixed(0)}px),-50%) scale(${(1.04-0.04*enter).toFixed(3)})`;
592
+ }
593
+ // background blob — low-mid depth, gentle counter-drift
594
+ const blob=sec.querySelector('.blob');
595
+ if(blob){ const d=+blob.dataset.depth; // 0.40
596
+ blob.style.transform=`translate(${(drift*-160*d).toFixed(1)}px, ${(drift*120*d).toFixed(1)}px) scale(${(1+0.08*enter).toFixed(3)})`;
597
+ }
598
+ // sticker tag — mid-fast depth, lively drift + wobble
599
+ const stick=sec.querySelector('.sticker');
600
+ if(stick){ const d=+stick.dataset.depth; // 0.85
601
+ stick.style.transform=`translateY(${(drift*230*d).toFixed(1)}px) rotate(${(-7+drift*9).toFixed(2)}deg)`;
602
+ stick.style.opacity=(smooth(0.16,0.36,T)*exit).toFixed(3);
603
+ }
604
+ // copy / title block + FAST per-word stagger (small offsets)
605
+ const copy=sec.querySelector('.copytext');
606
+ if(copy){
607
+ copy.style.transform=`translateY(${(drift*70).toFixed(1)}px)`;
608
+ copy.style.opacity=(0.10+0.90*live).toFixed(3);
609
+ const words=copy.querySelectorAll('h2 .w'); const n=words.length;
610
+ words.forEach((w,k)=>{
611
+ const t0=0.05+(k/n)*0.10; // tight, quick stagger
612
+ const wp=smooth(t0,t0+0.09,T);
613
+ const wy=(1-wp)*42;
614
+ w.style.opacity=wp.toFixed(3);
615
+ w.style.transform=`translateY(${wy.toFixed(1)}px)`;
616
+ });
617
+ }
618
+ // glassy stat card — fast foreground depth
619
+ const card=sec.querySelector('.glasscard');
620
+ if(card){ const d=+card.dataset.depth; // 1.20
621
+ card.style.transform=`translateY(${(drift*150*(d-1)*3.2).toFixed(1)}px) rotate(${(drift*-3).toFixed(2)}deg)`;
622
+ card.style.opacity=(smooth(0.20,0.42,T)*(1-smooth(0.66,0.86,T))).toFixed(3);
623
+ }
624
+ }
625
+ ticking=false;
626
+ }
627
+ function onScroll(){ if(!ticking){ ticking=true; requestAnimationFrame(parallax); } }
628
+ addEventListener('scroll',onScroll,{passive:true});
629
+ addEventListener('resize',()=>{ ticking=true; requestAnimationFrame(parallax); },{passive:true});
630
+ parallax(); // initial paint so nothing starts invisible
631
+
632
+ /* pointer micro-tilt on the phone (non-touch only — vestibular-safe) */
633
+ if(!isTouch && !reduce){
634
+ document.querySelectorAll('.phone').forEach(phone=>{
635
+ phone.parentElement.addEventListener('pointermove',ev=>{
636
+ const r=phone.getBoundingClientRect();
637
+ const px=(ev.clientX-r.left)/r.width-0.5, py=(ev.clientY-r.top)/r.height-0.5;
638
+ phone.style.transform=`perspective(900px) rotateY(${px*9}deg) rotateX(${-py*7}deg)`;
639
+ });
640
+ phone.parentElement.addEventListener('pointerleave',()=>{ phone.style.transform='perspective(900px) rotateY(0) rotateX(0)'; });
641
+ });
642
+ }
643
+
644
+ /* ── GSAP SHOWCASE WIRING — "THE DROP" (desktop & non-reduced) ───────
645
+ Guarded by the early return above (skips on reduce/mobile), then a
646
+ matchMedia gate so it auto-kills on resize into a small/reduced context.
647
+ Two techniques, kept ≤3 simultaneous motions in this single viewport:
648
+ • JUMP-SCARE micro (taste-guardrails §2): the stamp snaps in once at a
649
+ scroll threshold — scale 0.8→1.05 + rotateZ(-2→0), ease back.out(2),
650
+ NO scrub (a one-shot toggleActions tween). The signature moment.
651
+ • VELOCITY-REACTIVE (scroll-patterns #3): the marquee rows skewX +
652
+ scaleY off lerped scroll SPEED (deltaY/dt, lerp .15, capped), pushed
653
+ via gsap.quickTo — transform-only, disabled on touch.
654
+ If GSAP never loads, #drop simply stays at its complete static state. */
655
+ (function wireShowcase(tries){
656
+ if(!window.gsap || !window.ScrollTrigger){
657
+ if(tries>0) requestAnimationFrame(()=>wireShowcase(tries-1));
658
+ return;
659
+ }
660
+ gsap.registerPlugin(ScrollTrigger);
661
+ ScrollTrigger.defaults({ scrub:0.5, fastScrollEnd:true, invalidateOnRefresh:true });
662
+ const mm = gsap.matchMedia();
663
+ mm.add('(min-width:769px) and (prefers-reduced-motion:no-preference)', () => {
664
+ const stage = document.querySelector('#drop .drop-stage');
665
+ if(!stage) return;
666
+ const stamp = stage.querySelector('.drop-stamp');
667
+ const rows = [...stage.querySelectorAll('.vrow')];
668
+
669
+ /* JUMP-SCARE micro — one-shot, NOT scrubbed; fires when the beat lands.
670
+ scale 0.8→1.05 + rotateZ(-2→0), back.out(2) overshoot, then settles. */
671
+ stamp.style.willChange = 'transform';
672
+ const jump = gsap.fromTo(stamp,
673
+ { scale:0.8, rotationZ:-2, opacity:0 },
674
+ { scale:1.05, rotationZ:0, opacity:1, duration:0.5, ease:'back.out(2)', paused:true });
675
+ const jumpST = ScrollTrigger.create({
676
+ trigger:'#drop', start:'top 60%',
677
+ toggleActions:'play none none reverse',
678
+ onEnter:()=>jump.play(), onLeaveBack:()=>jump.reverse()
679
+ });
680
+
681
+ /* VELOCITY-REACTIVE — lerped scroll-speed → skewX + scaleY on the rows.
682
+ deltaY/dt tracked in a rAF loop (lerp .15, magnitude capped), applied
683
+ via gsap.quickTo. Transform-only; never touches width/gap. Desktop here
684
+ already excludes touch via the (min-width:769px) gate, but guard anyway. */
685
+ let velRAF=0, lastY=window.scrollY, lastT=performance.now(), v=0;
686
+ const skewTo = rows.map(r=>gsap.quickTo(r,'skewX',{duration:0.32, ease:'power3'}));
687
+ const scaleTo = rows.map(r=>gsap.quickTo(r,'scaleY',{duration:0.32, ease:'power3'}));
688
+ function trackVel(){
689
+ const now=performance.now(); const dt=Math.max(now-lastT,1);
690
+ const dy=window.scrollY-lastY;
691
+ v += (dy/dt - v)*0.15; // lerp toward instantaneous velocity
692
+ lastY=window.scrollY; lastT=now;
693
+ const cap=Math.max(-3,Math.min(3,v)); // cap magnitude (px/ms)
694
+ const skew = -cap*2.2; // skewX deg, direction-aware
695
+ const sY = 1 - Math.abs(cap)*0.02; // subtle vertical squash on fast scroll
696
+ rows.forEach((_,i)=>{ skewTo[i](skew); scaleTo[i](sY); });
697
+ velRAF=requestAnimationFrame(trackVel);
698
+ }
699
+ if(!isTouch) velRAF=requestAnimationFrame(trackVel);
700
+
701
+ return () => {
702
+ cancelAnimationFrame(velRAF);
703
+ jumpST.kill();
704
+ gsap.set([stamp, ...rows], { clearProps:'all' });
705
+ };
706
+ });
707
+ })(120);
708
+ })();
709
+ </script>
710
+ </body>
711
+ </html>