# FameEstate Scroll Animation — Pinned Clip-Path Hero Reveal

## Goal
Build a full-screen luxury real-estate hero that is **pinned and scrubbed over seven viewport heights** while a single GSAP timeline plays a cinematic reveal, then hands off to a plain `about` section. In order, as you scroll: the full-bleed background image **zooms out** (scale 1.5 → 1); a mustard-gold panel (the "revealer") **opens from a hair-thin vertical seam at screen center** — first growing top-to-bottom, then wiping outward left-and-right to fill the screen; three full-bleed images **cascade in one after another** from a collapsed center point, each expanding via clip-path while scaling 0 → 1; a gold **outro panel with a heading scales in**, then **splits down the middle into two halves that slide apart** (left half off-screen left, right half off-screen right) to uncover the `about` section beneath. Smooth scroll via Lenis. The whole hero is one scrubbed ScrollTrigger timeline.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugin **`ScrollTrigger`**, and `lenis` (npm) for smooth scroll. No other plugins, no framework — plain Vite-style module imports:
```js
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";
```
Register once: `gsap.registerPlugin(ScrollTrigger);`. Run everything inside `document.addEventListener("DOMContentLoaded", …)`.

## Layout / HTML
Two top-level `<section>`s. Class names are load-bearing — the JS/CSS query them.

```html
<section class="hero">
  <div class="hero-bg"><img src="…" alt="" /></div>

  <div class="hero-content">
    <h1>A modern approach to luxury living and timeless spaces</h1>
  </div>

  <div class="hero-revealer"></div>

  <div class="hero-images">
    <div class="hero-img"><img src="…" alt="" /></div>
    <div class="hero-img"><img src="…" alt="" /></div>
    <div class="hero-img"><img src="…" alt="" /></div>
  </div>

  <div class="hero-outro-content">
    <h1>Thoughtfully crafted spaces designed to inspire modern living connections</h1>
  </div>
</section>

<section class="about">
  <div class="about-content">
    <h3>Designing digital experiences that feel effortless</h3>
    <p>From initial concept to final detail, every element is considered with purpose and clarity</p>
  </div>
</section>
```

Notes:
- `.hero-images` holds exactly **3** `.hero-img` wrappers (each with one `<img>`).
- `.hero-outro-content` is a single element in the markup — the JS **clones it at runtime** and turns the pair into left/right halves (see GSAP section). Both the original and the clone keep the `hero-outro-content` class.
- Use the neutral editorial copy above verbatim (uppercase real-estate/design prose). "FameEstate" is the fictional demo brand — no real client names.

## Styling
Font (Google Fonts): **DM Sans** — import the full optical-size/weight axis:
```css
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");
```

Palette (CSS variables):
- `--base-100: #f0f0e6` — page background (warm off-white/cream); also the text color on the hero panels.
- `--base-200: #997f1f` — mustard/olive-gold; the color of BOTH the `hero-revealer` and the `hero-outro-content` panel.
- `--base-300: #0f0f0f` — near-black; the `about` section text color.

Global / reset:
- `* { margin:0; padding:0; box-sizing:border-box; }`
- `body { font-family:"DM Sans", sans-serif; background-color:var(--base-100); }`
- `img { width:100%; height:100%; object-fit:cover; }`
- `h1, h3, p { text-transform:uppercase; font-weight:500; line-height:1; }`
- `h1 { font-size: clamp(3rem, 4vw, 5rem); }`
- `h3 { font-size: clamp(2rem, 3vw, 4rem); }`
- `p { font-size: 1.1rem; }`

`.hero`:
- `position:relative; width:100%; height:100svh; overflow:hidden; z-index:2;` (sits ABOVE the about section, which is `z-index:1`).

Full-cover overlays — `.hero-bg, .hero-content, .hero-revealer, .hero-images` all share:
- `position:absolute; top:0; left:0; width:100%; height:100%; will-change:transform;`

Centered/scaled layers — `.hero-img, .hero-outro-content` share:
- `position:absolute; top:50%; left:50%; width:100%; height:100%;`
- `transform: translate(-50%, -50%) scale(0);` — **both start scaled to zero** (this is the initial state GSAP scales up from).
- `will-change:transform;`

Text panels — `.hero-content, .hero-outro-content`:
- `padding:2rem; display:flex; justify-content:center; align-items:center; text-align:center; color:var(--base-100);`
- Their `h1` is constrained: `.hero-content h1, .hero-outro-content h1 { width:65%; }`.

Panel-specific:
- `.hero-outro-content { background-color: var(--base-200); }` — the gold outro panel.
- `.hero-bg { transform: scale(1.5); }` — **background starts zoomed in 1.5×** (GSAP zooms it out to 1).
- `.hero-revealer { background-color: var(--base-200); clip-path: polygon(49.5% 50%, 50.5% 50%, 50.5% 50%, 49.5% 50%); }` — **initial clip is a 1%-wide, zero-height sliver collapsed at the exact center** (all four points at `y:50%`). Effectively invisible until animated. `will-change:transform` (from the shared rule).
- `.hero-img { clip-path: polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%); will-change:clip-path; }` — **initial clip is a single point at the center** (all four polygon points at `50% 50%`), fully collapsed.

`.about`:
- `position:relative; width:100%; height:100svh; padding:2rem; background-color:var(--base-100); color:var(--base-300); z-index:1;`
- **`margin-top: 500svh;`** — CRITICAL. This 5-viewport gap is the scroll runway the pinned hero consumes (see behavior notes). Without it there is no room to scroll through the pinned timeline.
- `.about-content { width:40%; height:100%; margin:0 auto; display:flex; flex-direction:column; justify-content:space-between; text-align:center; }` (heading at top, paragraph at bottom).

## GSAP effect (the important part — be exact)

### Smooth scroll wiring (Lenis + GSAP ticker)
```js
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => { lenis.raf(time * 1000); });
gsap.ticker.lagSmoothing(0);
```
Lenis' scroll events call `ScrollTrigger.update`; Lenis is driven by GSAP's ticker; lag smoothing is off so scrub stays glued to the scroll position.

### DOM prep — clone the outro into two halves
Grab the elements, then clone the outro panel so there are two identical stacked copies, one clipped to each half:
```js
const heroSection       = document.querySelector(".hero");
const heroBackground    = document.querySelector(".hero-bg");
const heroContent       = document.querySelector(".hero-content");
const heroRevealer      = document.querySelector(".hero-revealer");
const heroImagesWrapper = document.querySelector(".hero-images");
const heroImages        = gsap.utils.toArray(".hero-img");        // length 3
const heroOutroContent  = document.querySelector(".hero-outro-content");

const heroOutroClone = heroOutroContent.cloneNode(true);
heroOutroContent.classList.add("hero-outro-left");   // original → left half
heroOutroClone.classList.add("hero-outro-right");    // clone → right half
heroOutroContent.parentNode.appendChild(heroOutroClone);

gsap.set(".hero-outro-left",  { clipPath: "polygon(0% 0%, 50% 0%, 50% 100%, 0% 100%)" });   // shows LEFT half
gsap.set(".hero-outro-right", { clipPath: "polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%)" }); // shows RIGHT half
gsap.set(heroImagesWrapper, { scale: 1 });
```
Both halves still carry the `hero-outro-content` class, so a tween targeting `.hero-outro-content` animates BOTH at once. Each is a full-size gold panel clipped to opposite halves; overlapped, they read as one panel with the heading centered.

### The scrubbed timeline (one ScrollTrigger)
```js
const heroScrollTimeline = gsap.timeline({
  scrollTrigger: {
    trigger: heroSection,
    start: "top top",
    end: () => `+=${window.innerHeight * 7}`,   // scrub across 7 viewport heights
    pin: true,
    pinSpacing: false,          // no spacer — the about section's 500svh margin is the runway
    scrub: true,
    invalidateOnRefresh: true,
  },
});
```
This is a **scrub timeline built with explicit position parameters** — every tween below is placed at an absolute time on a 0 → 1.0 timeline (the numbers are relative "seconds" that get linearly mapped to the 7vh scroll distance). **No ease is specified on any tween, so GSAP's default `power1.out` shapes each tween over its own segment.** Add the tweens in this exact order, with these exact positions and durations:

1. **Background zoom-out** — pos `0`, dur `0.5`:
   `heroBackground → { scale: 1 }` (from CSS `scale(1.5)` → `1`). The landscape slowly settles/zooms out over the first half of the timeline.

2. **Revealer seam grows vertically** — pos `0`, dur `0.2`:
   `heroRevealer → { clipPath: "polygon(49.5% 0%, 50.5% 0%, 50.5% 100%, 49.5% 100%)" }`. From the collapsed center sliver to a **1%-wide full-height vertical bar** at screen center — a thin gold seam opening top-to-bottom.

3. **Revealer wipes open horizontally** — pos `0.2`, dur `0.3`:
   `heroRevealer → { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)" }`. The thin vertical bar expands outward to a full rectangle — the gold panel wipes the screen from center to both edges.

4. **Three images cascade in** — a `forEach` over the 3 `heroImages`, each placed at `cascadeStart + index * cascadeStagger`, where:
   ```js
   const cascadeStart = 0.4;      // first image at 0.40
   const cascadeStagger = 0.04;   // then 0.44, then 0.48
   const cascadeDuration = 0.16;
   ```
   Each tween: `heroImage → { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", scale: 1, duration: 0.16 }`. From the CSS start (`clip-path` collapsed to the center point + `scale(0)`) to a full-rectangle clip at `scale(1)`. Result: each image **irises open from a point at center while scaling up**, one after the other (positions 0.40, 0.44, 0.48), each new image landing on top of the previous.

5. **Outro panel scales in** — pos `cascadeStart + heroImages.length * cascadeStagger + cascadeStagger * 0.5` = `0.4 + 0.12 + 0.02 = 0.54`, dur `0.16`:
   `".hero-outro-content" → { scale: 1 }` (both halves, from CSS `scale(0)` → `1`). The gold heading panel pops in over the last cascaded image.

6. **Hard hide the earlier layers** — pos `0.7` (a `.set`, instantaneous):
   `heroScrollTimeline.set([heroBackground, heroContent, heroRevealer, heroImagesWrapper], { autoAlpha: 0 })`. Background, hero heading, revealer, and the images wrapper all snap invisible so only the gold outro remains.

7. **Section background → transparent** — pos `0.7` (a `.set`):
   `heroScrollTimeline.set(heroSection, { backgroundColor: "transparent" })`. So that when the outro halves part, the `about` section shows through instead of the hero's own background.

8. **Left half slides off left** — pos `0.7`, dur `0.3`:
   `".hero-outro-left" → { xPercent: -150 }`. The left half rockets fully off-screen to the left.

9. **Right half slides off right** — pos `0.7`, dur `0.3`:
   `".hero-outro-right" → { xPercent: 50 }`. The right half slides right by half its width — enough to push its visible (right-half) content off the right edge.

Steps 8 & 9 run simultaneously (both at pos `0.7`), splitting the gold panel down the seam and drawing the two halves apart to unveil the `about` section. The timeline's last tweens end at pos `1.0` (total timeline length = 1.0), exactly as the scrub reaches the end of the 7vh runway.

**No SplitText, no CustomEase, no lerp/rAF interpolation, no Three.js.** The only motion is this single scrubbed, pinned timeline of clip-path / scale / xPercent tweens, all on GSAP's default `power1.out` ease.

## Assets / images
Four **full-bleed landscape photos** (all roughly 16:9, ~1456×816), each rendered at `100%` width/height with `object-fit: cover` so they fill the entire viewport (exact crop doesn't matter — cover handles it). Editorial, cinematic set. Roles:
- **`hero-bg`** — the establishing background that starts zoomed 1.5× and settles to 1. A **golden-hour landscape of misty rolling hills with scattered pine trees**, low fog drifting through the valleys, hazy backlit sun. Dominant colors: warm gold, amber, soft cream sky, muted green.
- **`hero-img` #1** — a **dramatic backlit portrait: the profile silhouette of a person against crossing red spotlight beams** forming an X on a dark stage. Dominant colors: deep black silhouette, intense red and orange glow.
- **`hero-img` #2** — a **product still-life: a plain glossy white cosmetic bottle with a black cap resting on a smooth stone, framed by dried beige gypsophila/baby's-breath flowers** against a neutral taupe backdrop. Dominant colors: soft beige, tan, warm neutral, black accent.
- **`hero-img` #3** — a **coastal landscape: rugged tan sea cliffs dropping into deep blue ocean** at soft evening light, distant headland and a pale cloudy sky. Dominant colors: warm sandstone tan, deep teal-blue water, muted sky.

Describe generically by role and form — no brands. Each `hero-img` irises open from a center point while scaling up, one after another. If you have fewer than four, repeat.

## Behavior notes
- **Desktop-first.** At `max-width: 1000px`, the constrained widths relax to full width: `.hero-content h1, .hero-outro-content h1, .about-content { width: 100%; }`. The pin + scrub effect runs at all sizes.
- **Heights use `svh`** (`100svh`, `margin-top: 500svh`) so mobile browser chrome doesn't break the full-screen layout.
- The **`about` section's `margin-top: 500svh` is the scroll runway** — because the pin uses `pinSpacing: false`, no spacer is inserted, so the document's own height (hero 1vh + 5vh margin + about 1vh ≈ the `+=7*innerHeight` end) must supply the scroll distance. Keep them in sync.
- `invalidateOnRefresh: true` + the function-based `end` recompute the 7vh distance on resize.
- Nothing autoplays; the entire sequence is scroll-scrubbed (`scrub: true`), so it plays forward as you scroll down and reverses as you scroll up. No reduced-motion handling in the original.
- Keep the `will-change` hints (`transform` on the overlay layers, `clip-path` on `.hero-img`) — they matter for smooth clip-path animation.
