# Rotary card deck

A row of cards that reads as depth: one upright in front, the rest tilting
away either side. Tabs or a drag bring any card to the front and the whole
row turns to put it there.

## The idea

**Placement and tilt are separate.** The obvious implementation puts every
card on one wheel — rotate about a point far below and a card both travels
along an arc and leans by the same angle. That couples them: to get a
20-degree lean you are forced to accept however far that angle drags the
card down, and the fan comes out too deep and too tight.

Travel along an ellipse, and tilt on the spot:

```css
.card {
  transform-origin: 50% 50%;
  transform:
    translate(
      calc(sin(var(--angle)) * var(--rx)),
      calc((1 - cos(var(--angle))) * var(--ry))
    )
    rotate(var(--angle));
}
```

`--rx` spreads the cards sideways, `--ry` decides how far the outer ones
sink, and `--angle` is the only value JavaScript writes. CSS `sin()` and
`cos()` take the angle directly, so one number per card drives x, y and
rotation together and they can never disagree.

## The offset must be signed, and taken the short way round

```js
function offset(i, front, n) {
  const d = (i - front + n) % n
  return d > n / 2 ? d - n : d
}
```

A plain `i - front` makes the fan one-sided the moment you select either
end of the list: pick the first card and every other one piles up to its
right. A deck is a loop — the first card has the *last* one sitting to its
left.

## Traps

**Nothing may create a stacking context between the cards and their
section** if anything is blending against them. A `z-index` on a sibling
that overlaps seals the blend inside its own context, finds nothing behind
it, and the effect vanishes with no error.

**A negative `z-index` on the row takes it out of hit-testing.** It paints
behind the parent, and hit-testing follows paint — the cards look right and
nothing responds to the pointer. If you need the row painted below a
sibling, order it in the markup and reorder visually with flex `order`
instead.

**Set a z-index per card, nearest-the-front highest.** Without it they
stack in document order and whichever card happens to come later in the
list sits on top of the one being read. This stays invisible while the
cards are far enough apart not to touch, and appears the moment a narrower
screen brings them together.

**`getComputedStyle().getPropertyValue('--rx')` does not return pixels.** A
custom property comes back as its own unresolved token stream — you get the
string `calc(1838 * var(--u))` and `parseFloat` gives `NaN`, which then
flows through the drag into every card's angle. Measure the gap between two
on-screen cards instead; that is the thing the drag is actually tracking.

## Prompt

> Build a rotary card deck. Cards sit on an elliptical arc: position comes
> from `translate(sin(angle) * rx, (1 - cos(angle)) * ry)` and the lean from
> `rotate(angle)` about the card's own centre, so spread and tilt are
> independently adjustable. One custom property per card carries the angle.
> The index wraps — compute the signed offset the short way round the list
> so the fan is two-sided whichever card is in front. Give each card a
> z-index of `n - |offset|` so the front card always paints over its
> neighbours. Drive it from a tab row and from a pointer drag; measure one
> step of drag as the live gap between two neighbouring cards rather than
> deriving it from the radius.
