# Accordion Frames — Spotlight Row

## Goal
Build a full-viewport, dark hero section containing a single horizontal **accordion row of tall, thin image slivers**. All panels sit collapsed to a 20px-wide sliver by default; hovering one panel (tapping on mobile) makes it smoothly **expand to a wide 400px "frame"** while every other panel simultaneously **collapses back to 20px**, all sliding to make room via a slow, decelerating ease. A crisp **white focus frame** with two thin vertical crosshair lines running the full height of the viewport glides along the row to sit exactly over the currently focused panel. The star effect is this synchronized expand/collapse "accordion" motion plus the tracking frame.

## Tech
Vanilla HTML/CSS/JS. **No GSAP, no npm animation libraries, no smooth-scroll** — the entire motion is a **CSS `transition`** on `left`/`width` driven by a small vanilla-JS layout engine, plus a `ResizeObserver`. Ship it as one `index.html`, one `styles.css`, one ES-module `script.js` (`<script type="module">`). It must run in a fresh Vite project with zero dependencies.

## Layout / HTML
Minimal semantic skeleton — the JS injects the panels at runtime:

```html
<main>
  <section class="spotlight">
    <div class="spotlight-track">
      <div class="spotlight-panels">
        <div class="spotlight-focus-indicator"></div>
      </div>
    </div>
  </section>
</main>
```

- `.spotlight` — the full-viewport dark stage.
- `.spotlight-track` — a centered, width-constrained band (this is the element observed by the `ResizeObserver`; its measured content width drives all the math).
- `.spotlight-panels` — the relative-positioned container the JS fills with absolutely-positioned panels.
- `.spotlight-focus-indicator` — a single pre-existing white frame element that the JS positions over the focused panel.

Each panel the JS creates:
```html
<div class="spotlight-panel">
  <img src="<panel image>" />
</div>
```

## Styling

**Palette / dark stage**
- `.spotlight`: `position: relative; width: 100%; height: 100svh; background-color: #0f0f0f; overflow: hidden;` (the `overflow: hidden` is essential — panels and crosshairs must be clipped to the stage).

**Track**
- `.spotlight-track`: `position: absolute; top: 50%; left: 50%; width: 90%; max-width: 1400px; transform: translate(-50%, -50%);` — a centered band.

**Panels container**
- `.spotlight-panels`: `position: relative; width: 100%; height: 400px;` (drops to `260px` at `max-width: 1000px`).

**Panel**
- `.spotlight-panel`: `position: absolute; top: 0; height: 100%; overflow: hidden; cursor: pointer; will-change: left, width;`
- **The motion lives here:** `transition: all 1s cubic-bezier(0.075, 0.82, 0.165, 1);` — a slow (1 second) ease-out ("easeOutCirc"-style) curve. `left` and `width` are set inline by the JS in pixels; the CSS transition interpolates every change.

**Panel image**
- `.spotlight-panel img`: `position: absolute; left: 50%; transform: translateX(-50%); width: 400px; height: 100%; object-fit: cover; pointer-events: none; user-select: none;`
- Trick: the `<img>` is a **fixed 400px wide**, horizontally centered inside its panel. When the panel is a 20px sliver, the image is cropped by the panel's `overflow: hidden` to a 20px-wide vertical slice taken from the image's horizontal center. When the panel expands to 400px the full image is revealed — so the sliver is a live crop of the eventual full frame, and there is no image resize during the transition, only the panel window widening.
- On mobile (`max-width: 1000px`) the image width drops to `200px` and the expanded panel width to `100px`.

**Focus indicator (the white tracking frame + crosshairs)**
- `.spotlight-focus-indicator`: `position: absolute; top: 0; height: 100%; border: 3px solid #fff; pointer-events: none; z-index: 100; will-change: left, width; transition: all 1s cubic-bezier(0.075, 0.82, 0.165, 1);` — **the exact same transition as the panels**, so the frame glides in perfect sync with the accordion.
- Two full-viewport-height vertical crosshair lines via pseudo-elements, both `content: ""; position: absolute; left: 50%; transform: translateX(-50%); width: 3px; background: #fff;`:
  - `::before` → `bottom: 100%; height: 100svh;` (line shooting up from the frame's top edge to the top of the viewport).
  - `::after` → `top: 100%; height: 100svh;` (line shooting down from the frame's bottom edge to the bottom of the viewport).
- Net look: a bright white rectangle hugging the focused frame, with thin white vertical guide lines extending above and below it across the whole screen.

## The effect (be exhaustive — this replaces the GSAP section)

There is **no GSAP timeline**. The animation is achieved entirely by (a) CSS `transition: all 1s cubic-bezier(0.075, 0.82, 0.165, 1)` on the panels and the focus indicator, and (b) a JS function that, on every focus change or resize, recomputes each panel's exact pixel `left` and `width` and writes them inline. The browser then tweens from old to new values over 1s with that ease. Reproduce the math precisely:

**Constants**
```
PANEL_WIDTH_COLLAPSED       = 20    // px sliver width
PANEL_WIDTH_EXPANDED        = 400   // px focused frame width (desktop)
PANEL_WIDTH_EXPANDED_MOBILE = 100   // px focused frame width (mobile)
PANEL_GAP                   = 5     // px gap between panels
PANEL_COUNT_DESKTOP         = 20    // panels on desktop
PANEL_COUNT_MOBILE          = 10    // panels on mobile
BREAKPOINT_MOBILE           = 1000  // px; window.innerWidth < this => mobile
```

**State**: `trackWidth` (measured), `isMobile` (boolean), `focusedPanel` (index, default `0`), `panels` (array of elements).

**Per-panel position math** — `getPanelPosition(panelIndex)`:
1. `panelCount` = mobile ? 10 : 20. `expandedWidth` = mobile ? 100 : 400.
2. `totalTrackWidth = (panelCount - 1) * (PANEL_WIDTH_COLLAPSED + PANEL_GAP) + expandedWidth` — total width of the row (every panel is a 20px collapsed sliver except one 400px expanded one, with 5px gaps).
3. `offsetToCenter = (trackWidth - totalTrackWidth) / 2` — horizontal offset that centers the whole row inside the track. This is the starting `left`.
4. Walk from index 0 up to `panelIndex`, adding each preceding panel's width plus `PANEL_GAP`: a preceding panel contributes `expandedWidth` if it is the focused one, else `PANEL_WIDTH_COLLAPSED` (20). Accumulate into `left`.
5. `width` = `panelIndex === focusedPanel ? expandedWidth : PANEL_WIDTH_COLLAPSED`.
6. Return `{ left, width }`.

**Apply** — `applyPositions()`: loop all panels, compute `{left,width}`, set `panel.style.left = left+"px"` and `panel.style.width = width+"px"`. Then compute the focused panel's `{left,width}` and set the **focus indicator's** `left`/`width` to the same, so the white frame lands exactly on the focused panel. Because both the panels and the indicator carry the 1s cubic-bezier transition, changing these inline values makes everything glide together.

**Focus** — `focusPanel(index)`: set `focusedPanel = index`, then `applyPositions()`. This one call causes the newly-focused panel to widen 20→400, its neighbors to shift, the previously-focused panel to shrink 400→20, and the frame to slide — all in a single synchronized 1s ease.

**Build** — `buildPanels()`: remove any existing panels, reset `focusedPanel = 0`, then for `i` in `0..panelCount`: create `.spotlight-panel`, append an `<img>` whose `src` is the i-th panel image, and attach the interaction:
- desktop → `mouseenter` calls `focusPanel(i)`
- mobile → `click` calls `focusPanel(i)`

Append each panel to `.spotlight-panels`, push to `panels`, then `applyPositions()`. (Panel index 0 is focused/expanded on first paint.)

**Trigger & interaction model**
- Desktop: **hover** — moving the mouse onto any panel expands it; sweeping the mouse across the row makes the frames "accordion" open one after another following the cursor, the white frame chasing along.
- Mobile: **tap/click** to focus a panel.
- No scroll, no click-to-toggle, no auto-play. Idle state = panel 0 expanded.

**ResizeObserver** — observe `.spotlight-track`. On every callback: read `entry.contentRect.width` into `trackWidth`; compute `nextIsMobile = window.innerWidth < 1000`. If the mobile state flipped, set `isMobile` and **rebuild** the panels (count changes 20↔10). Otherwise just `applyPositions()` to re-center for the new width. Kick everything off by calling `buildPanels()` once, then `observer.observe(track)`. Run all of this inside a `DOMContentLoaded` listener.

**Easing note**: the single most important motion detail is the ease — `cubic-bezier(0.075, 0.82, 0.165, 1)` over `1s`. It starts fast and decelerates hard into a long slow settle, giving the panels a weighty, glidey "drawer" feel. Do not substitute a linear or default ease.

## Assets / images
20 **square (1:1) grainy, moody editorial fashion-film photographs**, all filling the same role: interchangeable panel fills, one per accordion sliver. There is no single subject type — the set is a curated mix that shares a soft, filmic, high-art mood rather than a literal theme. Two visual families run through it:
- **Monochrome portraits** — tight black-and-white studio-style headshots against flat light-grey grounds: a woman's sharp profile with pearl-drop earrings in a black blazer; a bald young face carved by hard directional shadow; a soft, near-overexposed face seen through a veil of translucent white gauze. High grain, deep blacks, luminous pale skin.
- **Ethereal desaturated-color scenes** — pale, almost-albino figures in white dresses inside dreamlike environments: a white-gowned figure flanked by two black swans on a misty green pond with water lilies; a white-clad figure seated in a decayed flower-filled interior with a gilt mirror; a red-blindfolded bald figure under ivy-wrapped gothic stone arches; a platinum-haired woman crowned with clear-crystal butterflies in a sunlit rose garden. Only one image (the opening panel) is a genuine **motion-blurred** figure — a runner in a loose grey jacket and cream trousers smeared across a muted grey-blue backdrop.

Dominant colors across the set: creamy whites and pale skin, deep blacks and charcoal greys, soft muted greens and misty pastels, with the occasional saturated accent (a red blindfold). Overall low-saturation, high-grain, cinematic and quiet — not clean commercial studio work.

Each image is displayed at a fixed 400px render width (200px on mobile) and cropped to a 20px vertical sliver when collapsed, so **the horizontal center of each image is what shows in the sliver** — the central subject (a face, a standing figure) generally sits mid-frame and is what reads in the collapsed sliver. The native files are square, and since the desktop panel is 400px tall the effective render is ~400×400; `object-fit: cover` fills the panel so any square or tall crop works. Only the first 10 are needed on mobile. No logos, no brand marks — neutral, editorial-artistic film photography. File paths follow a simple indexed pattern (e.g. `spotlight-1.jpg` … `spotlight-20.jpg`).

## Behavior notes
- Fully responsive via the single `1000px` breakpoint: 20 panels / 400px expanded / 400px image on desktop, 10 panels / 100px expanded / 200px image on mobile; interaction switches hover↔tap accordingly.
- The layout is recomputed in pixels on every resize, keeping the row centered inside the `max-width: 1400px`, 90%-wide track.
- No infinite loops or timers; motion only occurs in response to hover/tap/resize.
- The full-height crosshair lines rely on the stage's `overflow: hidden` to clip cleanly at the viewport edges; keep the `100svh` stage and `svh` units for correct mobile viewport height.
