# Thetalab — Hover Preview Landing

## Goal
Build a dark, full-screen studio/portfolio landing page. The left third of the screen holds a vertical list of 9 project names (pill chips). **Hovering a name crossfades a full-bleed background photo AND reveals a floating "preview card"** for that project — a portrait image that wipes open via an animated `clip-path`, plus a large title, a tags line, and a short description that each slide into place from different directions. Each project uses one of 3 rotating layout **variants** that place the card pieces in different corners and make the image wipe open from a different edge. Moving off the list crossfades everything back to a default muted background.

## Tech
Vanilla HTML/CSS/JS with ES module imports, bundled by Vite. Use **`gsap`** (npm) only — no GSAP plugins are needed (the `clip-path` polygon is tweened by GSAP's built-in CSSPlugin because both keyframes share the same 4-point count). No smooth-scroll / Lenis (the page never scrolls). No canvas, no WebGL.

```
import gsap from "gsap";
```

## Layout / HTML
Everything lives inside one fixed, `overflow: hidden`, 100vw×100vh black container. Static markup:

- `.container` (fixed, `background:#000`, overflow hidden)
  - `nav` — three equal flex columns (`nav > div { flex:1 }`):
    - `.menu-btn` → `<p>Menu</p>` (a frosted pill, left aligned)
    - `.logo` → `<a href="#">Thetalab</a>` (centered)
    - `.local-time` → `<p>ON 11:34AM</p>` (right aligned)
  - `footer` — two items, space-between: `<p>Watch Showreel</p>` and `<p>Collection 2024</p>`
  - `.items` — the left-third project list, containing 9 `.item` blocks, each `<div class="item"><p>NAME</p></div>` with names, in order:
    `Ra`, `Uptask`, `Genix`, `Unor`, `Maxim Stark`, `Pitcher`, `MindSpace`, `The Athletix`, `All Work`
  - `.preview-bg` — the background image layer; starts with a single `<img>` pointing at the default background.

The **preview cards are built in JS** (not in the static HTML) from a data array, and appended into `.container`. Each generated card is:

```html
<div class="preview variant-N preview-{i}">
  <div class="preview-img"><img src="{thumb}" alt=""/></div>
  <div class="preview-title"><h1>{title}</h1></div>
  <div class="preview-tags"><p>{tags}</p></div>
  <div class="preview-description"><p>{description}</p></div>
</div>
```

Card data (index → title / tags / description / variant / images). Variants cycle 1,2,3,1,2,3,1,2,3:

| # | title | tags | description | variant |
|---|-------|------|-------------|---------|
| 1 | Ra | Futuristic Fashion, Minimal Design | Exploring the intersection of minimalism and future fashion trends in web design. | variant-1 |
| 2 | Uptask | Fashion Innovation, Graphic Simplicity | Innovative fashion-forward web design with a core focus on simplicity and elegance. | variant-2 |
| 3 | Genix | Eco-Fashion, Sustainable Design | Sustainable fashion meets modern web aesthetics, highlighting eco-friendly apparel innovations. | variant-3 |
| 4 | Unor | Digital Fashion, UI/UX | Digital-first fashion branding that merges cutting-edge UI/UX principles with style. | variant-1 |
| 5 | Maxim Stark | Futuristic Wearables, Web Graphics | Futuristic wearables and gear presented in a sleek, graphically rich web interface. | variant-2 |
| 6 | Pitcher | Innovative Textiles, Web Presentation | Web presentation of innovative textiles that redefine the boundaries of modern fashion. | variant-3 |
| 7 | MindSpace | Augmented Reality, Fashion Design | Augmented reality in fashion design, creating immersive web experiences for users. | variant-1 |
| 8 | The Athletix | Virtual Fashion, Graphic Design | Virtual fashion collections displayed through stunning graphic design and web aesthetics. | variant-2 |
| 9 | All Work | Minimalist Aesthetics, Fashion Forward | Fashion forward web design encapsulating the essence of minimalist aesthetics and innovation. | variant-3 |

Item `i` (0-based) maps to card `preview-(i+1)`, background image `bg-(i+1)`, and thumbnail `main-(i+1)`.

## Styling
- Base: black `#000`, all text white `#fff`. Font family a neutral grotesque sans (design uses "Basis Grotesque Pro"; fall back to Inter / Helvetica Neue / system sans).
- `img { width:100%; height:100%; object-fit:cover; }` globally.
- `nav`: fixed, full width, `padding:2em`, flex align-center, `z-index:2`. `nav p`, `footer p` → `font-size:12px`.
- `.menu-btn p` and `.item p` share a frosted-pill look: `background:rgba(255,255,255,0.1)`, `border-radius:40px`, `backdrop-filter:blur(20px)`, `padding:6px 12px`, `width:max-content`.
- `.logo a`: `font-size:20px; font-weight:500; text-decoration:none`.
- `footer`: fixed bottom, full width, `padding:2em`, flex `space-between`, `z-index:2`.
- `.items`: fixed, `width:30%`, `height:100vh`, `padding:0 2em`, flex column, `justify-content:center`, `z-index:2`. `.item`: `width:max-content; padding:0.25em 0; cursor:pointer`. `.item p` also has `font-weight:lighter; border:1px solid rgba(255,255,255,0.1); transition:0.3s`. On `.item:hover p` → `background:none; border:1px solid rgba(255,255,255,0.25)` (CSS-only pill hover state, separate from the GSAP effect).
- `.preview-bg`: `position:absolute; width:100%; height:100%; opacity:0.35` — this permanent 35% opacity is what mutes ALL background photos so overlaid text stays readable.
- `.preview`: `position:absolute; width:100vw; height:100vh; color:#fff; z-index:1` (sits above `.preview-bg` at z-index 0, below nav/footer/items at z-index 2). All cards are stacked full-screen; only their inner pieces are positioned/animated.
- `.preview-title h1`: `font-weight:lighter; font-size:80px`. `.preview-tags p`, `.preview-description p`: `font-size:14px; line-height:120%`.

### Variant positioning + collapsed clip-paths (this defines the wipe direction)
Each variant absolutely positions `.preview-img` (fixed `300px × 400px`, `margin:2em`), `.preview-title`, `.preview-tags`, `.preview-description` (`width:250px`) differently, and gives `.preview-img` a **collapsed** starting `clip-path`:

- **variant-1** (image wipes UP from the bottom edge):
  - `.preview-img`: `bottom:0; right:0; clip-path: polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)`
  - `.preview-title`: `top:35%; left:25%` · `.preview-tags`: `bottom:25%; right:40%` · `.preview-description`: `right:25%; top:25%`
- **variant-2** (image wipes LEFT from the right edge):
  - `.preview-img`: `top:10%; right:5%; clip-path: polygon(100% 0, 100% 0, 100% 100%, 100% 100%)`
  - `.preview-title`: `bottom:30%; left:50%` · `.preview-tags`: `top:25%; left:25%` · `.preview-description`: `right:10%; bottom:15%`
- **variant-3** (image wipes RIGHT from the left edge):
  - `.preview-img`: `bottom:10%; left:15%; clip-path: polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)`
  - `.preview-title`: `bottom:40%; right:15%` · `.preview-tags`: `bottom:20%; right:30%` · `.preview-description`: `left:20%; top:15%`

## GSAP effect (exhaustive)

### Lookup tables (JS)
```
defaultClipPaths = {
  "variant-1": "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)",  // collapsed to bottom line
  "variant-2": "polygon(100% 0, 100% 0, 100% 100%, 100% 100%)",     // collapsed to right line
  "variant-3": "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)"            // collapsed to left line
}
// per-variant hidden offsets for the three text blocks (px), all opacity:0
variantTransforms = {
  "variant-1": { title:{x:75,opacity:0},  tags:{y:-75,opacity:0}, description:{x:-75,opacity:0} },
  "variant-2": { title:{x:-75,opacity:0}, tags:{y:-75,opacity:0}, description:{y:75,opacity:0}  },
  "variant-3": { title:{x:75,opacity:0},  tags:{y:75,opacity:0},  description:{x:75,opacity:0}  }
}
```
The revealed (full) image clip-path is always `polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)` (full rectangle).

### Initial state (on DOMContentLoaded, right after each card is created & appended)
For every card, read its variant and `gsap.set()` its `.preview-title`, `.preview-tags`, `.preview-description` to the corresponding `variantTransforms` offset + `opacity:0`. The `.preview-img` starts hidden purely from its CSS collapsed `clip-path` (no JS needed). Cards themselves keep `opacity:1` by default but read as empty because the image is clipped to a line and the text is at opacity 0. Track `activePreview` (starts as none) and a boolean `isMouseOverItem`.

### Background crossfade — `changeBg(src)`
Create a fresh `<img>`, absolutely fill it (`top/left:0; width/height:100%; object-fit:cover`), start at `opacity:0`, append into `.preview-bg`, then:
- `gsap.to(newImg, { opacity: 1, duration: 0.5 })` (default ease `power1.out`).
- If `.preview-bg` now holds more than one child, fade the OLD (first) child: `gsap.to(oldImg, { opacity: 0, duration: 0.5, onComplete: () => remove it })`.

This yields a 0.5s dissolve between backgrounds (always seen through the layer's fixed 0.35 opacity).

### On `item.mouseenter` (index `i`)
Fire these simultaneously (no timeline object; independent tweens, no stagger, no delay unless noted):
1. `isMouseOverItem = true`.
2. `changeBg("bg-" + (i+1) + ".jpg")` → the 0.5s background dissolve above.
3. Resolve `newActivePreview = .preview-(i+1)`.
4. **If** a *different* `activePreview` was already showing, retract it first:
   - `gsap.to(prevImg, { clipPath: itsDefaultCollapsedClipPath, duration: 0.75, ease: "power3.out" })` — its image wipes closed toward its own edge.
   - `gsap.to(prevPreview, { opacity: 0, duration: 0.3, delay: 0.2 })`.
   - Instantly `gsap.set()` the previous card's title/tags/description back to their hidden `variantTransforms` offsets.
5. `gsap.to(newActivePreview, { opacity: 1, duration: 0.1 })`; set `activePreview = newActivePreview`.
6. Slide the three text blocks in — for each of `.preview-title`, `.preview-tags`, `.preview-description`:
   `gsap.to(el, { x: 0, y: 0, opacity: 1, duration: 0.5 })` (default ease `power1.out`). They travel from their variant offsets (±75px x or y) to rest, fading 0→1, all at once (0.5s).
7. Open the image: `gsap.to(activeImg, { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", duration: 1, ease: "power3.out" })` — a 1s wipe from the collapsed edge line to the full rectangle. Direction per variant: v1 up, v2 leftward, v3 rightward.

Net timing per hover: bg dissolve 0.5s, text in 0.5s, image wipe 1s — all starting together; card opacity snaps in over 0.1s.

### On `item.mouseleave`
1. `isMouseOverItem = false`.
2. Immediately `gsap.set()` the current `activePreview`'s title/tags/description back to their hidden `variantTransforms` offsets (text snaps out with no tween).
3. `setTimeout(…, 10)`; inside, **only if** `isMouseOverItem` is still false (i.e. the cursor didn't jump straight to another item):
   - `changeBg("default-bg.jpg")` → dissolve back to the muted default background.
   - `gsap.to(activePreview, { opacity: 0, duration: 0.1 })` to hide the card.
   - `gsap.to(activeImg, { clipPath: itsDefaultCollapsedClipPath, duration: 1, ease: "power3.out" })` — image wipes closed over 1s.
   - Clear `activePreview` so the next hover starts clean.

   (Moving directly from one item to another skips this reset because the new `mouseenter` sets `isMouseOverItem=true` before the 10ms timeout runs; the retract logic in step 4 of `mouseenter` handles the handoff instead.)

## Assets / images
A cohesive, muted, editorial pool of interior-design / architecture / soft still-life photography (warm neutrals, moody light):
- **9 portrait preview thumbnails**, aspect ratio **3:4** (rendered at 300×400) — one per project card (`main-1 … main-9`).
- **9 full-bleed landscape backgrounds** (~16:9, cover) — one per project (`bg-1 … bg-9`); always shown through the 0.35 opacity layer.
- **1 default full-bleed background** (`default-bg`) — a moody architectural nook, shown before/after hovering.
No brand logos or client marks. Thumbnails and backgrounds can share the same visual family.

## Behavior notes
- **Desktop / hover only.** The whole experience is pointer-driven; there is no touch fallback and the page never scrolls (single fixed viewport).
- No `prefers-reduced-motion` branch in the original.
- Nav/footer/list sit at `z-index:2` above the cards (`z-index:1`) and background (`z-index:0`), so they stay legible during every transition.
