# Award List Hover — direction-aware row flip + stacked corner image preview

## Goal

Build a full-page awards list where hovering a row slides a 3-panel wrapper vertically (GSAP `y` tween) so the row flips from an award-name panel to an inverted project panel — the slide direction depends on whether the cursor enters/leaves from the top or the bottom of the row. Simultaneously, a fixed preview box in the bottom-right corner stacks up that row's image, scaling it in from 0; images scale back out and are removed when the cursor leaves the list or idles.

## Tech

Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) and `lenis` (npm) for smooth scrolling. No GSAP plugins are needed (no ScrollTrigger — scroll handling is a manual listener). Everything runs inside `DOMContentLoaded`.

## Layout / HTML

```
<body>
  <section class="intro"><h1>Intro</h1></section>
  <section class="awards">
    <p>Recognition and awards</p>
    <div class="awards-list"></div>
  </section>
  <section class="outro"><h1>Outro</h1></section>
  <div class="award-preview"></div>
  <script type="module" src="./script.js"></script>
</body>
```

The rows are generated in JS from a data array of **17 award objects**, each with 4 strings: `name`, `type`, `project`, `label`. Use neutral portfolio-style copy, e.g. `{ name: "Site of the day", type: "Awwwards", project: "Open Field Audio", label: "See Live" }`, a first row like `{ name: "Independent of the year", type: "Nominee", project: "INNOVATE 2024", label: "Awwwards" }`, and several `{ name: "Developer Award", ... }` rows with varied project names (invented, no real brands).

For each award object append to `.awards-list`:

```
<div class="award">
  <div class="award-wrapper">
    <div class="award-name"><h1>{name}</h1><h1>{type}</h1></div>
    <div class="award-project"><h1>{project}</h1><h1>{label}</h1></div>
    <div class="award-name"><h1>{name}</h1><h1>{type}</h1></div>
  </div>
</div>
```

Note the wrapper holds **three stacked 80px panels** (name / project / duplicated name) = 240px total, clipped to an 80px-tall row.

## Styling

- Reset: `* { margin: 0; padding: 0; box-sizing: border-box; }`.
- `body`: background `#e3e3db`; a bold modern grotesque sans-serif (e.g. `"Saans", "Helvetica Neue", Arial, sans-serif`).
- `h1`: uppercase, `font-size: 72px`, `font-weight: 800`, `letter-spacing: -1px`, `line-height: 0.9`.
- `p`: uppercase, `1.5rem`, weight 700. `.awards p` gets `padding: 5px 20px`.
- `section`: `position: relative; width: 100vw; height: 100vh; overflow: hidden;`. `.intro` and `.outro` are flex-centered. `.awards`: `min-height: 100vh; height: max-content;`.
- `.awards-list`: `border-top: 1px solid #000`.
- `.award`: `height: 80px;` clipped with `clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%)` (a full-rect clip that hides the overflowing wrapper).
- `.award-wrapper`: `position: relative; height: 240px; will-change: transform; transform: translateY(-160px);` — **initial state shows the bottom (duplicate) name panel**.
- `.award-name`, `.award-project`: `width: 100%; height: 80px; display: flex; justify-content: space-between; align-items: center; padding: 5px 15px; cursor: pointer; border-bottom: 1px solid #000;`.
- `.award-name`: background `#e3e3db`, color `#000`. `.award-project`: inverted — background `#000`, color `#e3e3db`.
- `.award-preview`: `position: fixed; bottom: 15px; right: 15px; width: 30%; height: 30%; z-index: 2;`.
- Global `img`: `position: absolute; width: 100%; height: 100%; object-fit: cover; will-change: transform;`.

## GSAP effect (exhaustive)

Initialize Lenis smooth scroll: `new Lenis({ autoRaf: true })`.

Define three wrapper positions (px for the `y` transform):
`BOTTOM = 0` (shows top name panel), `MIDDLE = -80` (shows project panel), `TOP = -160` (shows bottom name panel — the resting state).

Keep module-level state: `lastMousePosition {x,y}`, `activeAward` (element or null), `ticking` (rAF flag), `mouseTimeout`, `isMouseMoving`. Each row also keeps its own `currentPosition`, initialized to `TOP`.

**Every tween in this component uses `duration: 0.4`, `ease: "power2.out"`, via `gsap.to`.** There are no timelines, delays or staggers.

### 1. Row `mouseenter`

- Set `activeAward = thisRow`.
- Compute `enterFromTop = e.clientY < rect.top + rect.height / 2` (rect = row's `getBoundingClientRect()`).
- If `enterFromTop` **or** `currentPosition === BOTTOM`: set `currentPosition = MIDDLE` and tween the row's `.award-wrapper` to `y: -80` (duration 0.4, power2.out). (Entering from the bottom while resting at `TOP` intentionally does nothing — the wrapper stays put.)
- Create a new `<img>` for this row: `src = ./img{index+1}.jpg` (row index 0 → `img1.jpg` … row 16 → `img17.jpg`). Style it inline: `position: absolute; top: 0; left: 0;`, initial `scale: 0` (element style), and `zIndex = Date.now()` so newer images always stack on top. Append it to `.award-preview`, then tween it with `gsap.to(img, { scale: 1, duration: 0.4, ease: "power2.out" })`. A fresh image is appended on **every** enter, even re-hovers of the same row, so images pile up.

### 2. Row `mouseleave`

- Set `activeAward = null`.
- `leavingFromTop = e.clientY < rect.top + rect.height / 2`.
- `currentPosition = leavingFromTop ? TOP : BOTTOM`, then tween the wrapper to that `y` (`-160` or `0`), 0.4s power2.out. So leaving downward rolls the project panel up out of view (name panel slides in from below), and leaving upward rolls it down.

### 3. Document `mousemove`

- Update `lastMousePosition` from `e.clientX/clientY`; set `isMouseMoving = true` and clear any pending `mouseTimeout`.
- If the cursor is inside the `.awards-list` bounding rect, arm a **2000ms `setTimeout`**: when it fires (mouse idle), set `isMouseMoving = false` and, if `.award-preview` holds more than one `<img>`, keep only the last one — every other image tweens `scale: 0` (0.4s, power2.out) and is removed with `onComplete: () => img.remove()`.
- Then call `animatePreview()`: if `lastMousePosition` is **outside** the `.awards-list` rect (any side), tween **all** images in `.award-preview` to `scale: 0` (0.4s, power2.out) and remove each `onComplete`.

### 4. Scroll handling (hover state survives scrolling)

Add a passive `document` `scroll` listener with a rAF `ticking` guard: on scroll, `requestAnimationFrame(updateAwards)` once per frame. `updateAwards()`:

1. Calls `animatePreview()` (clears the preview stack if the cursor rect-check fails after scrolling).
2. If there is an `activeAward`, re-check whether `lastMousePosition` is still inside its current bounding rect; if not, tween its wrapper to `TOP` (`-160`) when the cursor is above the row's vertical midpoint, else to `BOTTOM` (`0`) — 0.4s power2.out — and clear `activeAward`.
3. Loop all rows (skipping the active one): if `lastMousePosition` now falls inside a row's rect (the page scrolled under a stationary cursor), tween that wrapper to `MIDDLE` (`-80`), 0.4s power2.out, and mark it as `activeAward`.
4. Reset `ticking = false`.

## Assets / images

17 editorial photographs (`img1.jpg` … `img17.jpg`), one per row, loaded relative to the page. They display inside a corner box that is 30% of viewport width × 30% of viewport height, cropped with `object-fit: cover`, so any roughly landscape (≈3:2 / 16:10) imagery works — e.g. moody architecture, product or portrait shots with a coherent palette. No logos or brand marks.

## Behavior notes

- Desktop / mouse-driven only; there is no touch fallback (fine to leave as-is).
- The preview images intentionally accumulate as a stack while the user keeps moving over rows; cleanup happens only on the 2s idle timeout (keep last) or when the cursor exits the list bounds (remove all).
- `scale` on the preview images is animated via GSAP's `scale` transform (initial `0` set via the element's inline `scale` style).
- No console errors; the page scrolls normally (intro → awards → outro) with Lenis smoothing.
