# Landing Preloader + Hero Reveal with Cursor Emoji Confetti

## Goal
Build a full-screen playful/editorial landing intro. A neutral beige **preloader** holds a small spinning square loader for a few seconds, then wipes upward off the top of the screen to reveal a bright-yellow hero. As it reveals, the two-line headline animates in character by character (each glyph slides up from below its clip mask), and a circular graphic medallion pops in at screen center and starts an **endless slow rotation**. The signature detail: **while the preloader is still running**, moving the cursor sprinkles round emoji "stickers" at the pointer — each pops in with a bouncy `back.out` ease, hangs for a beat, then drops off the bottom of the screen and unmounts. All motion is GSAP core; text is split into `<span>`s manually (no SplitText plugin).

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use **`gsap`** (npm) only — **no plugins** (no ScrollTrigger, no SplitText, no CustomEase) and no smooth-scroll library. The page does not scroll; it is a pure load-triggered sequence plus a `mousemove` particle spawner. `import gsap from "gsap";` at the top of the module. Everything runs immediately on module load (script is `type="module"`, placed at end of `<body>`).

## Layout / HTML
Semantic structure (class names are load-bearing — the JS/CSS query them):

```
<div class="container">
  <div class="preloader">
    <div class="loader"></div>
  </div>

  <div class="emojis"></div>   <!-- empty; runtime particles are appended here -->

  <section class="hero">
    <div class="nav">
      <div class="site-info">
        <p>Welcome to The Quiet Crowd! This is your gateway to a world of
           refined branding, where elegance meets ingenuity. Step into a realm
           where your brand's uniqueness whispers louder than a shout,
           captivating the hearts of your audience. The Quiet Crowd doesn't
           just create brands—it curates timeless identities that resonate.</p>
      </div>
      <div class="logo"><img src="/assets/logo.png" alt="" /></div>
      <div class="menu-btn"><img src="/assets/menu-btn.png" alt="" /></div>
    </div>

    <div class="hero-img"><img src="/assets/hero.png" alt="" /></div>

    <div class="header">
      <div class="header-row"><h1>The</h1></div>
      <div class="header-row"><h1>Quiet Crowd</h1></div>
    </div>
  </section>
</div>
```

Notes:
- Use **"The Quiet Crowd"** as the neutral placeholder brand. Headline is exactly two rows: `The` and `Quiet Crowd`.
- `.emojis` starts empty — the JS creates `<div class="emoji">` particles inside it at runtime.
- The nav is a 3-column row: welcome paragraph (left), logo (center), menu button (right).

## Styling
Fonts: body is **`"Inter"`** (import from Google Fonts, weight 400). Headline `h1` declares `font-family: "Cy Grotesk"` — this is an unavailable custom light display face; keep the declaration but let it fall back to a light-weight grotesque/sans (`font-weight: lighter`). Don't attempt to load the missing font.

Palette (exact hex — this is a high-contrast, offbeat scheme):
- Preloader background `#ded7ce` (warm beige); loader square `#c5beb5` (muted taupe).
- Hero background `#faec6c` (bright lemon yellow).
- Emoji chip background `#e3e3e3` (light grey — shows behind transparent stickers).
- Text is default black.

Global:
- `* { margin:0; padding:0; box-sizing:border-box }`.
- `img { width:100%; height:100%; object-fit:cover }`.
- `html, body { width:100%; height:100%; font-family:"Inter" }`.
- **Custom cursor:** `html, body { cursor: url("/assets/cursor.svg") 32 32, auto }` — a custom circular cursor graphic with a 32×32 hotspot.
- `.container { width:100%; height:100% }`.

Key elements and their **initial states** (the animation depends on these):

- `.preloader`: `position:fixed; inset:0; width:100vw; height:100vh; background:#ded7ce; display:flex; justify-content:center; align-items:center; overflow:hidden; z-index:1`. **Initial** `clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)` (full rectangle, covering everything). `will-change:clip-path`.
- `.loader`: `position:relative; width:40px; height:40px; background:#c5beb5; will-change:transform` (a small solid square, centered in the preloader).
- `.emojis`: `position:fixed; inset:0; width:100%; height:100%; overflow:hidden; pointer-events:none; z-index:2` (a full-screen clipping overlay above everything; never intercepts the pointer).
- `.emoji` (runtime particle): `position:absolute; background:#e3e3e3; border-radius:100%; overflow:hidden; pointer-events:none; will-change:transform; background-repeat:no-repeat; background-position:50% 50%; background-size:cover` (a **circular** chip whose sticker image cover-fills it; width/height/left/top/background-image are set inline per-particle).
- `.hero`: `position:relative; width:100vw; height:100vh; overflow:hidden; background:#faec6c; display:flex; flex-direction:column; justify-content:space-between; z-index:0` (nav pinned to top, header pinned to bottom).
- `.nav`: `width:100vw; display:flex; align-items:flex-start`. `.nav > div { flex:1; padding:1.5em }` (three equal columns).
- `.site-info p`: `width:450px; font-size:18px; font-weight:400`.
- `.logo`: `display:flex; justify-content:center`. `.logo img { width:200px; object-fit:contain; transform:scale(0.5) }` (rendered ~100px, centered).
- `.menu-btn`: `display:flex; justify-content:flex-end`. `.menu-btn img { width:70px; object-fit:contain }` (top-right).
- `.hero-img`: `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%) scale(0); width:40%; aspect-ratio:1; transform-origin:center; will-change:transform` (a square medallion at exact screen center, **starts at scale 0** = invisible).
- `.header`: `width:100%; display:flex; flex-direction:column; gap:0.75em; padding:1em 2em` (the two rows, bottom-left of hero).
- `.header-row`: `position:relative; padding-top:0.5em; clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%)` — a full-rect **clip mask** so the below-parked glyphs are hidden until they slide up into the row.
- `.header h1`: `font-family:"Cy Grotesk"; font-size:9.5vw; font-weight:lighter; line-height:90%` (huge, thin).
- `.header h1 span`: `position:relative; display:inline-block; transform:translateY(200px)` — **every character is parked 200px below** its baseline, clipped by the row's `clip-path`, waiting to slide up.

## GSAP effect (be exact)

### Setup — manual character split (no SplitText)
Before any animation, run a helper that splits both headline `h1`s into per-character `<span>`s:
```js
function splitTextIntoSpans(selector) {
  document.querySelectorAll(selector).forEach((el) => {
    el.innerHTML = el.innerText
      .split("")
      .map((c) => `<span>${c === " " ? "&nbsp;&nbsp;" : c}</span>`)
      .join("");
  });
}
splitTextIntoSpans(".header h1");
```
Each glyph becomes its own `<span>` (spaces → two `&nbsp;`). Combined with the CSS `.header h1 span { transform: translateY(200px) }`, every character starts pushed 200px down and clipped by its row mask.

### Preloader phase — three independent load tweens (fire immediately)

**1 — Preloader wipe (`delay: 5`):**
```js
gsap.to(".preloader", {
  clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
  duration: 1.5,
  delay: 5,
  ease: "power4.inOut",
});
```
After a **5s** hold, the preloader's clip-path collapses its two bottom corners up to the top edge — the whole beige panel **wipes upward off the top** over 1.5s (revealing the yellow hero beneath). Runs from t≈5.0 → t≈6.5.

**2 — Loader spin, then shrink (`delay: 1`):**
```js
gsap.to(".loader", {
  rotation: "+=180",
  duration: 1.5,
  delay: 1,
  repeat: 1,           // plays twice → two 180° half-turns, 3s total
  ease: "power4.inOut",
  onComplete: () => {
    gsap.to(".loader", {
      scale: 0,
      duration: 2,
      ease: "power4.inOut",
      onComplete: initializePageAnimations,  // hands off to the hero reveal
    });
  },
});
```
The small square starts spinning at t=1: `+=180°` with `power4.inOut`, `repeat:1` so it does the half-turn **twice** (3s of rotation, ends ~t=4). Then it scales `1 → 0` over 2s (ends ~t=6) and calls `initializePageAnimations`. So the hero reveal (below) kicks off at **t≈6**, overlapping the tail of the preloader wipe.

**3 — Emoji spawning gate:** a module-level `let isLoading = true;` is set to `false` as the very first line of `initializePageAnimations`. The `mousemove` particle spawner (below) only runs **while `isLoading` is true** — i.e. the emoji confetti is a preloader-only interaction and stops the moment the hero reveal begins.

### Hero reveal — `initializePageAnimations()` (one timeline, everything at position 0)
```js
function initializePageAnimations() {
  isLoading = false;
  const tl = gsap.timeline();

  // headline: each row's chars slide up into view, both rows in parallel
  document.querySelectorAll(".header-row").forEach((row) => {
    tl.to(row.querySelectorAll("span"), {
      y: 0,
      duration: 1,
      ease: "power4.out",
      stagger: { amount: 0.25, from: "start" },
    }, 0);
  });

  // center medallion pops in AND begins an endless spin, both at position 0
  tl.to(".hero-img", { scale: 1, duration: 1.5, ease: "power4.out" }, 0)
    .to(".hero-img", { rotation: 360, duration: 20, ease: "none", repeat: -1 }, 0);
}
```
- **Headline reveal:** for each `.header-row`, its character `<span>`s tween `y: 200px → 0` over **1s**, `power4.out`, `stagger { amount: 0.25, from: "start" }` (the 0.25s of stagger is distributed across that row's glyphs, left to right). Both rows are added at **position `0`**, so "The" and "Quiet Crowd" reveal simultaneously. The row `clip-path` masks each glyph until it arrives.
- **Medallion pop-in:** `.hero-img` scales `0 → 1` over **1.5s**, `power4.out`, at position `0`.
- **Medallion endless spin:** simultaneously (also position `0`) `.hero-img` tweens `rotation: 0 → 360` over **20s**, `ease:"none"` (linear), `repeat:-1` (infinite) — a slow, perpetual clockwise rotation that continues forever after the reveal settles.

### Cursor emoji confetti — `mousemove` spawner (only while `isLoading`)
Module constants and state:
```js
const mouseDistance   = 400;                 // px of travel needed to spawn
const emojiWaitTime   = 500;                 // ms a particle hangs before dropping
const emojiFallDelay  = 200;                 // ms min gap that paces sequential drops
const emojiRotations  = [90, -90];           // random initial/exit tilt
const emojiSizes      = [150, 200, 250, 300];// random chip diameter (px)
const totalEmojiVariants = 4;                // emoji-1..4
let lastMouseX = 0, lastMouseY = 0, lastEmojiTime = 0;
```

**Listener:**
```js
document.addEventListener("mousemove", (e) => {
  if (!isLoading) return;                     // preloader-phase only
  const d = Math.hypot(e.clientX - lastMouseX, e.clientY - lastMouseY);
  if (d > mouseDistance) {                     // spawn once per 400px of travel
    lastEmojiTime = createEmoji(e.clientX, e.clientY);
    lastMouseX = e.clientX;
    lastMouseY = e.clientY;
  }
});
```
Every time the cursor has travelled more than **400px** from the last spawn point, drop a new emoji at the current pointer position.

**`createEmoji(mouseX, mouseY)`:**
1. Pick a random `size` from `[150,200,250,300]` and a random `variant` in `1..4`.
2. Create `<div class="emoji">`; set inline `width`/`height` = `size`px, `backgroundImage = url(/assets/emoji-${variant}.png)`, and position it centered on the cursor: `left = mouseX - size/2`, `top = mouseY - size/2` (px). Append to `.emojis`.
3. Pick `initialRotation` randomly from `[90, -90]`.
4. Compute a pacing delay so rapid spawns don't all drop at once:
   `delayFromLast = Math.max(0, emojiFallDelay - (Date.now() - lastEmojiTime)) / 1000` (seconds).
5. `gsap.set(emoji, { scale: 0, rotation: initialRotation })` — starts invisible and tilted.
6. Two-step timeline:
   ```js
   gsap.timeline()
     .to(emoji, {                       // POP IN
       scale: 1, rotation: 0,
       duration: 0.5,
       ease: "back.out(1.75)",          // bouncy overshoot
     })
     .to(emoji, {                       // HANG, THEN DROP
       y: window.innerHeight + size,    // falls fully off the bottom
       rotation: initialRotation,       // tilts back as it falls
       duration: 0.5,
       ease: "power2.in",               // accelerating fall
       delay: emojiWaitTime / 1000 + delayFromLast,  // 0.5s + pacing
       onComplete: () => emoji.remove(),// unmount when off-screen
     });
   ```
   So each chip springs up from `scale 0` to `1` while un-tilting to `0°` (`back.out(1.75)`, 0.5s), hangs ~0.5s (plus pacing), then falls straight down past the viewport bottom (`y: innerHeight + size`) while re-tilting to its `initialRotation`, accelerating (`power2.in`), and removes itself on completion.
7. Return `Date.now()` (stored as `lastEmojiTime` for the pacing math).

### Timeline summary (approximate seconds)
| t (s) | what |
|------|------|
| 0–1  | idle; cursor movement spawns emoji confetti (pop-in `back.out`, hang, drop) |
| 1–4  | loader square spins `+=180°` twice (`power4.inOut`) |
| 4–6  | loader scales `1 → 0` (`power4.inOut`) |
| 5–6.5 | preloader clip-path wipes upward off the top (`power4.inOut`) |
| ~6   | `isLoading=false` (emojis stop); hero reveal timeline starts |
| 6–7  | headline chars slide up `y:200→0`, both rows, stagger 0.25, `power4.out` |
| 6–7.5 | center medallion scales `0 → 1` (`power4.out`) |
| 6 →∞ | medallion rotates `0 → 360` linearly, `repeat:-1` (endless) |

### Ease reference
- Preloader wipe, loader spin, loader shrink: **`power4.inOut`**.
- Headline char reveal and medallion scale-in: **`power4.out`**.
- Medallion infinite spin: **`none`** (linear).
- Emoji pop-in: **`back.out(1.75)`**; emoji fall: **`power2.in`**.

## Assets / images
All PNGs on transparent (or square) backgrounds; the emoji stickers are cover-cropped inside circular chips, everything else is `object-fit:contain`/`cover` as noted.

- **`hero.png`** — 1 **square (1:1)** graphic used as the central rotating medallion. In the reference it is an iridescent green/cyan **wireframe globe** (thin gridlines, transparent interior). Any bold, roughly-symmetrical square graphic reads well spinning at screen center.
- **`logo.png`** — 1 small **wide (~2:1)** brand lockup illustration for the centered nav logo. Reference: a flat editorial illustration of a figure in a trench coat and glasses pointing. Any compact horizontal wordmark/illustration works.
- **`menu-btn.png`** — 1 small **square** icon for the top-right menu button. Reference: a glossy 3D inflated balloon letter. Any playful square icon works.
- **`emoji-1.png … emoji-4.png`** — **4 square (1:1) sticker graphics**, a deliberately mixed set of playful 3D and flat illustrations (e.g. a chrome star, a holographic blob, flat-illustrated characters). Each is displayed cover-filling a circular chip, so any square image works; they're spawned at random.
- **`cursor.svg`** — 1 custom **circular cursor** graphic (~48px, 32×32 hotspot) replacing the default pointer site-wide.

If fewer than 4 emoji stickers are available, repeat to fill the set.

## Behavior notes
- **Autoplay once** on load; total intro ≈ 6.5s to the hero, then the medallion spins forever. No scroll, no click triggers.
- **Emoji confetti is preloader-only** and **desktop/pointer-only** — it requires `mousemove` and stops permanently once `isLoading` flips to `false` at the start of the hero reveal. No touch handling (not mobile-safe).
- `.emojis` overlay is `pointer-events:none` and `overflow:hidden` — particles never block interaction and are clipped to the viewport; each self-removes after falling off-screen.
- Keep the `will-change` hints (`clip-path` on `.preloader`, `transform` on `.loader`, `.hero-img`, and `.emoji`) — they matter for smooth clip-path and transform animation.
- No console errors expected on load; the sequence self-starts and needs no interaction to complete (interaction only adds the emoji layer).
```
