# Direction-Aware Grid Hover Highlight

## Goal
Build a full-viewport, dark "tech-menu" page with a two-row grid of labeled cells (skill/technology tags like "( html )", "( gsap )"…). The star effect: a single solid **highlight block glides beneath whichever cell the cursor is over**, smoothly tweening its position, its width/height, and its background color to snap exactly onto the hovered cell's bounds. Because the block always animates **from the previously hovered cell toward the new one**, it appears to slide in from whatever direction the cursor entered — a "direction-aware" hover. Each cell owns its own accent color, so the block also cross-fades through a palette as you sweep across the grid.

## Tech
Vanilla HTML/CSS/JS, shipped as `index.html` + `styles.css` + an ES-module `script.js` (`<script type="module" src="./script.js">`). **No GSAP, no npm dependencies, no smooth scroll.** The entire animation is a **CSS `transition`** on the highlight element; JS only writes inline `transform` / `width` / `height` / `background-color` values on `mousemove`, using `document.elementFromPoint` hit-testing. Must run in a fresh Vite project with zero installs.

## Layout / HTML
```html
<nav>
  <p>Motionprompts</p>
  <p>/ Experiment 448</p>
</nav>

<div class="container">
  <div class="grid">
    <div class="grid-row">
      <div class="grid-item"><p>( html )</p></div>
      <div class="grid-item"><p>( css )</p></div>
      <div class="grid-item"><p>( javascript )</p></div>
    </div>
    <div class="grid-row">
      <div class="grid-item"><p>( gsap )</p></div>
      <div class="grid-item"><p>( scrolltrigger )</p></div>
      <div class="grid-item"><p>( react )</p></div>
      <div class="grid-item"><p>( next.js )</p></div>
      <div class="grid-item"><p>( three.js )</p></div>
    </div>
  </div>
  <div class="highlight"></div>
</div>

<footer>
  <p>Unlock Source Code with PRO</p>
  <p>Link in description</p>
</footer>
```
Key points:
- Exactly **8 `.grid-item` cells**: 3 in the first row, 5 in the second. Each contains one `<p>` label wrapped in parentheses with inner spaces, e.g. `( html )`.
- `.highlight` is a **sibling of `.grid`**, placed directly inside `.container` (it is positioned against the container, not the grid).
- `nav` and `footer` are fixed chrome bars; the second nav `<p>` and both footer `<p>`s are dimmed.

## Styling
- Global reset: `* { margin: 0; padding: 0; box-sizing: border-box; }`.
- `body`: `font-family: "Akkurat Mono", sans-serif;` (a monospace grotesk — any mono with a sans-serif fallback reads right), `background-color: #1a1a1a`.
- All `p`: `text-transform: uppercase; color: #fff; font-size: 13px; font-weight: 500;` (labels render as "( HTML )" etc.).
- `nav, footer`: `position: fixed; width: 100vw; padding: 1em; display: flex; justify-content: space-between; align-items: center; background-color: #1a1a1a; z-index: 10;`. Nav pinned `top: 0` with `border-bottom: 1px solid rgba(255,255,255,0.2)`; footer pinned `bottom: 0` with the same `border-top`. `nav p:not(:first-child)` and `footer p` get `opacity: 0.3`.
- `.container`: `position: relative; width: 100%; height: 100svh; display: flex; align-items: center; justify-content: center;` — full-viewport stage that centers the grid.
- `.grid`: `position: relative; margin: 0 auto; width: 90%; height: 60%; display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.2);`.
- `.grid-row, .grid-item`: `flex: 1; display: flex; justify-content: center; align-items: center; height: 100%;` — both rows split the grid height 50/50; cells split each row evenly (3 wide cells on top, 5 narrower below).
- Hairline dividers, all `1px solid rgba(255,255,255,0.2)`: `.grid-row:nth-child(1) { border-bottom: … }` and `.grid-item:not(:last-child) { border-right: … }`.
- `.grid-item p`: `position: relative; z-index: 2;` — **essential**: the label paints above the highlight block, so text stays readable on top of the colored fill.
- `.highlight`: 
  ```css
  position: absolute;
  top: 0;
  left: 0;
  background: white;
  pointer-events: none;   /* essential: must never be hit by elementFromPoint */
  transition: transform 0.25s ease, width 0.25s ease, height 0.25s ease,
    background-color 0.25s ease;
  opacity: 1;
  ```
  It anchors at the container's top-left; JS drives it purely via inline `transform: translate(x, y)` + `width` + `height` + `background-color`.

## The effect (exhaustive — this replaces the GSAP section)
There is **no GSAP and no JS tweening loop**. The motion engine is the CSS `transition: transform 0.25s ease, width 0.25s ease, height 0.25s ease, background-color 0.25s ease` on `.highlight`; the JS just rewrites the target inline values and the browser tweens old→new over **0.25s with the default `ease` curve**. Reproduce the logic exactly:

Everything runs inside a `DOMContentLoaded` listener.

**1. Per-cell accent colors.** Define this 8-color palette, in this order:
```js
const highlightColors = [
  "#E24E1B", // burnt orange
  "#4381C1", // steel blue
  "#F79824", // amber
  "#04A777", // emerald
  "#5B8C5A", // sage green
  "#2176FF", // vivid blue
  "#818D92", // slate grey
  "#22AAA1", // teal
];
```
Loop all `.grid-item`s and stamp `item.dataset.color = highlightColors[index % highlightColors.length]` — with 8 cells, each gets a unique color in DOM order (row 1 left→right, then row 2 left→right).

**2. `moveToElement(element)` — snap the highlight to a cell.** If `element` exists:
- `rect = element.getBoundingClientRect()`, `containerRect = container.getBoundingClientRect()`.
- Set inline styles on the highlight:
  - `transform = translate(${rect.left - containerRect.left}px, ${rect.top - containerRect.top}px)` — the cell's position **relative to the container**.
  - `width = rect.width + "px"`, `height = rect.height + "px"` — the cell's exact size (cells in the 3-column row are wider than in the 5-column row, so the block visibly stretches/squeezes when crossing rows).
  - `backgroundColor = element.dataset.color`.

The CSS transition interpolates all four properties simultaneously, so the block **slides, resizes, and recolors in one 0.25s glide** from wherever it was to the new cell. This is what makes it feel direction-aware: enter a cell from the left and the block slides in from the left cell; drop down a row and it slides down while widening/narrowing.

**3. `moveHighlight(e)` — mousemove hit-testing.** Attached as a `mousemove` listener **on `.container`** (one listener, not per-cell):
- `hoveredElement = document.elementFromPoint(e.clientX, e.clientY)`.
- If `hoveredElement` has class `grid-item` → `moveToElement(hoveredElement)`.
- Else if `hoveredElement.parentElement` has class `grid-item` (the cursor is over the `<p>` label) → `moveToElement(hoveredElement.parentElement)`.
- Otherwise do nothing — over borders, gaps, or outside the grid the highlight simply **stays parked on the last cell** (it never hides or fades).

This is why `pointer-events: none` on `.highlight` is mandatory: without it, `elementFromPoint` would return the highlight itself and the effect would freeze.

**4. Initial state.** Immediately call `moveToElement(firstGridItem)` on load so the highlight starts parked on the first cell ("( html )") with its color `#E24E1B`. (Because the element starts at `top:0; left:0` of the container with no size, the very first paint tweens it into place — acceptable and matches the original.)

No ScrollTrigger, no timelines, no stagger, no rAF loop — trigger is `mousemove` only, plus the one on-load placement.

## Assets / images
None. The page is pure typography, hairline borders, and flat color fills.

## Behavior notes
- **Desktop-only effect.** At `max-width: 900px`: `.highlight { display: none; }` (the effect is disabled), `.container` becomes `height: 100%; min-height: 100svh; padding: 25vh 0;`, `.grid` becomes `height: max-content`, each `.grid-row` switches to `flex-direction: column`, cells become full-width with `padding: 60px 0`, and the cell dividers swap from `border-right` to `border-bottom` (`:not(:last-child)`). Also `footer p:nth-child(2) { text-align: right; }`.
- The highlight always fully covers exactly one cell — its geometry is measured live via `getBoundingClientRect`, so it stays correct at any viewport size (above the breakpoint).
- Motion only happens on mouse movement; there are no loops, timers, or scroll behavior.
- Dark UI throughout: near-black `#1a1a1a` ground, white 13px uppercase mono labels, 20%-white hairlines, and the 8 saturated accent fills listed above.
