# Tile Flip Board — full-screen 6×6 grid of 3D tiles, hover spin + board-wide flip reveal

## Goal

Build a full-viewport **6×6 grid of 3D flip tiles**. Each tile shows a slice of one shared poster image (the image is split across the whole grid via `background-position`), so at rest the 36 tiles reconstruct a single full-bleed poster. **Hovering a tile fires a GSAP timeline that spins it a complete 360° turn on `rotateX` while tilting on `rotateY`** (the tilt magnitude/direction depends on the tile's column, so tiles lean away from the board's vertical centreline). A **"Flip Tiles" button** flips the entire board 180° on `rotateX` with a **random-order stagger**, swapping every tile from the front poster to a second (back) poster and back again. A **fixed grid overlay** highlights the single 50×50px cell under the cursor on every `mousemove`, leaving a fading white-outline trail.

## Tech

Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) only — **no GSAP plugins, no ScrollTrigger, no smooth-scroll, no Three.js**. Everything runs after `DOMContentLoaded`. Ship one `index.html` (`<link rel="stylesheet" href="./styles.css">`, `<script type="module" src="./script.js">`), one `styles.css`, one ES-module `script.js`. Must run in a fresh Vite + npm project.

## Layout / HTML

```html
<nav>
  <a href="#">Junni</a>
  <button id="flipButton">Flip Tiles</button>
</nav>

<section class="board"></section>

<div class="blocks-container">
  <div id="blocks"></div>
</div>
```

- `.board` is **empty in markup** — the JS builds `.row` × 6, each containing `.tile` × 6.
- Each `.tile` gets two faces built by JS:
  ```html
  <div class="tile">
    <div class="tile-face tile-front"></div>
    <div class="tile-face tile-back"></div>
  </div>
  ```
- `#blocks` is **empty in markup** — the JS fills it with `.block` divs (the cursor-grid overlay).
- Nav label text is the neutral demo name (e.g. `Junni`); the button reads `Flip Tiles`. No real brand names.

## Styling

**Reset**: `* { margin:0; padding:0; box-sizing:border-box; }`. `html, body { width:100%; height:100%; }`.

**Fonts**: nav link and button use a **bold uppercase display font** (the original uses a custom face named `"MOSKO MAPPA"`; since there's no `@font-face`, just declare it with a heavy display/grotesque fallback stack, e.g. `"MOSKO MAPPA", "Anton", "Archivo Black", sans-serif`). Everything is `text-transform: uppercase`.

**Nav** — `position:absolute; top:0; left:0; width:100vw; display:flex; justify-content:space-between; align-items:center; padding:2em; z-index:10; pointer-events:none;`. Children re-enable pointer events (`pointer-events:all`).
- `nav a`: `color:#fff; text-decoration:none; text-transform:uppercase; font-size:28px;`.
- `nav button`: `border:none; outline:none; color:#fff; background-color:#000; border-radius:0.25em; padding:0.65em 1em 0.25em 1em; text-transform:uppercase; font-size:24px;`.

**Board (the 3D stage)** — `.board { width:100vw; height:100vh; padding:0.25em; display:flex; flex-direction:column; gap:0.25em; perspective:1000px; background-color:#000; position:relative; z-index:1; }`. The **`perspective:1000px` is load-bearing** — it gives the tile spins their 3D depth.

**Row** — `.row { flex:1; display:flex; gap:0.25em; }` (6 equal rows stacked; each row's 6 tiles are equal flex columns). The `0.25em` gaps + black board background read as thin black gridlines between tiles.

**Tile** — `.tile { flex:1; position:relative; transform-style:preserve-3d; }`. **`transform-style:preserve-3d` is essential** so the two faces occupy real 3D space and the back hides when facing away.

**Faces** — `.tile-face { position:absolute; width:100%; height:100%; backface-visibility:hidden; border-radius:0.5em; overflow:hidden; }`. `backface-visibility:hidden` is what makes each face vanish when rotated past 90°.
- `.tile-front { background-color:darkslategrey; }`
- `.tile-back { background-color:darkslategrey; transform:rotateX(180deg); }` — the back face is pre-rotated 180° on X so it reads correctly once the tile flips.

**Image slicing (the reconstruction trick)** — each face paints its image through a `::before`:
```css
.tile-front::before,
.tile-back::before {
  content:"";
  position:absolute;
  top:0; left:0; right:0; bottom:0;
  background-size:600% 600%;          /* 6× the tile → one image spans the whole 6×6 board */
  background-position:inherit;         /* inherits the per-tile position set inline by JS */
  clip-path:inset(0 round 0.25em);
}
.tile-front::before { background-image:url("<front poster image>"); }
.tile-back::before  { background-image:url("<back poster image>"); }
```
Both faces share `background-size:600% 600%`; the JS writes each face's `background-position` inline so the 36 tiles crop a 6×6 mosaic that reconstructs the whole poster.

**Cursor-grid overlay** — `.blocks-container { position:fixed; top:0; left:0; width:100vw; height:100vh; overflow:hidden; pointer-events:none; z-index:2; }`. `#blocks { width:105vw; height:100vh; display:flex; flex-wrap:wrap; justify-content:flex-start; align-content:flex-start; overflow:hidden; }`. `.block { width:50px; height:50px; border:0.5px solid transparent; transition:border-color 0.3s ease; }`. `.highlight { border-color:#fff; }` — the highlighted cell shows a thin white outline that fades in/out over the `border-color` transition.

## GSAP effect (be exhaustive)

### Imports & constants
```js
import gsap from "gsap";

const ROWS = 6;
const COLS = 6;
const BLOCK_SIZE = 50;
const COOLDOWN = 1000;   // ms hover throttle per tile
let isFlipped = false;   // module-level board flip state (front vs back poster)
```

### Building the board
- `createBoard()` appends 6 `.row` divs, each with 6 tiles from `createTile(row, col)`.
- `createTile(row, col)` builds the two-face markup, then computes `bgPosition = \`${col * 20}% ${row * 20}%\`` and writes it as inline `style.backgroundPosition` on **both** `.tile-front` and `.tile-back`. Columns 0–5 map to `0% 20% 40% 60% 80% 100%` horizontally; rows 0–5 the same vertically. Combined with the CSS `background-size:600% 600%`, each tile crops its unique 1/6 × 1/6 slice.

### 1. Hover spin (per tile, `mouseenter`)
Attach to every `.tile` (query after building). Keep a per-tile `lastEnterTime = 0`.
- On `mouseenter`, throttle: only proceed if `Date.now() - lastEnterTime > COOLDOWN` (1000 ms). Update `lastEnterTime`. **Re-entering the same tile within 1s does nothing** (an animating tile can't be re-triggered mid-spin).
- Compute the tilt from the tile's **index in the flat `.tile` NodeList** (`index % 6` = its column):

  | `index % 6` (column) | 0 | 1 | 2 | 3 | 4 | 5 |
  |---|---|---|---|---|---|---|
  | `tiltY` (deg) | −40 | −20 | −10 | +10 | +20 | +40 |

  Left half tilts negative, right half positive, magnitude grows toward the outer columns — the board "fans" away from its vertical centre.
- Call `animateTile(tile, tiltY)`, a **`gsap.timeline()`** with three steps:

  ```js
  gsap.timeline()
    .set(tile, { rotateX: isFlipped ? 180 : 0, rotateY: 0 })
    .to(tile, { rotateX: isFlipped ? 450 : 270, rotateY: tiltY, duration: 0.5, ease: "power2.out" })
    .to(tile, { rotateX: isFlipped ? 540 : 360, rotateY: 0,     duration: 0.5, ease: "power2.out" }, "-=0.25");
  ```

  - **`.set`** snaps the tile to its current resting face (`rotateX` = 180 if the board is flipped, else 0) and zeroes `rotateY`.
  - **Tween 1** spins ¾ of a turn (+270° on `rotateX`) and tilts out to `tiltY`, `duration:0.5`, `ease:"power2.out"`.
  - **Tween 2** completes the full turn (+360° total from the start) and returns `rotateY` to 0, `duration:0.5`, `ease:"power2.out"`, positioned **`"-=0.25"`** so it overlaps the first tween by 0.25s (the two half-second tweens run ~0.75s total).
  - Net motion: a **complete 360° `rotateX` flip that lands back on the same face** (a flourish, not a reveal), with `rotateY` going 0 → `tiltY` → 0. Which poster is visible is governed only by `isFlipped`.

### 2. Board flip (`#flipButton` click)
```js
flipButton.addEventListener("click", () => flipAllTiles(tiles));

function flipAllTiles(tiles) {
  isFlipped = !isFlipped;
  gsap.to(tiles, {
    rotateX: isFlipped ? 180 : 0,
    duration: 1,
    stagger: { amount: 0.5, from: "random" },
    ease: "power2.inOut",
  });
}
```
- Toggles `isFlipped`, then tweens **all 36 tiles' `rotateX` to an absolute 180° (or back to 0°)**, `duration:1`, `ease:"power2.inOut"`.
- **`stagger:{ amount:0.5, from:"random" }`** — the 36 flips are spread across a total of 0.5s in random order, so the board flips as a scattered wave. This is the real front↔back poster reveal (the hover spin never changes the visible face).

### 3. Cursor grid overlay (`mousemove`)
- `createBlocks()`: `numCols = Math.ceil(window.innerWidth / 50)`, `numRows = Math.ceil(window.innerHeight / 50)`, append `numCols * numRows` `.block` divs (each `data-index=i`) into `#blocks`. Store `{ numCols, numBlocks }` on `window.blockInfo`.
- `document.addEventListener("mousemove", highlightBlock)`: measure `#blocks` `getBoundingClientRect()`, `col = Math.floor((e.clientX - rect.left)/50)`, `row = Math.floor((e.clientY - rect.top)/50)`, `index = row * numCols + col`. Add `.highlight` to `blocks.children[index]`, then `setTimeout(() => block.classList.remove("highlight"), 250)`. The CSS `transition:border-color 0.3s ease` fades the white outline in and out, so the cursor drags a trail of briefly-lit grid cells.

### Timing / easing summary
| Action | property | from → to | duration | ease | stagger / position |
|---|---|---|---|---|---|
| hover — set | `rotateX` / `rotateY` | → `0 or 180` / `0` (snap) | — | — | timeline start |
| hover — tween 1 | `rotateX` / `rotateY` | +270° / 0 → `tiltY` | 0.5 | `power2.out` | — |
| hover — tween 2 | `rotateX` / `rotateY` | +360° total / `tiltY` → 0 | 0.5 | `power2.out` | `"-=0.25"` overlap |
| board flip | `rotateX` | 0 ↔ 180 (absolute) | 1 | `power2.inOut` | `{ amount:0.5, from:"random" }` |
| cell highlight | `border-color` (CSS) | transparent → `#fff` → transparent | 0.3s CSS | ease | 250ms hold |

### Init order
`init()` runs on `DOMContentLoaded`: `createBoard()` → attach tile `mouseenter` handlers + button click → `window.blockInfo = createBlocks()` → add the `mousemove` listener.

## Assets / images

**2 full-bleed landscape posters** (roughly viewport aspect, ~16:9 / 3:2), each displayed once but sliced across the entire 6×6 grid:

- **Front poster** — a dark charcoal/near-black background with a **large bold white display wordmark** centred, and a small **handwritten-script tagline** beneath it. This is the board's initial (unflipped) state. Use neutral invented text, no real brands.
- **Back poster** — an **electric-blue background** with a **bold white all-caps headline**, two lines of smaller uppercase subtext, and a small monogram/mark. This is revealed after pressing **Flip Tiles**. Neutral invented text, no real brands.

Because `background-size:600% 600%` stretches each image to 6× the tile box, any image reconstructs seamlessly across the board; pick landscape art whose composition reads well when tiled into 36 rounded cells.

## Behavior notes

- **Desktop / mouse-driven** — the whole thing is hover- and `mousemove`-based; there's no touch fallback (leave as-is).
- **Hover throttle** — each tile's 1000ms cooldown means a spin always finishes before it can be re-triggered; sweeping the cursor across the board fires a lively cascade of independent 360° spins, one per fresh tile.
- **Hover vs flip are independent** — the hover timeline reads `isFlipped` to spin around whichever face is currently showing but always returns to it; only the button changes which poster is face-up.
- **Cursor grid never blocks clicks** — `.blocks-container` is `pointer-events:none` (over the board, `z-index:2`), while the nav/button sit at `z-index:10` with pointer events re-enabled.
- No console errors; nothing hijacks scroll (the board is a fixed full-screen stage).
