# Fullscreen Horizontal Slider — Segmented Progress Nav + Kinetic Display-Type Titles

## Goal
Build a full-screen horizontal image carousel with a signature choreography driven entirely by **clicking segments in a top progress-bar navigation**. Each slide holds one centered image. Above them sits a slim horizontal bar split into 30 thin segments; the **active segment stretches wide** (a pure-CSS `flex` grow) while the others stay hairline-thin. Clicking a different segment does three things at once, all `1.5s` long on a custom **`"hop"` CustomEase**: (1) the whole horizontal track of slides **slides sideways** to the chosen slide, (2) a full-screen **background-color overlay tweens to a new random color**, and (3) a giant two-row **display-type title** re-animates — every letter is destroyed and re-created, then **slides in from the side** (from the right when advancing, from the left when going back) with `power2.out`. It's an editorial portfolio slider: oversized serif kinetic typography floating over a color wash, with a centered image per slide.

## Tech
- Vanilla HTML / CSS / JS with ES module imports, bundled by a **Vite**-style dev server (npm project).
- **`gsap` (npm)** plus exactly one GSAP plugin: **`CustomEase`**. No ScrollTrigger, no SplitText, no smooth-scroll library, no canvas/WebGL. The whole thing is **click-driven** — there is no scroll and no rAF loop.
- Imports and registration:
```js
import gsap from "gsap";
import { CustomEase } from "gsap/CustomEase";
gsap.registerPlugin(CustomEase);
CustomEase.create("hop", "M0,0 C0.071,0.505 0.192,0.726 0.318,0.852 0.45,0.984 0.504,1 1,1");
```
- Title copy lives in a separate `data.js` (`export const titles = [...]`). All DOM building + wiring happens inside a single `document.addEventListener("DOMContentLoaded", …)` in `script.js` (`<script type="module" src="./script.js">`).

## Layout / HTML
The markup is mostly empty shells that JS fills. Class names are load-bearing. Use neutral/fictional copy — no real brand names. The demo wordmark is **"Motionprompts"**.

```html
<div class="container">
  <nav>
    <a href="#" id="logo">Motionprompts</a>
    <a href="#">Subscribe</a>
  </nav>
  <footer>
    <a href="#">Unlock Source Code with PRO</a>
    <a href="#">Link in description</a>
  </footer>

  <div class="bg-overlay"></div>     <!-- full-screen color wash -->
  <div class="slider-nav"></div>     <!-- JS injects 30 .nav-item-wrapper segments -->
  <div class="slides"></div>         <!-- JS injects 30 .slide elements in a horizontal flex row -->

  <div class="slide-title">
    <div class="slide-title-row"><!-- 7 empty .letter divs --></div>
    <div class="slide-title-row"><!-- 7 empty .letter divs --></div>
  </div>
</div>

<script type="module" src="./script.js"></script>
```

The two `.slide-title-row` divs each contain **exactly 7 empty `<div class="letter">`** in the HTML (14 letter slots total). JS injects/replaces a `<span>` inside each on every title change.

**Per-slide DOM (built in the JS loop, i = 0..29):**
```
.slide            (flex 1, one viewport wide)
  .img            (centered box, 50% × 50% of viewport)
    img           src = /c/aristidebenoist-slider/img{i+1}.jpg
```

**Per-nav-segment DOM (built in the same loop):**
```
.nav-item-wrapper   (segment 0 also gets class "active")
  .nav-item         (the thin tick inside)
```

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

**Fonts** (name these families; supply free substitutes if unavailable):
- **Body / UI**: `"PP Neue Montreal"` — a neutral grotesque sans (substitute: Inter / Neue Haas Grotesk).
- **Display** (logo + the giant title letters): `"Timmons NY 2.005"` — a tall, high-contrast condensed **serif display** face (substitute: a heavy fashion-editorial serif like a condensed Didone / GT Sectra display / Ogg). Set on `nav a#logo` and `.letter span`.

**Colors / tokens:**
- Text is black `#000`.
- `.bg-overlay` initial background: **`rgb(213, 183, 71)`** (a mustard/gold), with `filter: brightness(0.75);` and `opacity: 0.5;` — a soft muted color wash over the whole viewport. Later JS tweens its `background-color` to random hex values.

**Structure:**
- `.container`: `width:100vw; height:100vh; overflow:hidden;` — clips the oversized horizontal track and the giant letters.
- `nav`, `footer`: `position:fixed; left:0; width:100vw; padding:2.75em; display:flex; justify-content:space-between; z-index:2;` — `nav{top:0}`, `footer{bottom:0}`.
- `a`: `text-decoration:none; text-transform:uppercase; font-size:12px; font-weight:500; color:#000;`.
- `nav a#logo`: `position:relative; top:-12px;` display serif, `font-size:42px;`.
- `.bg-overlay`: `position:fixed; inset:0; width:100vw; height:100vh;` (see color tokens above).

**The segmented progress nav (the click target):**
- `.slider-nav`: `position:fixed; top:5%; left:50%; transform:translateX(-50%); width:25%; height:15px; display:flex; justify-content:space-between; z-index:10;`.
- `.nav-item-wrapper`: `flex:1; height:100%; display:flex; justify-content:center; align-items:center; transition: all 750ms cubic-bezier(0, 0.75, 0.5, 1);` — **CSS transition**, not GSAP.
- `.nav-item`: `width:1px; height:100%; border:1px solid rgba(0,0,0,0.15); transition: all 750ms cubic-bezier(0, 0.75, 0.5, 1);` — a faint hairline tick.
- `.nav-item-wrapper.active`: `flex:5;` — the active segment claims 5× the width of the others, so the bar reads like a progress indicator with one long lit cell.
- `.nav-item-wrapper.active .nav-item`: `width:50%; border:1px solid rgba(0,0,0,1);` — the active tick grows and turns solid black.

**The horizontal slide track:**
- `.slides`: `position:fixed; top:0; left:0; width:3000vw; height:100vh; display:flex;` — **3000vw** = 30 slides × 100vw laid side by side. GSAP translates this whole element on `x`.
- `.slide`: `flex:1; width:100vw; height:100vh; display:flex; justify-content:center; align-items:center;`.
- `.slide .img`: `width:50%; height:50%; opacity:0.75;` — image sits in a centered box occupying half the viewport each way.
- `img`: `width:100%; height:100%; object-fit:cover;`.

**The giant kinetic title:**
- `.slide-title`: `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:75%; height:65%; display:flex; flex-direction:column; pointer-events:none;` — floats centered over the slides, non-interactive so nav clicks pass through.
- `.slide-title-row`: `flex:1; width:100%; display:flex; gap:0;` (two rows).
- `.slide-title-row:nth-child(2)`: `position:relative; left:4em;` — the second row is nudged right for an offset editorial layout.
- `.letter`: `flex:1; height:100%; padding-left:2em; clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);` — each of the 7 cells per row is an equal-width column; the full-rectangle `clip-path` masks the span so a letter that slides in from the side is **clipped at the cell edges** (it wipes in rather than flying across).
- `.letter span`: `position:relative; display:inline-block; font-family:"Timmons NY 2.005"; font-size:280px; filter: brightness(0.25) saturate(0.75) !important;` — NOTE: this `!important` filter **overrides** the inline `filter` GSAP sets, so every letter renders as a dark, desaturated version of the current color (near-black), regardless of the random color assigned to it.

## GSAP effect (be exact)

### The "hop" CustomEase
`CustomEase.create("hop", "M0,0 C0.071,0.505 0.192,0.726 0.318,0.852 0.45,0.984 0.504,1 1,1")` — a fast-out, long-settle ease: it accelerates hard in the first ~30% then glides slowly into the end. Used for both the slide-track slide and the color tween.

### Build loop (30 items, index `i = 0..29`)
For each `i`:
1. Create `.nav-item-wrapper` (+ `.nav-item` child); segment `0` gets class `active`. Append to `.slider-nav`.
2. Create `.slide` (+ `.img` + `<img src="/c/aristidebenoist-slider/img{i+1}.jpg">`); slide `0` also gets class `active`. Append to `.slides`.
3. Attach a **click handler** to the wrapper (closure captures its own `i` via `let`).

Track two module-level values: `numberOfItems = 30` and `currentIndex = 0`.

### Click handler on a nav segment `i`
```js
if (i === currentIndex) return;                    // ignore clicks on the current segment

// active class swap (drives the CSS width transition on the bar)
document.querySelectorAll(".nav-item-wrapper").forEach(n => n.classList.remove("active"));
thisWrapper.classList.add("active");

// 1) slide the whole track horizontally
gsap.to(".slides", { x: `${-i * 100}vw`, duration: 1.5, ease: "hop" });

// 2) tween the background wash to a fresh random hex color
const newColor = getRandomColor();                 // "#" + 6 random hex chars
gsap.to(".bg-overlay", { backgroundColor: newColor, duration: 1.5, ease: "hop" });

// 3) re-animate the title (see below), then commit the index
updateTitle(i, newColor);
currentIndex = i;
```
- `getRandomColor()`: builds `"#"` + 6 characters picked at random from `0123456789ABCDEF`.
- The horizontal target is **`x: "-i*100vw"`** (e.g. clicking segment 7 → `x: "-700vw"`), so the chosen slide lands centered in the viewport.

### `updateTitle(newIndex, color)` — the kinetic letters
`titles[newIndex]` is `[rowArray0, rowArray1]`, each row a 7-element array of single characters (many are empty strings `""`, which create gaps → the scattered word layout). For **each** of the 2 `.slide-title-row`s, for **each** of its 7 `.letter` cells:
```js
const direction = newIndex > currentIndex ? 150 : -150;   // forward → enter from right; back → from left
// remove the old <span> if present
const span = document.createElement("span");
gsap.set(span, { x: direction, color: color, filter: "brightness(0.75)" });
span.textContent = title[rowIndex][letterIndex] || "";     // may be an empty string
letter.appendChild(span);
gsap.to(span, { x: 0, duration: 1, ease: "power2.out", delay: 0.125 });
```
Key points:
- Direction is decided **before** `currentIndex` is updated, so it reflects travel direction: advancing to a higher index → letters start at `x: 150` (enter from the right); going back → `x: -150` (enter from the left).
- Every letter uses the **same** `duration: 1`, `ease: "power2.out"`, `delay: 0.125` — **no stagger**; all 14 letters glide in together, clipped by their `.letter` cell so they appear to wipe in from the edge.
- The `color` set inline is the same random color used for the wash, but the CSS `filter … !important` on `.letter span` overrides the inline `filter`, so letters render near-black/desaturated regardless.

### Initial state (on load, after the build loop)
Call `updateTitle(0, getComputedStyle(bgOverlay).backgroundColor)` once. Since `newIndex (0)` is not `> currentIndex (0)`, `direction = -150`, so the very first title wipes in from the left. Slide `0` and nav segment `0` start `active`; the track sits at `x: 0`.

### Title data (`data.js`)
`export const titles` is an array of 30 entries, each `[ [7 chars], [7 chars] ]`. Empty strings are intentional gaps. Use short, neutral invented words (no real brands). Reproduce this exact set for a faithful result:
```js
export const titles = [
  [["p","","r","","i","s","m"], ["","t","","o","n","e",""]],
  [["","l","u","m","e","","n"], ["d","","r","e","a","m",""]],
  [["r","","u","s","","h",""], ["","s","l","i","","c","e"]],
  [["e","","c","h","o","","e"], ["","c","o","","d","e","6"]],
  [["t","e","","c","h","y",""], ["","m","","","a","p","l"]],
  [["","w","a","v","","e","s"], ["b","o","","x","","",""]],
  [["c","","u","b","","","e"], ["","s","i","t","","9","0"]],
  [["r","u","","s","h","","x"], ["t","","o","r","","k",""]],
  [["c","","o","d","","e",""], ["l","a","b","","","0","8"]],
  [["m","i","x","","e","","d"], ["","","a","r","","k",""]],
  [["","t","e","","s","t",""], ["b","","e","d","","5","4"]],
  [["f","o","c","u","","","s"], ["","d","o","c","k","",""]],
  [["p","","a","","c","e",""], ["s","e","t","","1","","7"]],
  [["","b","","l","a","s","t"], ["m","o","","d","","","e"]],
  [["z","o","","n","e","",""], ["g","e","3","","","n",""]],
  [["d","","r","e","a","","m"], ["s","c","a","p","0","",""]],
  [["e","l","e","v","a","n",""], ["","p","a","","t","","h"]],
  [["","s","","h","i","f","t"], ["","n","e","","","u","e"]],
  [["i","","c","o","","","n"], ["","m","e","m","o","",""]],
  [["","a","","","u","r","a"], ["w","","a","v","e","","6"]],
  [["s","t","e","l","","l","a"], ["","o","","r","b","i","t"]],
  [["v","","e","r","t","e",""], ["c","o","","r","","e",""]],
  [["i","n","f","i","","9",""], ["","","","e","t","h","o"]],
  [["","","q","u","a","n","t"], ["d","e","","c","","","k"]],
  [["","n","","","o","v","a"], ["r","","a","y","","",""]],
  [["","r","a","d","i","a","n"], ["g","l","o","","","w","0"]],
  [["c","o","s","m","i","c",""], ["p","","a","t","h","",""]],
  [["","s","o","l","a","r",""], ["d","r","i","f","","","t"]],
  [["z","e","n","","l","a","y"], ["","e","r","v","","y",""]],
  [["a","p","e","","x","",""], ["f","o","r","g","e","0","0"]],
  [["","c","r","y","s","t","a"], ["l","b","y","t","e","",""]],
];
```

## Assets / images
30 slide images at `/c/aristidebenoist-slider/img1.jpg … img30.jpg`. Each is shown in a **centered box occupying 50% × 50% of the viewport** with `object-fit: cover` at `opacity: 0.75`, so any aspect ratio works (roughly square-to-landscape framing reads best; crops are inevitable). Content is an eclectic editorial mix — extreme close-up portraits, moody studio product shots, abstract 3D renders (glossy discs, spheres, stacked panels), gadgets/devices, and interiors. Subjects need not relate; the point is variety across the 30 slides. No brand marks or logos in the images.

## Behavior notes
- **Click-only, no scroll, no rAF.** The only interactions are clicks on the 30 nav segments. Clicking the already-active segment is a no-op.
- **Responsive (`max-width: 900px`):** `.slider-nav` widens to `width:40%`; `.slide .img` grows to `width:80%; height:75%`; `.slide-title` becomes `left:47.5%; height:25%` and its second row resets to `left:0`; `.letter span` font-size drops from `280px` to `100px`.
- The bar's segment-widening is a **CSS transition** (`750ms cubic-bezier(0,0.75,0.5,1)`), intentionally slightly out of sync with the `1.5s` GSAP `"hop"` slide/color tweens.
- Colors are random each click, so no two sessions look identical; only the letter darkening (`filter … !important`) keeps the type readable over the shifting wash.
