# We Go Again — Click-to-Swap Project Showcase

## Goal
Build a full-viewport, three-column creative-agency showcase. The right column is a slim, internally-scrollable **vertical thumbnail gallery**; the wide middle column shows the currently featured project (big title, a paragraph of copy split into lines, credits, a large featured image); the left column holds the site nav + agency intro. Behind everything sits a **full-screen, heavily-blurred version of the featured image** as an ambient color wash. The star effect: **clicking any thumbnail runs a two-phase GSAP transition** — the current title/copy/credits lines slide **up and out** (staggered, accelerating) while the featured image **shrinks, rises and zooms**; then the details are rebuilt for the new project and the new text **rises in from below** while the new image **grows up from below and zooms back to normal** — and the blurred background **crossfades** between the two images the whole time.

## Tech
Vanilla HTML/CSS/JS with ES module imports, bundled by Vite. No framework.
- `gsap` (npm) — all motion. **No GSAP plugins, no ScrollTrigger.**
- `split-type` (npm, `SplitType`) — splits the copy paragraph into lines so each line can be masked/animated.

Import them as:
```js
import gsap from "gsap";
import SplitType from "split-type";
```

There is **no smooth-scroll library** and **no ScrollTrigger** — the only scroll is native `overflow: auto` inside the gallery column.

## Data model
All project content lives in a separate `data.js` that exports an array of ~**15** objects, one per gallery item:
```js
export const galleryItems = [
  { title: "Beyond The Summit",
    copy: "One or two sentences of editorial description...",
    director: "Alex Honnold",
    cinematographer: "Jimmy Chin" },
  // ...14 more with the same four fields
];
```
Fields: `title` (string), `copy` (a 2–3 sentence paragraph), `director` (name), `cinematographer` (name). Content is neutral placeholder film/documentary metadata — invent 15 varied entries. The number of gallery thumbnails is driven by `galleryItems.length`.

## Layout / HTML
```html
<div class="container">
  <!-- full-screen blurred backdrop -->
  <div class="blurry-prev">
    <img src="<image 1>" alt="" />
    <div class="overlay"></div>
  </div>

  <!-- LEFT column: nav + agency intro -->
  <div class="col site-info">
    <nav>
      <a href="#">Home</a>
      <a href="#">Work</a>
      <a href="#">Contact</a>
    </nav>
    <div class="header"><h1>Welcome to Studio</h1></div>
    <div class="copy">
      <p>We are a full-service creative agency delivering innovative
         design solutions for businesses around the globe.</p>
    </div>
  </div>

  <!-- MIDDLE column: featured project (item 0 at load) -->
  <div class="col project-preview">
    <div class="project-details">
      <div class="title"><h1>Beyond The Summit</h1></div>
      <div class="info"><p>...item 0 copy paragraph...</p></div>
      <div class="credits"><p>Credits</p></div>
      <div class="director"><p>Director: Alex Honnold</p></div>
      <div class="cinematographer"><p>Cinematographer: Jimmy Chin</p></div>
    </div>
    <div class="project-img"><img src="<image 1>" alt="" /></div>
  </div>

  <!-- RIGHT column: gallery (thumbnails injected by JS) -->
  <div class="gallery-wrapper">
    <div class="gallery"></div>
  </div>
</div>
<script type="module" src="./script.js"></script>
```
The middle column's `.project-details` and `.project-img` for **item 0** are hardcoded in HTML; every subsequent project is destroyed and rebuilt in JS. The gallery thumbnails are all created by JS.

"Welcome to Studio" and the agency blurb are neutral placeholders — no real brand marks.

## Styling

**Globals**
- `* { margin:0; padding:0; box-sizing:border-box; }`
- `html, body { width:100vw; height:100vh; background:#0f0f0f; font-family:"PP Neue Montreal", sans-serif; }` (use any clean neutral sans as fallback).
- `img { width:100%; height:100%; object-fit:cover; }`
- `h1 { color:#fff; font-size:36px; font-weight:500; }`
- `a, p { color:#fff; font-size:16px; font-weight:500; text-decoration:none; }`

**Container** — `position:relative; width:100%; height:100%; display:flex; overflow:hidden;` (the three columns are flex children; `overflow:hidden` clips the transitions).

**Blurred backdrop** — `.blurry-prev { position:absolute; inset:0; width:100%; height:100%; }` holds one or more `<img>` plus `.overlay { position:absolute; inset:0; backdrop-filter:blur(80px); }`. The overlay blurs the image(s) behind it into a soft ambient wash over the whole screen. (It sits at the bottom of the stacking order; the three columns render on top.)

**Left column** — `.col { position:relative; padding:1em; }`. `.site-info { flex:1; display:flex; flex-direction:column; justify-content:space-between; border-right:1px solid rgba(255,255,255,0.1); }`. `nav { display:flex; gap:1em; }`. `.header { position:absolute; top:50%; transform:translateY(-50%); }` (vertically centers the H1).

**Middle column** — `.project-preview { flex:2; }`.
- `.project-details { position:absolute; top:1em; left:1em; width:50%; }`
- `.title { margin-bottom:0.5em; }`  `.info { margin-bottom:1em; }`
- **Masks (essential):** `.title, .credits, .director, .cinematographer, .line { clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); }` — a full-box clip that acts as an overflow mask so text sliding vertically inside each block is clipped at the block's edges.
- **Initial hidden offsets (CSS):** `.title h1 { position:relative; transform:translateY(40px); will-change:transform; }` and `.info p .line span, .credits p, .director p, .cinematographer p { display:inline-block; position:relative; transform:translateY(20px); will-change:transform; }`. These push the text below its mask; JS snaps them into view at load (see effect).
- `.project-img { position:absolute; left:1em; bottom:1em; width:75%; height:50%; overflow:hidden; will-change:transform; }` with `.project-img img { will-change:transform; }`. The large featured image; `overflow:hidden` clips its inner img zoom.

**Right column (frosted gallery panel)**
- `.gallery-wrapper { z-index:2; overflow:auto; padding:0.75em; background:rgba(255,255,255,0.1); border-left:1px solid rgba(255,255,255,0.1); backdrop-filter:blur(20px); }` — a frosted, scrollable strip.
- `.gallery { width:100px; height:300vh; display:flex; flex-direction:column; gap:0.75em; }` — a **tall** stack (3× viewport) so the thumbnails scroll internally.
- `.item { position:relative; flex:1; background:#aeaeae; }` — each thumbnail is a tall sliver, its image `object-fit:cover`.
- **Thumbnail dim/undim overlay:** `.item::after { content:""; position:absolute; inset:0; background:rgba(0,0,0,0.65); transition:background-color 0.5s ease-in-out; transition-delay:0.5s; }` and `.item.active::after { background:rgba(0,0,0,0); }`. So every thumbnail is darkened except the active one, which brightens via a 0.5s CSS transition that waits 0.5s before starting.

**Responsive** — `@media (max-width:900px)`: `.container` becomes `flex-direction:column`; `.site-info { flex:0.5; border-right:none; border-bottom:1px solid rgba(255,255,255,0.1); }`; `.header { top:unset; bottom:1em; transform:none; }`; `.site-info .copy { display:none; }`; `.project-details { width:calc(100% - 1em); }`; `.project-img { width:93%; }`; `.gallery-wrapper` uses `border-top` instead of left; `.gallery { width:300vw; height:100px; flex-direction:row; }` (horizontal thumbnail strip).

## The GSAP effect (be exhaustive)

### SplitText helper
`createSplitText(element)`:
1. `const split = new SplitType(element, { types: "lines" });`
2. Clear `element.innerHTML = ""`.
3. For **each** `split.lines`: build `<div class="line"><span>lineText</span></div>` (line text = the split line's `textContent`) and append to `element`. So every visual line becomes a `.line` mask wrapping a `<span>` — the span is what animates, the `.line` clip-path masks it.

### On load (init, inside `DOMContentLoaded`)
1. `createSplitText(document.querySelector(".info p"))` — split item 0's copy into masked lines.
2. Select the animatable set `elementsToAnimate = ".title h1, .info p .line span, .credits p, .director p, .cinematographer p"` and **`gsap.set(elementsToAnimate, { y: 0 })`** — this cancels the CSS `translateY(40px/20px)` offsets, snapping item 0's text into its visible resting position (item 0 has **no** entrance animation; it just appears).
3. Build the gallery: loop `i` from `0` to `galleryItems.length-1`, create `.item` (add class `active` only when `i === 0`), append an `<img src="/c/we-go-again/img${i+1}.jpg">`, set `dataset.index = i`, attach a `click` listener → `handleItemClick(i)`, append to `.gallery`.

State variables: `activeItemIndex = 0`, `isAnimating = false`.

### On thumbnail click — `handleItemClick(index)`
Guard: `if (index === activeItemIndex || isAnimating) return;` then `isAnimating = true`.

**(A) Swap active class** — remove `active` from `gallery.children[activeItemIndex]`, add `active` to `gallery.children[index]`, set `activeItemIndex = index`. (Fires the CSS `::after` un-dim/dim: the new thumbnail brightens, the old darkens, each over 0.5s after a 0.5s delay.)

**(B) Blurred background crossfade** (`ease: "power2.inOut"`, `duration: 1`, both delayed `0.5`):
- Create a new `<img src="/c/we-go-again/img${index+1}.jpg">`, `gsap.set(newImg, { opacity:0, position:"absolute", top:0, left:0, width:"100%", height:"100%", objectFit:"cover" })`, and **insert it as the first child** of `.blurry-prev` (`insertBefore(newImg, blurryPrev.firstChild)`).
- Grab the previous backdrop image (now `.blurry-prev img:nth-child(2)`); `gsap.to(prevImg, { opacity:0, duration:1, delay:0.5, ease:"power2.inOut", onComplete: () => remove it })`.
- `gsap.to(newImg, { opacity:1, duration:1, delay:0.5, ease:"power2.inOut" })`.

**(C) Phase 1 — animate the CURRENT project OUT** (starts immediately, `duration:1`):
- **Text out:** `gsap.to(elementsToAnimate, { y:-60, duration:1, ease:"power4.in", stagger:0.05 })` — every current title h1 / info line span / credits / director / cinematographer slides **up** to `y:-60`, accelerating (`power4.in`), cascading with a 0.05s stagger; the clip-path masks swallow them.
- **Featured image out:** `gsap.to(currentProjectImg /* the .project-img wrapper */, { scale:0, bottom:"10em", duration:1, ease:"power4.in", onStart, onComplete })`:
  - `onStart` → simultaneously `gsap.to(currentProjectImg's inner <img>, { scale:2, duration:1, ease:"power4.in" })`. So the wrapper collapses `scale 1→0` and lifts `bottom 1em→10em` while the inner image **zooms in** `scale 1→2` (a push-in as the frame shrinks away upward).
  - `onComplete` → **Phase 2** (below).

**(D) Phase 2 — rebuild + animate the NEW project IN** (runs in Phase-1's `onComplete`, ~1s after click):
1. Remove the old `.project-details` and old `.project-img` from the DOM.
2. Build a fresh `.project-details` with the new item's data — same structure: `.title h1`, `.info p` (raw paragraph, not yet split), `.credits p` = "Credits", `.director p` = `Director: <name>`, `.cinematographer p` = `Cinematographer: <name>`. Build a fresh `.project-img` with `<img src="/c/we-go-again/img${index+1}.jpg">`. Append both to `.project-preview`.
3. `createSplitText(newInfoP)` — split the new copy into masked `.line > span`.
4. Select the new set (`.title h1, .info p .line span, .credits p, .director p, .cinematographer p` within the new details) and:
   - **Text in:** `gsap.fromTo(newTextEls, { y:40 }, { y:0, duration:1, ease:"power4.out", stagger:0.05 })` — all rise from `y:40` up to `y:0`, decelerating (`power4.out`), staggered 0.05s. (Note: every element animates from `y:40`, overriding the CSS 20px/40px offsets.)
   - **Featured wrapper in:** `gsap.fromTo(newProjectImg, { scale:0, bottom:"-10em" }, { scale:1, bottom:"1em", duration:1, ease:"power4.out" })` — grows `scale 0→1` and rises `bottom -10em→1em` (comes up from below the frame).
   - **Featured inner img in:** `gsap.fromTo(newProjectImg's <img>, { scale:2 }, { scale:1, duration:1, ease:"power4.out", onComplete: () => isAnimating = false })` — the inner image zooms back out `scale 2→1` to settle. Its `onComplete` releases the `isAnimating` lock.

**Timing summary:** total ≈ **2s** — Phase 1 (out) 1s + Phase 2 (in) 1s; the backdrop crossfade overlaps both (0.5s delay + 1s). OUT eases are `power4.in` (accelerate away, upward); IN eases are `power4.out` (decelerate in, from below); backdrop is `power2.inOut`. Stagger is `0.05` on both text sets. No timeline object is used — these are parallel `gsap.to`/`gsap.fromTo` tweens coordinated by the wrapper tween's `onStart`/`onComplete` callbacks.

## Assets / images
**15 images** named `img1.jpg … img15.jpg`, served from `/c/we-go-again/`. Each image plays three roles at once: (1) a **tall vertical thumbnail** in the ~100px-wide gallery (cover-cropped to a slim strip), (2) the **large featured image** (a 75%×50% landscape-ish framed box) when its item is active, and (3) the **full-screen blurred ambient background** when active.

Visually it's a curated **high-end editorial / commercial** mix — roughly half **fashion & beauty portraits** (studio-lit models against warm brown, grey or orange seamless backdrops, ~2:3 vertical) and half **product / design still-lifes** (matte cosmetic pump bottles, beverage cans, capsule containers, an amber dropper serum bottle on beige podiums under dramatic spotlights, plus a colorful mechanical keyboard, a curved metallic building, and a warm sunlit interior). Framing is mixed (portrait to square); `object-fit:cover` handles all crops. Because they are heavily blurred as backgrounds and sliver-cropped as thumbnails, **any centered, moody editorial/commercial photo set works** — keep subjects roughly centered. **No brand names or logos** on any product.

## Behavior notes
- **Interaction is click-only** on gallery thumbnails; no hover, no scroll-trigger, no autoplay. Item 0 is active at load with no entrance animation.
- The `isAnimating` lock blocks re-clicks mid-transition; clicking the already-active item is a no-op.
- The gallery column scrolls **internally** (native `overflow:auto`; the `.gallery` is 300vh / 300vw tall) to reach all 15 thumbnails.
- Responsive at `900px`: layout stacks vertically and the gallery becomes a horizontal strip; otherwise the effect is identical.
- No reduced-motion handling in the original. Fully client-side, light performance cost (no WebGL/canvas).
